Append items to a growing list

Keep an append-only list such as an event log or a chat history as a real array in one entry, so adding to it is a single write that never reads the list first.


Use this when the only operation is append, such as an event log, a chat transcript, or a history feed. Items are written once and never changed, and the list is read whole with a single get().

Store the array

The KV store holds JSON natively, so pass the array itself:

await puter.kv.set('log', [
    { at: 1, event: 'opened' },
    { at: 2, event: 'edited' },
]);

const log = await puter.kv.get('log');
log.length;        // 2 (already an array, no parse step)

Append with add()

puter.kv.add() appends without the list ever travelling to your app and back:

await puter.kv.add('log', [{ at: 3, event: 'saved' }]);

One round trip. Two tabs appending at the same moment each add their own entry, so neither overwrites the other. The read-modify-write version loses one of the two writes.

add() also upserts, so a key that doesn't exist yet is created as an array:

<script src="https://js.puter.com/v2/"></script>
<script>
(async () => {
    // No seeding required. This creates 'log' as an array.
    await puter.kv.add('log', [{ at: Date.now(), event: 'opened' }]);

    // An array argument spreads: this appends two entries, not one nested array.
    await puter.kv.add('log', [
        { at: Date.now(), event: 'edited' },
        { at: Date.now(), event: 'saved' },
    ]);

    const log = await puter.kv.get('log');
    console.log(log.length);   // 3
})();
</script>

Choosing where the values land

The type of the argument decides where add() appends.

An array targets the root. Its elements are appended to the value stored under the key:

await puter.kv.set('log', [{ event: 'opened' }]);

await puter.kv.add('log', [{ event: 'saved' }]);
// → [ { event: 'opened' }, { event: 'saved' } ]

An object is a path map. Each key names a path inside the value, and each value is what to append at that path:

await puter.kv.set('profile', { name: 'Puter', tags: ['alpha'] });

await puter.kv.add('profile', { tags: ['beta', 'gamma'] });
// → { name: 'Puter', tags: ['alpha', 'beta', 'gamma'] }

Paths use dot notation, so { 'settings.labels': ['urgent'] } appends to settings.labels and leaves the rest of the object alone.

So an object is never appended as an item. It selects a target. That is why a single object is wrapped in an array, which reads as "append this one thing at the root":

await puter.kv.add('log', [{ at: Date.now(), event: 'closed' }]);

Passing that object bare would read at and event as paths. Neither exists on an array, so the call rejects with "The document path provided in the update expression is invalid for update" and the stored value is left unchanged.

Notes

← All recipes