Store Server-Side Data
Everything your app stores with puter.kv or puter.fs lives
in the signed-in user's own account, so each user only sees their own data.
Some data belongs to the whole app instead, such as a leaderboard that every
player reads and adds to. That data goes behind a
serverless worker, which runs your code on the server and keeps
the data in your account as the developer.
A worker has two Puter.js objects. Inside it,
me.puter is Puter.js signed in as you,
the worker's owner, so every request reads and writes the same store. The
user.puter object belongs to the user
who called the worker, which is how a route finds out who is calling.
Data that only its owner reads needs no worker, as in Store Data. To let specific people see a user's data, the user can share it with them, as in Share a List With Someone.
Write a Record
This leaderboard worker saves each player's score in your
key-value store with me.puter,
and asks user.puter who the player
is:
const PREFIX = 'leaderboard:score:';
router.post('/scores', async ({ request, user }) => {
if ( ! user ) {
return new Response('sign in required', { status: 401 });
}
const { score } = await request.json();
if ( typeof score !== 'number' ) {
return new Response('invalid score', { status: 400 });
}
const { uuid, username } = await user.puter.getUser();
await me.puter.kv.set(`${ PREFIX }${ uuid }`, { username, score, at: Date.now() });
return { username, score };
});
The key is built from the caller's uuid, so a request can only change its own
row, whatever the request body says.
The route gets a user only when your app calls it with
puter.workers.exec(), which sends the signed-in user's
session. A plain fetch() arrives without one, so the ! user check makes the
route sign-in only. To also answer 401 for an expired session, see
Handle Errors in a Worker.
Read the Records
To read the board, list the prefix with puter.kv.list() through
the same me.puter:
router.get('/scores', async () => {
const rows = await me.puter.kv.list(PREFIX, true);
return rows
.map(row => row.value)
.sort((a, b) => b.score - a.score)
.slice(0, 20);
});
A handler that returns a plain object or array is sent as JSON. This route
doesn't check for user, so anyone can read the board, even with a plain
fetch().
The route reads every player's row on each request. For a board with thousands
of players, also write a key per player with the score in it, zero-padded as in
Query a Collection. Then read the
top 20 by passing reverse: true and limit: 20 to
puter.kv.list().
Keep an Index in Sync shows how to keep the
extra key up to date.
Deploy the Worker
Put both routes in one file and create the worker once, which gives it a
permanent URL such as https://leaderboard-api.puter.work. See
deployment for the steps.
Call It From Your App
Call the worker with puter.workers.exec(), which takes the
same arguments as fetch():
const API = 'https://leaderboard-api.puter.work';
// Submit this player's score
const res = await puter.workers.exec(`${ API }/scores`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ score: 120 }),
});
await res.json(); // { username: 'grace', score: 120 }
// Read the whole board
const board = await (await puter.workers.exec(`${ API }/scores`)).json();
// [ { username: 'ada', score: 450, at: 1788820000000 },
// { username: 'grace', score: 120, at: 1788827048741 } ]
Every user of your app now reads and writes the same board.
Full Code
The worker file:
const PREFIX = 'leaderboard:score:';
router.post('/scores', async ({ request, user }) => {
if ( ! user ) {
return new Response('sign in required', { status: 401 });
}
const { score } = await request.json();
if ( typeof score !== 'number' ) {
return new Response('invalid score', { status: 400 });
}
const { uuid, username } = await user.puter.getUser();
await me.puter.kv.set(`${ PREFIX }${ uuid }`, { username, score, at: Date.now() });
return { username, score };
});
router.get('/scores', async () => {
const rows = await me.puter.kv.list(PREFIX, true);
return rows
.map(row => row.value)
.sort((a, b) => b.score - a.score)
.slice(0, 20);
});