Recipes

Small, finished things

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.

A counter that survives a reload

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>

A note that saves itself

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>

Upload a photo and show it

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>

A chat box

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>

Do something the browser must not

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>

List what you have stored

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

When these stop being enough

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.