puter.kv.share()

Websites Puter Apps Node.js Workers

Shares the keys under a prefix with another Puter user, or with up to 50 users in one call.

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

The other user becomes the share's holder, and you stay its owner. The keys stay in your store, and the holder reaches them through the share.

Each share has a handle. The holder finds it with puter.kv.listShared() and passes it as handle to the other puter.kv methods, in the app whose data it is or outside any app. You can pass it too, with keys relative to the prefix, the same way the holder does. Sharing with other users shows how a holder uses a share.

Syntax

puter.kv.share(prefix, recipient)
puter.kv.share(prefix, recipient, access)
puter.kv.share(prefix, recipients)
puter.kv.share(prefix, recipients, access)
puter.kv.share(options)

Parameters

prefix (String) (required)

The key prefix to share, such as 'list:42:'. The share covers every key that starts with it. The trailing : is optional, and the returned prefix always ends with one.

A share stays on the prefix it was made for. Build the prefix from an id that never changes, such as list:<uuid>:, rather than from a name.

The segments between the : separators can use only lowercase letters (a-z), digits, _, ., and -, as in list:42: or room_1.draft-a:. A prefix can't be empty or have an empty segment such as list::42:. Any other prefix, such as List:42: or café:, is rejected with invalid_kv_share_prefix.

The longest prefix depends on the app's uid and the access level. For an app uid of the usual form, app- and a uuid, the prefix without its trailing : can be at most 168 characters for 'watch', 163 for 'read', and 162 for 'write'. In the store that belongs to no app, it can be at most 199, 194, and 193. A longer prefix is rejected with invalid_kv_share_prefix, and the error message gives the limit.

recipient (String | Object) (required)

The user to share with, as a username, { username }, or { uid } with the user's uuid. A username that contains @ is rejected with invalid_recipient.

recipients (Array)

The users to share with, from 1 to 50, each in any form recipient takes. Every recipient gets a share of the same prefix at the same access level, with the same label and notify setting. A user named twice gets one share. An empty array or more than 50 recipients is rejected with bad_request.

access (String) (optional)

What the holder can do. Defaults to 'read'. Each level includes the ones before it.

options (Object)

The options form takes these properties:

Option Type Description
prefix String Required. The key prefix, as above.
recipient String | Object Who to share with, as above.
recipients Array Several users to share with, as above. Pass one of recipient or recipients. With both, recipient is ignored.
access String What the holder can do, as above. Defaults to 'read'.
label String | null Plain text the holder is shown with the share, such as 'Groceries from Dan'. Whitespace at either end is trimmed, and the rest can be at most 128 characters and 512 bytes of UTF-8. Control characters, invisible formatting characters (such as zero-width spaces and text direction marks), line breaks, and more than 3 combining marks on one character are rejected with kv_share_label_invalid. An empty string means no label.
notify Boolean Whether you and the holder can send each other notifications through the share's handle with puter.notifications.notify(), from the app whose data it is. Defaults to false. Turning it on also sends the holder a kv.share.received notification, as What the holder sees describes. Shares of the store that belongs to no app can't carry notifications.
appUuid String The app whose data to share. Outside an app, such as in a Node.js script using the user's auth token, it can name any app that exists. An app always shares its own data, and passing a different app's id fails with events_kv_handle_outside_namespace.

Without an app, puter.kv uses a store that belongs to no app. Shares of that store have an appUid of null.

Return value

A Promise that resolves to the share:

  • handle (String): The share's id. The holder passes it as handle.
  • prefix (String): The shared prefix, ending with :.
  • access (String): 'watch', 'read', or 'write'.
  • appUid (String | null): The app whose data is shared, or null for the store that belongs to no app.
  • holder (String): The holder's username.
  • label (String | null): The label the holder is shown.
  • notify (Boolean): Whether you and the holder can notify each other through the share.
  • createdAt (Number): When the share was made, in Unix seconds.

With recipients, the Promise resolves to an array with one item per recipient, in the order you passed them. Each item has recipient, the recipient as you passed it, and either the share's properties listed above or error:

  • error (Object): Why that recipient didn't get a share, as { message, code }. Its code is one of the errors about a recipient, which are invalid_recipient, subject_does_not_exist, bad_request when it's you, invalid_kv_share_prefix when a share with that user has the same prefix in different letter case, recipient_not_accepting_shares, or events_kv_handle_limit_reached when you already have 256 live shares with that user.

Errors

Any other error, such as a bad prefix, rejects the whole call. Before it shares with anyone, the call checks that all the new shares fit within your limits and rate limits, and rejects the whole call if they don't.

The promise rejects with { message, code }:

Code Meaning
invalid_recipient A username contains @, or recipient isn't a username, { username }, or { uid }.
subject_does_not_exist No user has that username or uuid.
bad_request You named yourself as the recipient, appUuid isn't the id of an app that exists, or recipients is empty or has more than 50 entries.
invalid_kv_share_prefix prefix is empty, too long, has an empty segment, or has a character other than lowercase letters, digits, _, ., and - between its separators. Also returned when one of your shares with the same user has the same prefix in different letter case.
kv_share_access_invalid access isn't 'watch', 'read', or 'write'.
kv_share_label_invalid label isn't a string or null, is too long, or contains a character it can't have.
kv_share_notify_invalid notify isn't a boolean.
recipient_not_accepting_shares The recipient blocked you, or doesn't accept shares. On an existing share, you raised its access, set a new label, or turned notify on.
account_is_not_verified Your email address isn't confirmed.
events_kv_handle_requires_account A temporary account tried to share.
events_kv_handle_limit_reached You already have as many live shares as your plan allows, in total, in this app, or with this user. With recipients, the new shares would go over your limit in total or in this app.
events_kv_handle_not_delegated An app shared without the user's consent for that prefix and level.
events_kv_handle_outside_namespace An app named another app's data.
events_kv_handle_owner_only The call came from an access token with less access than the account.
forbidden An app shared through an access token it issued.
kv_handles_disabled read and write shares are turned off on this deployment.
events_kv_handles_disabled watch shares are turned off on this deployment.
events_disabled Events, which watch shares need, are turned off on this deployment.
too_many_requests Over the rate limit, or another change to the same share was still running. Try again.

Sharing the same prefix again

Sharing the same prefix of the same app's data with the same user again changes the existing share instead of making a second one. The share keeps its handle and createdAt, and gets the access of the new call. Use this to raise or lower a holder's access. Since access defaults to 'read', a call without it sets the share to 'read', lowering a 'write' share.

The holder's subscriptions through the handle keep working and follow the new access. Lowering a share to 'watch' stops values on its events, and raising it to 'read' starts them for subscriptions that asked for values.

Leave out label to keep the current label, pass a new one to replace it, or pass null to clear it. Leave out notify to keep the current setting, or pass true or false to change it.

A call that changes nothing, because every recipient already has the share with the same access, label, and notify, counts only toward the limit on named recipients. So your app can share again each time it opens to get the handle back. See Rate Limits and Quotas for the numbers.

Concurrent calls that share the same prefix with the same user usually make one share, and every call resolves to it. In rare cases they make two. puter.kv.listSharedByMe() lists both, and you can stop the extra one with puter.kv.unshare().

What the holder sees

The holder never learns who shared or what the prefix is. The keys they read and write are relative to the prefix. The label is the only thing about the share you choose to tell them.

When a share of an app's data allows notifications, the holder gets a kv.share.received notification inside that app. It's sent when you make a share with notify: true, and when you turn notify on for an existing share, and for no other change. The holder gets at most 5 of these an hour from the same owner. A share past that is still made, without a notification. The Puter desktop doesn't show the notification, so your app is where the holder finds out about the share. The live notification needs notifications to be turned on for events on the deployment, so have the holder's app also call puter.kv.listShared() when it opens.

Who can share

  • Account. Where the deployment requires a confirmed email address, an unconfirmed account is rejected with account_is_not_verified. Sharing doesn't need a phone number, a card, or a paid plan. A temporary account can't share and gets events_kv_handle_requires_account.
  • Apps. An app can share only its own data, and only with the user's consent, which it asks for with puter.perms.request('kvShare', …). The consent names a prefix and a level. It covers that prefix and every key under it, at that level and the levels below it. A request without a level asks for 'read', the same default access has.
  • Recipient blocks. A user who blocked you, or who doesn't accept shares from anyone, can't be shared with, and the call fails with recipient_not_accepting_shares. A block added later doesn't remove an existing share. You can still lower its access, clear its label with label: null, turn notify off, or stop it. Raising its access, setting a new label, or turning notify on fails with recipient_not_accepting_shares.

See Rate Limits and Quotas for how many shares an account can make per minute and hold at once, and Key-value store for how much all the holders of your shares can read and write together.

Examples

Share a list with another user

const prefix = `list:${crypto.randomUUID()}:`;
await puter.kv.set(`${prefix}meta`, { title: 'Groceries' });

const share = await puter.kv.share(prefix, 'alice', 'write');
puter.print(`Shared with ${share.holder} as ${share.access}`);

Add a label, and lower the access later

const share = await puter.kv.share({
    prefix: 'trip:japan-2027:',
    recipient: 'alice',
    access: 'write',
    label: 'Japan 2027',
});

// Same prefix and user, so this changes the same share.
await puter.kv.share('trip:japan-2027:', 'alice', 'read');

Let you and the holder notify each other

const share = await puter.kv.share({
    prefix: `room:${crypto.randomUUID()}:`,
    recipient: 'alice',
    access: 'write',
    notify: true,
});
puter.print(`Alice can be notified through ${share.handle}`);

Share with several users at once

const results = await puter.kv.share('board:planning:', ['alice', 'bob', 'carol'], 'read');
for (const result of results) {
    if (result.error) puter.print(`Not shared with ${result.recipient}: ${result.error.code}<br>`);
    else puter.print(`Shared with ${result.holder}<br>`);
}