Server SDK
cloudbed/server is how a capsule declares its data and behavior. The whole server is one default export:
import { boolean, capsule, mutation, query, string, table } from "cloudbed/server";
const schema = {
todos: table({
text: string(),
done: boolean().default(false),
ownerId: string(),
}),
};
export default capsule({
schema,
queries: {
todos: query((ctx) =>
ctx.db.todos.where("ownerId", ctx.auth.userId).orderBy("createdAt", "desc").all()),
},
mutations: {
addTodo: mutation((ctx, text: string) => {
if (!text) throw new Error("Text required");
return ctx.db.todos.insert({ text, ownerId: ctx.auth.userId });
}),
},
});
The server is authoritative. Queries decide what each user may read; mutations validate their arguments and re-check ownership before writing. The platform independently re-validates every write against the declared schema on commit, but per-row authorization (which user may touch which row) is your capsule's job — as the ownerId checks above show.
Schema
table(fields)— declare a table.string(),number(),boolean()— field types, each chainable with.default(value).
Every row automatically carries three server-managed fields you never write yourself:
| Field | Type | |
|---|---|---|
id |
string |
unique row id |
createdAt |
string |
ISO timestamp, set on insert |
updatedAt |
string |
ISO timestamp, maintained on update |
Handlers
query(fn)— a read. Runs on subscription and automatically re-runs (and pushes to subscribed clients) after every mutation.mutation(fn)— a write. Receivesctxplus whatever arguments the client passed. Throw anErrorto reject; the client's mutation promise rejects with{ code, message }.endpoint({ method, path }, fn)— a plain HTTP handler for webhooks and non-browser clients, registered underendpoints:. Return a response descriptor built with:json(value, { status?, headers? })— JSON, default 200text(value, options?)— plain textempty(options?)— default 204redirect(url, options?)— default 302
Each handler invocation is bounded at 10 seconds of wall-clock time.
The context
Every handler receives ctx:
| Property | What it is |
|---|---|
ctx.db |
Typed database handle, one property per schema table (below) |
ctx.auth |
The calling user: userId, displayName, provider ("guest" | "google"), isGuest, isAuthenticated, and email / emailVerified / picture when signed in |
ctx.env |
Server-only env vars from .env.cloudbed.server (claimed deploys only) |
ctx.log |
info / warn / error loggers; entries are kept by the runtime (see log retention) and exposed on the deploy's inspect API |
Before sign-in, users are guests: ctx.auth.userId is guest:local by default, and opening the app with ?guest=<name> acts as the named guest guest:<name> (persisted per tab — useful for testing multi-user behavior). Ownership checks work the same whether or not the user has signed in.
Reading and writing
ctx.db.<table> exposes a small chainable query builder:
ctx.db.todos.where("ownerId", ctx.auth.userId).orderBy("createdAt", "desc").limit(50).all();
ctx.db.todos.get(id); // one row by id, or null
ctx.db.todos.insert({ text, ownerId }); // returns the full row (with id, timestamps)
ctx.db.todos.update(id, { done: true }); // partial patch, returns the updated row
ctx.db.todos.delete(id);
where filters by equality, orderBy defaults to "asc", and a query may return at most 1,000 rows — see limits.