Capsules, explained: one small full-stack app
A capsule is one small full-stack app on fuast. A server half, a client half, a tiny import surface. Here is the contract, why it is so strict, and a working sample.
A capsule is the unit of software on fuast. One capsule is one small full-stack app: a backend and a frontend, in a couple of files, with a contract small enough to hold in your head. This post walks through the whole shape.
The shape of a capsule
A capsule is a folder with two halves.
my-app/
server/index.ts schema, queries, mutations, endpoints
client/index.tsx the Preact UI, exports App
The server/ half is the backend: your database tables, the functions that read
and write them, and any HTTP endpoints for the outside world. The client/ half
is the UI: a single Preact component that calls those functions by name. That is
the entire app. No build config to babysit, no services to wire together.
The rule that makes it work: a tiny import surface
Here is the constraint that defines a capsule. Server files import only from
fuast/server. Client files import only from fuast/client and fuast/ui.
Relative imports between your own files are fine. Everything else is off the table:
no npm packages, no Node builtins, no fetch from the client to outside hosts.
That sounds limiting until you see what it buys you. Two things, and they are the whole point.
First, a capsule is easy to reason about. Every capsule in the world imports from
the same short list, so once you have read one you can read all of them. There is
no dependency tree to audit, no lockfile, no "works on my machine." The
batteries you would normally pull from npm, auth, a
database, storage, scheduled jobs, email, are already in
fuast/server.
Second, an AI can write one on the first try. A model that has read the contract
knows the complete set of things it is allowed to import and call. There is no
guessing which of forty database libraries you use, because there is one, and it is
ctx.db. This is what agent-native means in
practice: the surface is small enough that "build me an app" produces something
that actually runs.
The server half
The server is a single capsule({ ... }) call. You declare a schema with
table() and typed fields, then queries and mutations over it.
import { capsule, table, string, text, query, mutation } from "fuast/server";
export default capsule({
schema: {
notes: table({ title: string(), body: text(), authorId: string() })
.index("by_author", ["authorId"]),
},
queries: {
mine: query(async (ctx) =>
ctx.db.notes.withIndex("by_author", (q) => q.eq("authorId", ctx.auth.userId)).collect()),
},
mutations: {
add: mutation(async (ctx, title: string, body: string) => {
if (ctx.auth.isGuest) throw new Error("sign in first");
return ctx.db.notes.insert({ title, body, authorId: ctx.auth.userId });
}),
},
});
A query reads and a mutation writes. Both get a ctx
with two things that matter: ctx.db for the database, and ctx.auth for the
verified signed-in user. That ctx.auth.userId is the load-bearing detail. The
platform verifies identity for you, so you never accept a user id as an argument.
You filter rows by ctx.auth.userId and re-check ownership before you update or
delete. Trusting client-supplied identity is the one mistake the contract works
hard to prevent.
The client half
The client is one Preact component named App. It calls your server functions by
name with useQuery and useMutation, and renders with the pre-styled
fuast/ui components so you are not hand-rolling CSS.
import { h, useQuery, useMutation, useState } from "fuast/client";
import { Page, PageHeader, Card, Field, Input, Button } from "fuast/ui";
type Note = { id: string; title: string };
export function App() {
const notes = useQuery<Note[]>("mine");
const add = useMutation<[string, string]>("add");
const [title, setTitle] = useState("");
return (
<Page>
<PageHeader title="Notes" />
<form onSubmit={(e: Event) => { e.preventDefault(); void add(title, "…"); setTitle(""); }}>
<Field label="Title"><Input value={title} onInput={(e: any) => setTitle(e.currentTarget.value)} /></Field>
<Button type="submit">Add</Button>
</form>
{(notes ?? []).map((n) => <Card key={n.id}>{n.title}</Card>)}
</Page>
);
}
useQuery refetches after a mutation, so the list updates itself. The one thing
people forget is e.preventDefault() on the form submit. Include it and the app
behaves.
From capsule to running app
Once the two halves typecheck, fuast deploy builds and ships them and prints a
URL. Every capsule runs isolated from every other one, so a workspace full of
small apps stays walled off. Then you share it like a
doc and your team signs in.
That is a capsule: a server, a client, a short list of imports, and nothing else to configure. Small enough to read, strict enough for an AI to write. Build one and deploy it.
Build one this afternoon.
Tell your AI what your team needs. fuast runs it and signs everyone in.
Start free