Deploying
What a deploy does, secrets, the version marker, CI, app access and editors, and reading the backend function's own logs.
railcode deploy [--private] [--no-source] [--force]Run it from the app directory. It reads railcode.json, runs the CLI-owned build, uploads
the static files and the backend function as one unit, ratifies the manifest, and prints the
live URL: https://<app>.<org>.<serving-domain>/.
The two halves are one deploy. The static files and the backend module activate together and revert together. Each deploy's backend function is uploaded as its own immutable script, and the runtime ABI is recorded on the deploy row. So a revert reproduces the original executable rather than rebuilding it.
Where it lands
- Which server: resolved from
--api-url, thenRAILCODE_API_URL, then the saved CLI config, thenhttps://api.railcode.app. - Auth: the saved personal API token, or
RAILCODE_API_TOKENfor non-interactive deploys. On a401the token is cleared and you are asked to log in again. - The app is created or resolved by slug in your saved org. The first successful deploy creates the app. A failed first deploy leaves nothing behind.
- Project source is uploaded with the built files so
railcode pullcan bring it back. It honors your.gitignoreplus a built-in exclude list. Env files never ship:.env,.env.local, and similar files are excluded even when.gitignoremisses them, while.env.examplestill ships..claude/ships on purpose, so whoever pulls the source inherits the app's instructions.
Secrets
Secrets are live app state, not part of a deploy. Every activation (deploy, revert, cold revert) re-applies the current set before the switch is visible. So a revert can never bring back a rotated value.
railcode secrets set NAME # hidden prompt, or piped on stdin. Never inline
railcode secrets import .env # every NAME=value line of an env file
railcode secrets ls # names + set-at + digest, never values
railcode secrets rm NAMEValues are write-only. They can be replaced or removed, never read back. The backend function
reads them as secrets.NAME.
Caps: 64 per app, 5 KB per value.
The version marker
Every successful deploy and every railcode pull writes a small .railcode file. It records
which deploy the folder matches:
{ "api_url": "…", "org_uuid": "…", "app": "demo", "app_uuid": "…", "deploy": 3 }The next deploy sends that number back. The server refuses to publish over work the caller
has not seen. Think of it as an If-Match for deploys.
- Deploy numbers count from 1 per app.
- A stale base is a
409naming the live version, who moved it, and when. Runrailcode pull, then deploy again. - A base the app does not have is a
422naming the app's real range. This is usually a copied folder.railcode pullresyncs it. - Never commit
.railcode. It is per-folder sync state. A stale one handed to a colleague makes their next deploy claim a base that is not theirs.railcode initadds it to.gitignore.
Pulling source back
railcode pull [<deploy>] [--app <slug>] [--dir <path>] [--force]Downloads the source tree stored with a deploy: the live one by default, or a deploy number
you name. Existing local files are only overwritten with --force, and the command lists
which ones differ before it refuses. Nothing is ever deleted.
Working in a shared app
An app can have several people deploying it, so do not assume it is yours alone.
railcode apps show <app> # prints `your role`, `can manage`, `can edit`- Pull before you deploy. A
409means a colleague published after your last sync. --forceoverwrites a teammate's deploy. It marks the history row as knowingly deploying over a conflict. Pull, look at what changed, and ask before forcing.- Do not merge blind.
railcode pulllists differing files rather than overwriting them. That list is where your tree and the live one differ. Read it. - Never deploy from a tree missing
manifest.yaml. A manifest absent from the uploaded tree reads as removed. Removal only removes authority, so it auto-applies with no permission required. The app silently drops to pass-through and every declared grant is removed. The only signal is one deploy line. This is the most likely way to break a shared app.
Manifest ratification
On deploy, the platform ratifies the manifest against the deployer's own grants. The unit is the diff against what is already ratified, not the whole document:
| The diff | Outcome |
|---|---|
| Unchanged (content hash matches) | Deploys as ordinary code, whatever you hold |
| Adds operations you hold, and/or only removes operations | Auto-ratifies on the spot |
| Adds an operation you don't hold | Rejected up front with a 403. Nothing is published |
railcode manifest validate # strict local parse, before deploying
railcode manifest show <app> # the ratified doc, who ratified it, its content hashrailcode deploy also needs the org-level app:deploy capability in your role. An editor
grant alone is not enough.
App access
A new app defaults to organization access.
| Mode | Who can open it |
|---|---|
organization | Every org member (the default) |
private | Owners and editors only |
restricted | Owners and editors, plus explicitly granted members |
Org admins and owners bypass per-app access. A user who lacks access sees a 404, not a 403.
railcode apps access <app> # current mode + per-user grants
railcode apps set-access <app> --mode private
railcode apps set-access <app> --mode restricted --members [email protected],[email protected]
railcode apps add-editor <app> [email protected] # atomic; never rewrites the rest
railcode apps add-viewer <app> [email protected] # restricted mode onlyrailcode deploy --private is a one-shot action. It sets access to private for that deploy
only. It is never persisted, and it does not work from CI (setting access is a separate call
that a deploy token is refused on).
Three tiers
| Tier | Can | Cannot |
|---|---|---|
| owner | Everything below, plus delete / archive / transfer / set-access, and app kv / app files | Nothing |
| editor | Deploy, read and revert deploy history, pull source, read analytics, read the access policy, add/remove viewers | Delete, archive, transfer, change the mode or editor list, app kv / app files |
| member (viewer) | Open the app while it is restricted | Anything else |
Editor is a working right rather than an audience setting. An editor can open the app in every mode, and the grant survives mode changes.
Prefer the atomic add-editor / remove-editor over set-access --editors when you grant
one person. set-access rewrites the whole policy and can race a concurrent edit. Omitting
--editors leaves the list alone; --editors "" clears it.
Transferring ownership demotes the previous owner to editor rather than cutting them off.
Deploying from CI
Do not hand a pipeline your personal token. It carries every power you hold, on every route, and never expires. Use a deploy token: an app-scoped credential that can POST that one app's deploy route and nothing else.
railcode ci github [--app <slug>] [--repo <owner/name>] [--branch <name>]Run it inside the project. It resolves the app and repository, mints a deploy token, and
hands the plaintext to GitHub as the repository secret RAILCODE_API_TOKEN through the gh
CLI over stdin. The token never reaches your screen, your shell history, or an argv that
another process can read. Then it writes .github/workflows/railcode-deploy.yml.
The generated workflow carries all three values a runner needs, because a runner has no
~/.railcode/config.json:
- run: npx --yes railcode@latest deploy
env:
RAILCODE_API_URL: https://api.railcode.app
RAILCODE_ORG_UUID: <org uuid> # an identifier, not a secret
RAILCODE_API_TOKEN: ${{ secrets.RAILCODE_API_TOKEN }}Without RAILCODE_ORG_UUID the command stops at "No organization on file" despite a valid
token.
Manage tokens directly:
railcode token create [--app <slug>] [--name <label>] [--expires-in-days <n>]
railcode token list [--app <slug>]
railcode token revoke [--app <slug>] <token-prefix>The plaintext is shown once, at mint time. Every owner, editor, and admin of the app can see
and revoke every token on it. Tokens are long-lived by default, because an expiring CI
credential breaks a pipeline with no warning. --expires-in-days is opt-in.
What a stolen deploy token can do: replace the served code of that one app. It cannot raise that app's authority. A deployed manifest that adds power is still blocked.
After the deploy
Open the printed URL and check:
- Unauthenticated visitors go through the platform login, not a custom app login.
- The frontend loads with no console errors and no failed
/api/*calls. - A backend route returns the expected
ctx.user. - SQL, LLM, connector, and agent features show configured, empty, or disabled states cleanly, never a raw error or a hang.
Then read the backend function's own record of what happened:
railcode logs app --app <slug> # every invocation: path, status, duration, who
railcode logs app --app <slug> --follow # tail
railcode logs app <invocation_id> --app <slug> # ONE full trace, as JSONEvery invocation, http or cron, produces a record. A single trace also carries the backend
function's console lines, any uncaught error, and every governed operation with its
verdict. So a refusal appears as denied with the resource name instead of a silent failure.
invocation_id joins the gate record, the logs, and every data-plane audit row, so one
request reads as one trace. Retention is about 14 days.