Core concepts
Organizations, apps, backend functions, managed agents, connectors, and the authority model. The vocabulary the rest of the docs assumes.
The vocabulary the rest of these docs assumes.
Organization
Everything is scoped to an organization. Apps, data connections, saved queries, connectors, agents, roles, and grants all belong to one org. The CLI works against the org saved at login.
There is no anonymous access. Every person who opens an app is a signed-in member. These are internal tools, not public products.
The Railcode proxy
We run a proxy in front of every deployment. A request for an app passes two gates before the app is served. First: is this person a member of the org? Second: do they have access to this app, directly or through one of their roles? Passing the first gate is never a shortcut through the second. Someone who fails either gate sees a 404.
Because of the proxy, every app is behind company auth by default. The frontend can call the backend function with no API key, and the backend function receives a caller it can trust. See how Railcode works.
App
An app is a static frontend plus a backend function, deployed and versioned as one unit. It is
served at <app>.<org>.<serving-domain>.
The two halves activate and revert together. A page from one deploy never talks to a backend function from another.
Apps carry a platform-assigned generation. Everything created today is generation 2 ("apps v2"). Generation 1 is the legacy browser-SDK shape; see legacy.
Backend function
The backend function is the app's server side: one self-contained ES module that imports
@railcode/sdk and handles requests under /api/*.
It is also the app's principal. Platform calls run under the app's own ratified authority rather than the caller's. This is what lets a member use a feature through the app that they could not perform directly.
The verified caller
ctx.user // { id, email, name, is_admin, roles } | null
ctx.trigger // "http" | "cron"
ctx.invocationId // this invocation's id, and your idempotency keyThe platform verified ctx.user at the proxy and embedded it in the invocation token. App
code cannot fake its caller. It is null only on cron triggers.
This is the foundation of every authorization decision you write. Never take identity or ownership from a request body.
Authorization
Two different things, often confused:
- App access decides who may open the app:
organization,private, orrestricted. - Everything after that is a check you write in the backend function against
ctx.user: who may edit, approve, delete, or see whose records.
The key/value store is flat and enforces nothing. A key prefix is a convention. The check is what makes it real.
The manifest: authority
manifest.yaml sits beside railcode.json. It declares which privileged operations the app
performs:
run_as: app
llm: true
email: true
saved_queries: [revenue_by_month]
connectors:
stripe: ["GET /v1/charges"]
agents: [digest]
egress: [api.example.com]
crons:
- schedule: "0 6 * * *"
path: /api/refreshTwo rules matter:
- Declare only what the backend function uses. For
agentsandconnectors, a missing declaration is a 403 refusal rather than pass-through. - A deploy cannot grant itself power. The platform ratifies the manifest against the deployer's own grants. It checks the diff against what is already ratified. A diff that adds an operation you do not hold is rejected up front, and nothing is published.
Storage
Each app has one flat key/value store and one file store. Only the backend function can reach them. The store has no built-in per-user partitioning. You design the keys and you write the ownership check. See data and files.
Data connections and saved queries
A data connection is a Postgres, BigQuery, or Turso database that an admin configures.
A saved query is a named, versioned SQL template that an admin publishes against one connection. Apps and members invoke it by name with typed params. This is the grant-gated alternative to embedding SQL in an app. Prefer saved queries; ad-hoc SQL authority is deliberately scarce.
Saved queries can reference :_ctx_user_id, :_ctx_user_email, and :_ctx_org. The server
injects them from whoever invokes the query. That gives you per-caller row scoping that the
caller cannot forge.
Connectors
A connector is a third-party account as an org resource with an owner and an access mode. One kind of object covers both a shared team credential and an account somebody linked for themselves.
- http connectors are a method/path proxy:
connector("stripe").fetch("/v1/charges"). - mcp connectors expose tools called by name:
connector("gmail-jp").call("send_email", {...}).
The connector holds the credential. Your backend function never sees it. A row someone links
starts restricted: visible to them, admins, and anyone they share it with.
Names are not provider ids. When two people link the same provider, the row gets a suffix
(gmail-jp, gmail-sebastian). Always read the name from railcode connector list before
you write it into a manifest.
Linking happens outside the app. The person runs railcode connector link gmail or links from
the dashboard. Do not build a connect flow into an app.
Managed agents
A managed agent is server-side AI that runs under its own ratified manifest. Each run gets a code sandbox and a durable, auditable record. Use one whenever the work must read or write a file, run code, outlive the request, run on a schedule, or answer from Slack.
Agents have a visibility: org (shared, invokable by anyone with a grant) or personal
(owned and invokable by its creator alone, admins included).
Agents cannot own storage. They read and write through an app they have data access to, usually a companion app.
Roles and grants
Members hold a tier (owner / admin / member) plus any custom roles. Grants are additive and
name resource types: app, llm, email, sc_endpoint, connector, saved_query, and
agent.
App authority declared in manifest.yaml is ratified against this same grant model. This is
why a deploy can be blocked for adding authority the deployer does not personally hold.
The version marker
Every successful deploy and every railcode pull writes a .railcode file. It records which
deploy the folder matches. The next deploy sends that number back, and the server refuses to
publish over work the caller has not seen.
A stale base is a 409 naming the live version, who moved it, and when. It means a colleague
published after your last sync. Run railcode pull, then deploy again.