Your code, on your server

cloud.fn

Storage and AI cover a lot, but anything holding a secret — charging a card, sending mail, calling a private API — has nowhere safe to live in a browser. Neither does anything the client must not be trusted to do honestly.

Define functions on the server and call them from the page. The context they receive is already authenticated and already scoped: ctx.fs and ctx.kv are that caller’s, so reading somebody else’s data is not a mistake you can make by forgetting a check.

These live in your codebase and deploy with your app. perusta does not execute uploaded code — that would need real sandboxing, and is deliberately not what this is.

Editor
Output
Press Run.
Network — real requests to this site’s own backend
No requests yet.

cloud.fn.<name>(args?)

Calls the function of that name. `args` is any JSON value and is passed through untouched; whatever the function returns comes back. An unknown name is a not_found error.

ctx.userId · ctx.appId · ctx.anonymous(

Who is calling. Taken from the verified session, never from the request body, exactly as it is for fs and kv.

ctx.fs · ctx.kv · ctx.ai(

The same namespaces, bound to the caller and without the wire — ctx.fs.readText(path) rather than a request. ctx.ai.chat collects the whole reply rather than streaming, since a function returns one value.

ctx.request(

The raw Request, for headers perusta does not model.

Defining them

Functions are a plain object on the server. The keys become the callable names, and nothing else on the object is reachable — lookup is own-property only, so `constructor` and `__proto__` are not routes.

// app/api/perusta/[...route]/route.ts
export const functions = {
  async publish(ctx, { slug }) {
    const draft = await ctx.fs.readText(`drafts/${slug}.md`);
    await ctx.fs.write(`published/${slug}.md`, draft);
    await ctx.fs.delete(`drafts/${slug}.md`);
    return { url: `/p/${slug}` };
  },
};

export const { GET, POST, OPTIONS } = toRouteHandlers(
  perustaFromEnv({ functions }),
);

Typed end to end

Pass the function map as a type argument and the calls are checked on both sides — the arguments you send and the value you get back:

import type { functions } from './server/functions';

const cloud = createClient<typeof functions>();

const { url } = await cloud.fn.publish({ slug: 'hello' });
//      ^ string, inferred from the server        ^ checked

Errors

Throw a PerustaError and its message reaches the caller. Throw anything else and they get a generic 500 — an unexpected failure must not put a stack trace or a connection string in somebody’s browser.

import { PerustaError } from 'perusta-server';

async function publish(ctx, { slug }) {
  if (!slug) throw new PerustaError('bad_request', 'slug is required');
  // …a database error here becomes a plain 500, message withheld
}

What they are not

Not a sandbox. These are functions you wrote, running in your own deployment with your own environment variables — the same trust level as any other code in your repo.

Running scripts somebody uploads at runtime is a different feature with a much larger security surface, and perusta does not do it.