Share Data Between Apps


Each app has its own separate key-value store in every user's account. If a Contacts app and a Calendar app both save a key called settings, those are two different entries, and neither app can see the other's.

Sometimes you want apps to work together, like a calendar that shows birthdays from a contacts app. With puter.kv, an app can use another app's data once the user allows it. An app can also mark entries as private so they're never shared, which is useful for things like login tokens.

Ask the User for Access

The calendar asks for access to the contacts app's data with puter.perms.request(). The user sees a prompt naming both apps and what's being asked for:

const contacts = await puter.apps.get('contacts');

const granted = await puter.perms.request('appData', {
    app: contacts.uid,
    scopes: { kv: 'read' },
});

This returns true if the user allows it and false if they don't. Once allowed, later calls return true without asking again.

There are three levels of access. Only ask for what you need:

Scope Lets your app call
read get, list
write set, add, incr, decr, update
delete del, remove, expire, expireAt

write doesn't include delete, so an app with write access can add and change entries but can't remove them. To ask for more than one, pass an array, like scopes: { kv: ['read', 'write'] }. Clearing another app's whole store with puter.kv.flush() is never allowed.

On a website, sign the user in with puter.auth.signIn() before asking. See Using another app's data for details.

Read Another App's Data

To use another app's store, pass { appUuid } as the last argument to any puter.kv call:

const options = { appUuid: contacts.uid };

const birthdays = await puter.kv.get('birthdays', options);

For puter.kv.list(), put appUuid in the options object:

const keys = await puter.kv.list({ pattern: 'contact:', appUuid: contacts.uid });

If the user didn't allow that level of access, the call is rejected with forbidden.

Write to Another App's Data

With write access, the same option works for calls that write:

await puter.kv.add('invites', [{ title: 'Team lunch', at: '2026-10-09' }], options);
await puter.kv.update('birthdays', { bob: '01-01' }, options);
await puter.kv.incr('inviteCount', 1, options);
await puter.kv.set('lastSyncedBy', 'calendar', options);

The contacts app sees the new invites in its own store like any other data.

Keep an Entry Private

Some entries should never leave your app, even if the user gives another app access to your data. A login token is a good example. When the user approves a request, they can't see what's in your store, so they wouldn't know they're handing it over.

To make an entry private, pass disableSharing: true when you save it:

await puter.kv.set('oauthToken', token, { disableSharing: true });

To other apps, a private entry looks like it doesn't exist:

Your own app reads and writes it normally. Calls that change part of an entry, like puter.kv.update() and puter.kv.incr(), keep it private.

It also works with an expiry, and with a batch write, where it applies to every entry in the batch:

const inOneHour = Math.floor(Date.now() / 1000) + 60 * 60;
await puter.kv.set('session', session, inOneHour, { disableSharing: true });

await puter.kv.set([
    { key: 'apiKey', value: apiKey },
    { key: 'refreshToken', value: refreshToken },
], { disableSharing: true });

Rewriting a Private Entry

puter.kv.set() replaces the whole entry, including the private flag. If you save a private key again without the flag, it becomes shareable:

await puter.kv.set('oauthToken', newToken);                            // now shareable
await puter.kv.set('oauthToken', newToken, { disableSharing: true });  // still private

An easy way to avoid this is to write private entries through a small helper:

const setPrivate = (key, value) => puter.kv.set(key, value, { disableSharing: true });

Notes

← All recipes