The Events API tells your app when a user's data changes. Subscribe to a file, a folder, a path that doesn't exist yet, a key in your app's key-value store, key-value data another user shared with you, or the user's notifications, and your handler runs each time it changes. You don't poll and you don't run a server.
There are two kinds of subscription:
onLocal()) belong to the open page and end when it closes. Use them to keep a UI in sync.onPersistent()) are stored on the account and keep running while your app is closed. Each change runs a handler your app published. Use them for background work, such as processing uploads as they arrive.What you watch is written as a subject string:
| Subject | Watches |
|---|---|
fs:~/Documents |
Everything under a folder, or one file by path or uid |
fs:~/inbox/*.json:add |
Matching paths, one kind of change |
kv:cart |
One key in your app's key-value store (kv:cart* for every key starting with cart) |
kv:<handle>:* |
Keys another user shared with you |
notif:app-user |
Your app's notifications for the user |
Subjects and events has the full subject syntax and the shape of each event.
With the User-Pays Model, deliveries are billed to the user holding the subscription, not to you. See Rate Limits and Quotas for limits and costs.
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<script>
(async () => {
// (1) Create a directory to watch
const dir = puter.randName();
await puter.fs.mkdir(dir);
// (2) Subscribe to everything under it
const sub = await puter.events.onLocal(`fs:${dir}`, ({ event }) => {
if (event.op === 'gap') {
puter.print(`missed some changes (${event.reason})<br>`);
return;
}
puter.print(`${event.op}: ${event.path}<br>`);
});
// (3) Change something, and the handler runs
await puter.fs.write(`${dir}/hello.txt`, 'Hello!');
// (4) Stop listening (cleanup)
setTimeout(() => sub.off(), 2000);
})();
</script>
</body>
</html>
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<script>
(async () => {
// (1) Exactly one key, and separately every key under a prefix.
const one = await puter.events.onLocal('kv:cart', ({ event }) =>
puter.print(`${event.op}: ${event.key}<br>`));
const many = await puter.events.onLocal(
`kv:${puter.appID}:cart:*`,
({ event }) => puter.print(`under cart: ${event.key}<br>`));
// (2) `cart` reaches the first, `cart:items` only the second.
await puter.kv.set('cart', { total: 0 });
await puter.kv.set('cart:items', ['apple']);
// (3) Cleanup
setTimeout(async () => {
await one.off();
await many.off();
await puter.kv.del('cart');
await puter.kv.del('cart:items');
}, 2000);
})();
</script>
</body>
</html>
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<script>
(async () => {
// (1) A directory to work in. `inbox/` below it does not exist yet.
const dir = puter.randName();
await puter.fs.mkdir(dir);
// (2) Subscribe anyway. The subscription anchors on `dir` and
// matches the rest of the path as it appears.
const sub = await puter.events.onLocal(
`fs:${dir}/inbox/trigger.json:add`,
({ event }) => puter.print(`appeared: ${event.path}<br>`),
{ onError: (error) => puter.print(`subscription ended: ${error.code}<br>`) },
);
puter.print(`anchored on ${sub.anchor.path}, matching ${sub.match}<br>`);
// (3) Create it, several directories deep
await puter.fs.write(`${dir}/inbox/trigger.json`, '{}', {
createMissingParents: true,
});
setTimeout(() => sub.off(), 2000);
})();
</script>
</body>
</html>
Publishing a handler needs an app you own, so this runs in your own app rather than in the playground.
// Once, as the app's developer
await puter.events.handlers.publish('ingestUpload', async ({ event, ctx }) => {
await fetch(ctx.endpoint, { method: 'POST', body: event.path });
});
// In your app, for each user
await puter.perms.request('backgroundEvents');
const sub = await puter.events.onPersistent({
subject: 'fs:inbox',
handlerName: 'ingestUpload',
context: { endpoint: 'https://example.com/ingest' },
});
// It outlives the page. End it when it's no longer needed.
await puter.events.unsubscribe(sub.subId);
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<script>
(async () => {
const dir = puter.randName();
await puter.fs.mkdir(dir);
const sub = await puter.events.onPersistent({
subject: `fs:${dir}`,
context: { label: 'inbox' },
});
for (const row of await puter.events.list()) {
puter.print(`${row.subject}: ${row.queue ? 'queue' : 'every listener'}`);
puter.print(` (context: ${row.contextKeys?.join(', ') ?? 'none'})<br>`);
}
await puter.events.unsubscribe(sub.subId);
})();
</script>
</body>
</html>
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<script>
(async () => {
let cursor;
let seen = 0;
do {
const page = await puter.events.fetch({
subject: 'notif:app-user',
cursor,
});
for (const event of page.items) {
puter.print(`${event.type}: ${event.notification.title ?? ''}<br>`);
seen++;
}
cursor = page.cursor;
} while (cursor);
if (!seen) puter.print('nothing missed<br>');
})();
</script>
</body>
</html>
These Events features are supported out of the box when using Puter.js:
puter.events.onLocal() - Subscribe for as long as this client is connectedsubscription.off() - End a session subscriptionputer.events.onPersistent() - Subscribe with a subscription that keeps running when your app is closedputer.events.list() - List the persistent subscriptions you holdputer.events.update() - Change a persistent subscription's context without ending itputer.events.unsubscribe() - End a persistent subscriptionputer.events.fetch() - Read the notifications you missed while nothing was listeningputer.events.handlers - Publish, list and remove the named handlers persistent subscriptions runputer.events.workers - List and destroy an app's events workerYou can see various Puter.js Events features in action from the following examples: