Recipes
Each of these is a complete HTML file. Save one next to your deployment, open it, and it works — no build step, no framework, no keys.
kv.incr happens on the server, so two tabs cannot clobber each other the way read-then-write would.
<script src="https://unpkg.com/perusta"></script>
<button id="btn">clicked 0 times</button>
<script type="module">
await perusta.auth.ensureUser();
const btn = document.getElementById('btn');
const show = (n) => (btn.textContent = `clicked ${n} times`);
show((await perusta.kv.get('clicks')) ?? 0);
btn.onclick = async () => show(await perusta.kv.incr('clicks'));
</script>Debounced so every keystroke is not a round trip. Reload and the text is still there.
<script src="https://unpkg.com/perusta"></script>
<textarea id="pad" rows="12" cols="50"></textarea>
<p id="status"></p>
<script type="module">
await perusta.auth.ensureUser();
const pad = document.getElementById('pad');
const status = document.getElementById('status');
if (await perusta.fs.exists('note.txt')) {
pad.value = await perusta.fs.readText('note.txt');
}
let timer;
pad.oninput = () => {
status.textContent = 'typing…';
clearTimeout(timer);
timer = setTimeout(async () => {
await perusta.fs.write('note.txt', pad.value);
status.textContent = 'saved';
}, 500);
};
</script>The file goes up as raw bytes, not base64. fs.url() gives you something to put straight in an img tag — the session cookie authorises it.
<script src="https://unpkg.com/perusta"></script>
<input type="file" id="pick" accept="image/*" />
<div id="gallery"></div>
<script type="module">
await perusta.auth.ensureUser();
const gallery = document.getElementById('gallery');
async function show() {
gallery.replaceChildren();
for (const entry of await perusta.fs.readdir('photos').catch(() => [])) {
const img = document.createElement('img');
img.src = perusta.fs.url('photos/' + entry.name);
img.width = 160;
gallery.append(img);
}
}
document.getElementById('pick').onchange = async (event) => {
const file = event.target.files[0];
await perusta.fs.write('photos/' + file.name, file, { mime: file.type });
await show();
};
show();
</script>No API key on this page. The reply streams in, and the whole history lives in kv so it survives a reload.
<script src="https://unpkg.com/perusta"></script>
<div id="log"></div>
<form id="form"><input id="msg" autocomplete="off" /></form>
<script type="module">
await perusta.auth.ensureUser();
const log = document.getElementById('log');
const history = (await perusta.kv.get('chat')) ?? [];
const add = (role, text) => {
const p = document.createElement('p');
p.textContent = role + ': ' + text;
log.append(p);
return p;
};
history.forEach((m) => add(m.role, m.content));
document.getElementById('form').onsubmit = async (event) => {
event.preventDefault();
const text = document.getElementById('msg').value;
document.getElementById('msg').value = '';
history.push({ role: 'user', content: text });
add('you', text);
const line = add('ai', '');
let reply = '';
for await (const chunk of perusta.ai.chat(history)) {
reply += chunk;
line.textContent = 'ai: ' + reply;
}
history.push({ role: 'assistant', content: reply });
await perusta.kv.set('chat', history);
};
</script>The API key lives on the server and never reaches this page. The function gets the caller already signed in, so there is no token to pass and no user id to check.
<!-- server: app/api/perusta/[...route]/route.ts -->
<!--
export const functions = {
async summarise(ctx, { path }) {
const text = await ctx.fs.readText(path);
const summary = await ctx.ai.chat('Summarise: ' + text);
await ctx.fs.write(path + '.summary.txt', summary);
await ctx.kv.incr('summaries');
return { summary, total: await ctx.kv.get('summaries') };
},
};
-->
<script src="https://unpkg.com/perusta"></script>
<button id="go">Summarise my note</button>
<pre id="out"></pre>
<script type="module">
await perusta.auth.ensureUser();
await perusta.fs.write('note.txt', 'Granite is very old rock.');
document.getElementById('go').onclick = async () => {
const { summary, total } = await perusta.fn.summarise({ path: 'note.txt' });
document.getElementById('out').textContent =
summary + '\n\n(' + total + ' summaries so far)';
};
</script>readdir returns real metadata, and usage tells you where you stand against any quota.
<script src="https://unpkg.com/perusta"></script>
<ul id="list"></ul>
<p id="usage"></p>
<script type="module">
await perusta.auth.ensureUser();
for (const entry of await perusta.fs.readdir('/')) {
const li = document.createElement('li');
li.textContent = entry.isDir
? entry.name + '/'
: `${entry.name} — ${entry.size} bytes`;
document.getElementById('list').append(li);
}
const { bytes, files } = await perusta.fs.usage();
document.getElementById('usage').textContent =
`${files} files, ${bytes} bytes`;
</script>Going further
The two Drive examples are the same file manager — uploads, folder tree, previews, transcription, quotas — written twice. One with Next and React, one as a single HTML file with a script tag. Same perusta calls in both.