Browse all docs

Anvil Cloud / Getting started

Examples

The anvil-cloud repository includes two examples that serve different jobs:

Example Use it for
examples/notes Canonical local-first demo: React/Vite UI, auth, database, query, mutation, endpoint, job, workflow, generated client, local Lens, and CLI JSON.
examples/aws-preview AWS-compatible smoke Cell for preview deploy, inspect, logs, auth rejection, and destroy. It stays small so preview smoke failures point at the adapter, not a sprawling demo.

Start with examples/notes when learning the Cell model. Use examples/aws-preview when validating AWS preview behavior.

The Cloud repository also includes agent contract examples under docs/examples/agents: a local Project Agent, a local Agent Cell, an AWS Bedrock Agent Cell, and provider registration. These are contract examples, not separate runnable workspaces.

anvil cloud new --template <template> scaffolds runnable starter Cells shaped like the same examples:

Template Pattern it demonstrates
crud table, query, mutation, generated client
auth auth-required query/mutation with owner-scoped rows
workflow durable workflow alongside the starter CRUD path
service supervised service alongside the starter CRUD path
agent mounted agent with an approval contract and local invocation
sandbox sandbox-required mounted agent
anvil cloud new support-cell --template agent --client headless

All templates keep the starter listTodos/addTodo generated client path working so they can be checked, built, and extended by agents without first repairing the scaffold.

Notes example

The notes Cell defines:

  • a notes table with title, body, archived, and ownerId
  • a public status query
  • an authenticated listNotes query
  • authenticated createNote and archiveNote mutations
  • a public /api/health endpoint
  • a summarizeNote job
  • an onboardUser workflow
  • a React/Vite client that uses generated query/mutation metadata
import {
  app,
  boolean,
  endpoint,
  job,
  mutation,
  query,
  table,
  text,
  userId,
  workflow,
} from "@anvil-cloud/runtime";

export default app({
  schema: {
    notes: table({
      title: text().min(1).max(120),
      body: text().max(2000).optional(),
      archived: boolean().default(false),
      ownerId: userId(),
    }),
  },
  capabilities: {
    database: true,
    jobs: true,
    workflows: true,
  },
  queries: {
    status: query({
      auth: "public",
      handler: async () => ({ ok: true, cell: "notes" }),
    }),
    listNotes: query({
      auth: "required",
      handler: async (ctx) => {
        return ctx.db.notes.where("ownerId", "=", ctx.auth.requireUser()).all();
      },
    }),
  },
  mutations: {
    createNote: mutation<{ title: string; body?: string }>({
      auth: "required",
      handler: async (ctx, input) => {
        const note = await ctx.db.notes.insert({
          title: input.title,
          body: input.body ?? "",
          archived: false,
          ownerId: ctx.auth.requireUser(),
        });

        await ctx.jobs.enqueue("summarizeNote", { noteId: note.id });
        return note;
      },
    }),
  },
  endpoints: {
    health: endpoint({
      method: "GET",
      path: "/api/health",
      auth: "none",
      handler: async () => ({ ok: true, cell: "notes" }),
    }),
  },
  jobs: {
    summarizeNote: job({
      handler: async (ctx, payload) => {
        await ctx.log.info("Summarize note job received", payload);
        return { summarized: true };
      },
    }),
  },
  workflows: {
    onboardUser: workflow({
      steps: [
        {
          name: "seedWelcomeNote",
          handler: async (ctx) => {
            return ctx.db.notes.insert({
              title: "Welcome to Anvil Notes",
              body: "This note was created by a local workflow.",
              archived: false,
              ownerId: ctx.auth.requireUser(),
            });
          },
        },
      ],
    }),
  },
});

The full source lives in examples/notes/src/cell.server.ts. The client source is in examples/notes/src/client.

Common patterns

Mounting a Cell Agent

import { app, defineAgent, endpoint } from "@anvil-cloud/runtime";

const support = defineAgent({
  name: "support",
  instructions: "./agents/support/instructions.md",
  model: { provider: "local", model: "stub" },
  capabilities: {
    cells: ["read"],
    filesystem: "none",
    secrets: "none",
  },
  approvals: {
    requiredFor: ["email.sendExternal"],
  },
});

export default app({
  agents: { support },
  endpoints: {
    chat: endpoint({
      method: "POST",
      path: "/api/chat",
      auth: "required",
      agent: "support",
      handler: async () => ({ ok: true }),
    }),
  },
});

Validate and invoke it locally:

anvil cloud agents validate --json
anvil cloud agents manifest --json
anvil cloud agents invoke support --input "Review this Cell" --json

Filtering with where clauses

const recent = await ctx.db.notes
  .where("createdAt", ">", Date.now() - 86400000)
  .all();

Supported operators: =, !=, >, >=, <, <=.

Requiring auth

const userId = ctx.auth.requireUser();

Throws AUTH_REQUIRED if no user is authenticated.

Checking roles

if (!ctx.auth.hasRole("admin")) {
  throw new Error("FORBIDDEN");
}

Using environment variables

const apiKey = ctx.env.require("OPENAI_API_KEY");

Never use process.env directly. Guard rejects it.

Logging

ctx.log.info("Note created", { noteId: note.id });

Logs are written to .anvil/local/logs.ndjson locally and CloudWatch in AWS preview.

File uploads

capabilities: {
  files: {
    publicRead: false,
  }
}

// in a mutation or endpoint
await ctx.files.put("uploads/avatar.png", buffer);
const file = await ctx.files.get("uploads/avatar.png");

Outbound fetch

capabilities: {
  outboundFetch: {
    allow: ["api.stripe.com"],
  }
}

// Guard checks that fetch calls match declared hosts
const res = await fetch("https://api.stripe.com/v1/customers");

Running the example

cd anvil-cloud/examples/notes
node ../../packages/cli/dist/index.js check --json
node ../../packages/cli/dist/index.js dev --port 8787 --client-port 5173

Create a local user and token:

node ../../packages/cli/dist/index.js auth add-user local_demo \
  --email demo@example.test \
  --roles admin \
  --json

TOKEN=$(node ../../packages/cli/dist/index.js auth token local_demo --json | jq -r .token)

Call the runtime directly:

curl -X POST http://localhost:8787/_anvil/mutation/createNote \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"input":{"title":"Hello","body":"World"}}'

curl -X POST http://localhost:8787/_anvil/query/listNotes \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"input":{}}'

Or run the repeatable smoke verifier:

cd anvil-cloud
pnpm verify:notes-local

Inspecting the example

While anvil cloud dev is running:

node ../../packages/cli/dist/index.js lens --json
node ../../packages/cli/dist/index.js inspect --local --json
node ../../packages/cli/dist/index.js logs --local --json
node ../../packages/cli/dist/index.js db dump notes --local --json

Read next