Store a Large Collection


A list in one key, as in Store a Small List, holds a few thousand small items. For a collection that can grow past that, such as every order in a store, give each record its own key in puter.kv. There's no limit on how many records you keep, each one can expire on its own, and your app loads the collection a page at a time.

Store a Record

Write each record under its own key with the puter.kv.set() method. Start the key with the collection's name and end it with the record's ID:

const id = crypto.randomUUID();

await puter.kv.set(`todo:${ id }`, { text: 'Buy milk', done: false });

The ID works like a primary key. It has to be unique inside the collection, and it's how you reach the record later.

Read, Change or Delete a Record

To read one record, use the puter.kv.get() method with its full key. To change a field, use puter.kv.update(), and to replace the record, use puter.kv.set() again. To remove it, use puter.kv.del(). None of them read or write the rest of the collection:

const key = `todo:${ id }`;

const todo = await puter.kv.get(key);
await puter.kv.update(key, { done: true });
await puter.kv.set(key, { text: 'Buy oat milk', done: false });
await puter.kv.del(key);

Load a Page

To load records, use the puter.kv.list() method with the collection's prefix as the pattern. Set returnValues: true to get each record's value with its key, and a limit for the page size:

const page = await puter.kv.list({
    pattern: 'todo:',
    returnValues: true,
    limit: 50,
});
// { items: [{ key: 'todo:1b9d…', value: { text: 'Buy milk', done: false } }, …], cursor: '…' }

The records come back sorted by key. Query a Collection shows how to design keys that filter and sort the way a view needs.

Load More

When more records are left, the page has a cursor. Pass it back to load the next page, for example from a "Load more" button:

const next = await puter.kv.list({
    pattern: 'todo:',
    returnValues: true,
    limit: 50,
    cursor: page.cursor,
});

The last page has no cursor. Check for the cursor instead of the number of items, since a page can hold fewer than limit records while more are left, for example when some records expired. To have the page filled up to limit when it can be, add fetchUntilFull: true.

Read Every Record

To go through the whole collection, such as for an export or a migration, add stream: true. The call then returns the pages one at a time for a for await loop:

let done = 0;

for await ( const page of puter.kv.list({ pattern: 'todo:', returnValues: true, stream: true }) ) {
    done += page.items.filter(({ value }) => value.done).length;
}

Every page reads only the keys that match the pattern, so a narrow prefix keeps a listing fast. A listing with no pattern reads every key your app stored for the user.

Expire a Record

To have a record delete itself later, pass a Unix timestamp in seconds as the third argument of puter.kv.set(). Each record counts down on its own:

const inOneDay = Math.floor(Date.now() / 1000) + 60 * 60 * 24;

await puter.kv.set(`todo:${ id }`, { text: 'Buy milk', done: false }, inOneDay);

Expired records drop out of listings on their own. Store Temporary Data covers expiry in more detail.

← All recipes