Skip to main content
This walkthrough gets a Pylon server running with a schema, one policy, and one server function. You’ll then hit the API with curl to see it work end-to-end. Once you want a realtime UI on top, point the React SDK at the running server or clone one of the example apps.

1. Install the CLI

Pylon is currently installed from source. A curl install script and Homebrew tap are on the roadmap.
git clone https://github.com/pylonsync/pylon
cd pylon
cargo install --path crates/cli --locked
Requires Rust ≥ 1.85 and Bun ≥ 1.0. Verify:
pylon --version

2. Scaffold a new app

pylon init notes
cd notes
This creates two files:
notes/
  app.ts             # schema + manifest entry point
  tsconfig.json      # TypeScript config
You’ll add a functions/ directory by hand in step 4.

3. Define the schema

Replace app.ts with:
import { entity, field, policy, buildManifest } from "@pylonsync/sdk";

const Note = entity(
  "Note",
  {
    title: field.string(),
    body: field.richtext(),
    authorId: field.string(),
    updatedAt: field.datetime(),
  },
  {
    indexes: [{ name: "by_author", fields: ["authorId"], unique: false }],
  },
);

const notePolicy = policy({
  name: "note_public",
  entity: "Note",
  allowRead: "true",
  allowInsert: "auth.userId == data.authorId",
  allowUpdate: "auth.userId == data.authorId",
  allowDelete: "auth.userId == data.authorId",
});

const manifest = buildManifest({
  name: "notes",
  version: "0.1.0",
  entities: [Note],
  policies: [notePolicy],
  queries: [],
  actions: [],
  routes: [],
});

console.log(JSON.stringify(manifest, null, 2));
The trailing console.log is required — pylon dev captures stdout as the manifest.

4. Add a server function

Create functions/createNote.ts:
import { mutation, v } from "@pylonsync/functions";

export default mutation({
  args: {
    title: v.string(),
    body: v.string(),
  },
  async handler(ctx, args) {
    if (!ctx.auth.userId) throw ctx.error("UNAUTHENTICATED", "log in first");

    const id = await ctx.db.insert("Note", {
      title: args.title,
      body: args.body,
      authorId: ctx.auth.userId,
      updatedAt: new Date().toISOString(),
    });
    return { id };
  },
});
The filename becomes the RPC name — this will be callable at POST /api/fn/createNote. Install @pylonsync/functions and @pylonsync/sdk so bun can resolve the imports at runtime:
bun add @pylonsync/sdk @pylonsync/functions

5. Run the server

pylon dev app.ts
Pylon watches app.ts and functions/*.ts, auto-migrates the database (SQLite at .pylon/dev.db by default, Postgres if DATABASE_URL is set), and serves the API on http://localhost:4321. Expected output:
✓ notes v0.1.0 — 1 entities, 0 queries, 0 actions, 1 policies, 0 routes
  Server:   http://localhost:4321
  Database: .pylon/dev.db

6. Hit the API

Create a guest session:
curl -X POST http://localhost:4321/api/auth/guest
# → { "token": "eyJ…", "user_id": "usr_01H…" }
Call the mutation (replace TOKEN + USER_ID with the values from above):
curl -X POST http://localhost:4321/api/fn/createNote \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title":"First note","body":"hello from Pylon"}'
# → { "id": "not_01H…" }
List the rows:
curl http://localhost:4321/api/entities/Note
# → [{ "id": "not_01H…", "title": "First note", ... }]
You now have a working Pylon backend with a typed schema, row-level policy, RPC function, and HTTP API.

Next steps

Entities

All the field types and index options.

Policies

How row-level access rules work.

Functions

Queries, mutations, actions, validators.

Live queries

How db.useQuery stays in sync.
Want a full React UI on top? The chat example is the shortest end-to-end reference — schema, functions, Vite app, live subscriptions.