Share a List With Someone


KV sharing is in beta. Method shapes, limits, and behavior may change between releases.

puter.kv keeps each user's data in their own Puter account, where only they can reach it. A shared grocery list or to-do list needs two people working on the same data. With puter.kv.share(), a user can give another Puter user access to the keys under a prefix. The keys stay in the owner's account, and the other person reads and writes them from their own.

In this recipe, Dan makes a grocery list in your app and shares it with Alice, and then both of them edit it.

You want to… Use
Let another user see or change part of your app's data puter.kv.share(), as in this recipe
Let another app of the same user use your data Share Data Between Apps
Share files Share a File
Show data to anyone, signed in or not Hosting or a worker. A share is for one Puter user.
Let each person edit only their own entries A part for each member, or a worker that checks each write

Give the List Its Own Prefix

A share covers every key that starts with its prefix. Build each list's prefix from an id, like list:<uuid>:. A share stays on the prefix it was made for, so a prefix made from the list's name would stop matching when the user renames the list.

const user = await puter.auth.getUser();
const prefix = `list:${crypto.randomUUID()}:`;

await puter.kv.set(`${prefix}meta`, { title: 'Groceries', from: user.username });
await puter.kv.set(`${prefix}item:${crypto.randomUUID()}`, { text: 'Milk', done: false });

Each item gets its own key with a random id. When Dan and Alice add items at the same time, each write goes to a different key, so both items are kept. With all the items in one key, the last write would replace the other person's change.

The meta key describes the list. The person you share with isn't told who shared it or what its prefix is, so meta is where their app finds a title to show.

Ask Once for Permission to Share

Your app needs the user's permission to share their data. Ask for it with puter.perms.request(), naming the list: prefix so one approval covers every list the user makes:

const allowed = await puter.perms.request('kvShare', { prefix: 'list:', access: 'write' });
document.querySelector('#share-button').hidden = !allowed;

The user sees a prompt asking whether your app may let other Puter users it picks read and change the list: section of its data. The call returns true once they allow it, so the share button only shows then. Later calls return true without asking again. Share Data From Your Backend covers this permission in more detail.

Share the List

Pass the prefix, the other person's username and the access level to puter.kv.share():

const share = await puter.kv.share({
    prefix,
    recipient: 'alice',
    access: 'write',
    label: 'Groceries from Dan',
});

With write access, Alice can add, change and delete any key under the prefix, meta included. The label is plain text Alice is shown with the share, and the only thing about it you choose to tell her.

The share is of your app's data, so Alice uses it in your app, and her other apps can't see it. Each of them pays for their own reads and writes, including the ones Alice makes through the share.

Find the Lists Shared With You

On Alice's side, puter.kv.listShared() lists the shares made with her in your app. Each one has a handle, which she passes to the other puter.kv methods to use that share. Read meta through each handle to show the lists:

const { items } = await puter.kv.listShared();

for (const { handle, label } of items) {
    const meta = await puter.kv.get('meta', { handle });
    const row = document.createElement('li');
    row.textContent = meta?.title ?? label ?? 'Shared list';
    row.dataset.handle = handle;
    document.querySelector('#lists').append(row);
}

Set the text with textContent, so a title or label shows as plain text and is never read as HTML. Anyone with write access can change meta, so use it for display only. What a person can do comes from the share's access.

A share made while Alice has your app open arrives as a kv.share.received notification, in your app only. It carries the handle and label:

await puter.events.onLocal('notif:app-user', ({ event }) => {
    if (event.type === 'kv.share.received') {
        addList(event.notification.handle, event.notification.label);
    }
});

Read and Write Through the Handle

Alice passes a list's handle with every call, and uses keys relative to the shared prefix. She writes item:<id>, and it's stored as list:<uuid>:item:<id> in Dan's account:

const options = { handle };

await puter.kv.set(`item:${crypto.randomUUID()}`, { text: 'Eggs', done: false }, options);
const entries = await puter.kv.list('item:*', true, options);

The keys that puter.kv.list() returns are relative too, like item:<id>.

Use One Helper for Both Sides

Dan uses full keys and no handle, while Alice uses relative keys and a handle. This helper hides the difference, so the rest of your app works the same for both of them:

const openList = ({ prefix = '', handle }) => {
    const options = handle ? { handle } : {};
    const relative = (key) => key?.slice(prefix.length);
    return {
        get: (key) => puter.kv.get(prefix + key, options),
        set: (key, value) => puter.kv.set(prefix + key, value, options),
        del: (key) => puter.kv.del(prefix + key, options),
        list: async (pattern) => (await puter.kv.list(prefix + pattern, true, options))
            .map(({ key, value }) => ({ key: relative(key), value })),
        watch: (pattern, onChange) => puter.events.onLocal(
            `kv:${handle ?? puter.appID}:${prefix}${pattern}`,
            ({ event }) => onChange({ ...event, key: relative(event.key) }),
            { includeValue: true },
        ),
    };
};

const dansList = openList({ prefix });     // Dan, who owns the list
const alicesList = openList({ handle });   // Alice, through the share

await alicesList.set(`item:${crypto.randomUUID()}`, { text: 'Bread', done: false });
const items = await dansList.list('item:*');

Dan always uses the prefix. A handle stops working when its share stops, so the owner's code works without one. His subject names your app with puter.appID, since a kv: subject for a key that contains : needs the app id in front, as described in Key-value data.

Watch for Changes

The watch() part of the helper calls puter.events.onLocal(), so each person sees the other's changes as they happen:

await alicesList.watch('item:*', (event) => {
    if (event.op === 'gap') return reloadList();
    if (event.self && !event.viaWorker) return;
    showItem(event.key, event.value);
});

The self field is true for changes the current user made, so the check skips their own writes, which your app already shows. The viaWorker field is true when a worker made the change as that user, so those changes still show up. A gap event means some changes weren't delivered, for example during a reconnect, so reload the list. The event lists every field.

When Dan and Alice change the same item at about the same time, the last write wins.

Keep Private Entries Out

Entries saved with disableSharing: true are hidden from Alice even inside the shared prefix. A write to one through the share is refused, which tells Alice that key exists, but never its value. Keep private entries under a prefix that isn't shared, such as private:.

To see who has access to a list, or to stop sharing it, see Manage Who Has Access to Shared Data.

← All recipes