Skip to main content
A function is server TypeScript, RPC-callable from the client. Pylon runs them in a Bun process managed by the runtime. Three flavors:
  • Query — read-only, can subscribe to changes
  • Mutation — writes through the transactional path
  • Action — arbitrary side effects (HTTP calls, emails, file ops)

Writing a function

Create a file in functions/:
The filename becomes the RPC name — createMessage is callable at POST /api/fn/createMessage.

Context object

ctx gives you:
The three flavors expose different context surfaces:
  • Queryctx.db (read-only), ctx.auth, ctx.env, ctx.requireMember.
  • Mutation — the above plus writes on ctx.db, ctx.scheduler, ctx.error, ctx.llm, ctx.connections, ctx.stream.
  • Actionno ctx.db. Reach the database through ctx.runQuery(name, args) / ctx.runMutation(name, args) (each runs as its own transaction). Actions also get ctx.scheduler, ctx.error, ctx.email, ctx.llm, ctx.connections, and ctx.request (raw HTTP request — the only ctx that has it, so webhook signature checks live in actions).
A mutation handler IS the transaction — every ctx.db.* call inside the handler shares one BEGIN/COMMIT, and a thrown error rolls everything back atomically. There’s no ctx.db.transact([...]) because the handler already wraps your writes; if you need batch atomicity from outside a mutation, use the HTTP /api/transact endpoint or call runMutation from an action.

Auth (secure by default)

Every function declares who can call it. The framework enforces this before the handler runs — a missing if (!ctx.auth.userId) check no longer leaks data, because the runtime made the check first.
Four modes: When the mode doesn’t match the caller, the request is rejected before the handler runs — 401 AUTH_REQUIRED for "user" / "guest", 403 FORBIDDEN for "admin". Admin sessions bypass every mode (same convention as policies).
Why this matters for actions specifically: policies gate ctx.db.* reads and writes, but policies don’t gate action handlers. An action that charges Stripe, sends email, or hits a private API has no default protection — except auth. Forgetting this single field is the canonical action-shaped vulnerability in any TypeScript backend. Pylon makes it the secure default. internal: true functions ignore auth — they’re unreachable over HTTP and inherit the wrapping handler’s context.

ctx.db honors policies (strict mode)

By default, ctx.db.* inside a function bypasses entity policies — server code is trusted. That’s the historical Pylon behavior and it’s still the default. Strict mode flips the default: every ctx.db.get/query/insert/update/delete/lookup/search runs through the policy engine using the function’s caller auth, exactly as if the same operation came in through /api/entities/*. Enable per deploy:
When strict mode is on, the canonical Pylon-shaped IDOR — “I wrote getRecording.ts that does ctx.db.get('Recording', args.id) and forgot to check tenant ownership” — becomes impossible. The policy on Recording fires, sees that the caller isn’t a member of the row’s org, and the call returns POLICY_DENIED before the row leaves the database. For the legitimate cross-tenant cases (admin tools, webhook receivers post signature verification, scheduled cron sweeps), use the explicit escape hatch:
Rules of thumb:
  • Plain ctx.db.* — the default. Acts as the caller. Use for anything that should reflect the user’s view of the data.
  • ctx.db.unsafe.* — explicit bypass. Use for webhooks, cron sweeps, admin tooling, and anything that genuinely needs cross-tenant reads. Every call should have a justifying comment.
  • Admin contexts bypass strict modeauth.isAdmin === true skips the gate the same way it bypasses entity-route policies. Ops scripts and the ctx.auth.elevate({ admin: true, reason: "..." }) path inside verified webhooks still work everywhere (the reason is mandatory and audited).
Strict mode is opt-in for now (one minor cycle) so existing apps can migrate at their own pace. The rollout plan:
  1. Now (v0.3.161+)ctx.db.unsafe.* is callable. Mark known cross-tenant paths with it. Plain ctx.db.* still bypasses policies (existing behavior); strict mode is opt-in via env.
  2. v0.4 — strict mode default-on. Apps that didn’t migrate get POLICY_DENIED on the calls that genuinely need cross-tenant access; the fix is to mark those unsafe.

Validators

v.* describes expected argument shapes:
See SDK reference for the full list.

Queries

Queries are live by default — the React client subscribes and re-runs on relevant changes. See Live queries.

Actions

Use for side effects outside the database:
Actions have no ctx.db. They reach the database through ctx.runQuery(name, args) / ctx.runMutation(name, args) — each of those runs as its own transaction, but the action as a whole is not atomic. If a sequence of writes must commit or roll back together, do them inside a single mutation and have the action call it once.

Calling functions from the client

Errors

Throw typed errors that propagate to the client with structured codes:
The client receives { code, message } and can render different UI per code. See Error codes for the canonical list.

Next

Live queries

How query subscriptions stay in sync.

Validators

All argument shapes v.* supports.