fuast is in alpha. Read the capsule contract →
fuast
← Blog
GuideJuly 24, 2026

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

Keep reading