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-value key, or the user's notifications, and your handler runs each time it changes. There's nothing to poll and no server to run.
There are two kinds of subscription:
onLocal()) live on 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, running a handler your app publishes. 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 a prefix) |
notif:account |
The user's notifications |
See Subjects and events for the full subject syntax, the event shape, and how to share key-value events with another user.
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 — 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>
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<script>
(async () => {
// (1) An app of your own to publish the handler under
const name = `ingest-${puter.randName()}`;
const app = await puter.apps.create(name, `https://example.com/${name}`);
// (2) Publish the handler. It closes over nothing — everything it
// needs arrives as `ctx`.
await puter.events.handlers.publish(
'ingestUpload',
async ({ event, ctx }) => {
await fetch(ctx.endpoint, {
method: 'POST',
body: JSON.stringify({ path: event.path, key: ctx.apiKey }),
});
},
{ appUid: app.uid },
);
// (3) Subscribe. `context` is read now and never again.
const dir = puter.randName();
await puter.fs.mkdir(dir);
const sub = await puter.events.onPersistent({
subject: `fs:${dir}`,
handlerName: 'ingestUpload',
context: { endpoint: 'https://example.com/ingest', apiKey: 'k-123' },
});
puter.print(`watching ${dir} as ${sub.subId}<br>`);
// (4) It outlives this page. End it explicitly when you are done.
await puter.events.unsubscribe(sub.subId);
})();
</script>
</body>
</html>
<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.delivery}`);
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:account',
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 this caller holdsputer.events.unsubscribe() - End a persistent subscriptionputer.events.fetch() - Read what a subject recorded 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: