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.
This is for apps of the same user. To share data with another user, see Share a List With Someone.
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, read, write and delete. Ask only for
what you need. The methods each one allows are listed in
Using another app's data. A write grant doesn't include
delete, so the app 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 });
A call the user didn't allow 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 with
puter.kv.set():
await puter.kv.set('oauthToken', token, { disableSharing: true });
To other apps, a private entry looks like it doesn't exist:
puter.kv.get()returnsnull, just like a missing key.puter.kv.list()leaves it out.- Changing or deleting it is rejected with
forbidden. - Events for it aren't delivered.
It's also hidden from other users the data is shared with, as
Share Only Part of the Data
shows. 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. Private doesn't mean encrypted. The entry is stored in the user's
account like the rest of your app's data.
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 });
Only the app that owns an entry can make it private, so passing
disableSharing together with appUuid is rejected with bad_request.
Save a Private Entry Again
puter.kv.set() replaces the whole entry, including the private
flag, so pass disableSharing: true every time you save a private key. A small
helper keeps it from being left out:
const setPrivate = (key, value) => puter.kv.set(key, value, { disableSharing: true });
await setPrivate('oauthToken', newToken);