railcode
v0.3.9
Published
Developer CLI for the multi-tenant Railcode platform: log in, scaffold, and deploy static apps.
Readme
railcode CLI
Developer CLI for the multi-tenant Railcode platform: log in, scaffold, and deploy static apps from the terminal. It authenticates with a personal API token (long-lived, revocable) and uses the existing multipart deploy API.
Install
pnpm install
pnpm build # → dist/index.js (the `railcode` bin)Then run node dist/index.js <command>, or pnpm link --global to put railcode
on your PATH.
Commands
railcode signup [--human|--agent] [--api-url <url>]
Sign up and deploy a private welcome app
railcode login [--api-url <url>] Sign in and mint a personal API token
railcode login --setup-token <token> Sign in with a one-time onboarding setup token
railcode init <app> [dir] [--template react|static] [--force]
Scaffold an app. dir defaults to ./<app>;
pass "." to scaffold into the current dir
railcode deploy [--private] [--no-source] [--force]
Build (if configured) and deploy the app here
railcode pull [<deploy>] [--app <slug>] [--dir <path>] [--force]
Download a deploy's source (default: the live one)
railcode db <list|query> ... List data connectors / run read-only SQL
railcode connections <list|create|delete> ... Manage org data connectors (admin)
railcode connector <list|catalog|link|add-mcp|tools|call|docs|fetch|access|share|...> ...
Link your accounts (Gmail, Linear, …) or custom
APIs/MCP servers, call them, share them
railcode agent <list|show|create|update|delete|run|schedule> ...
Manage org-scoped managed agents
railcode members <list|set-role|remove|add> ... Members + system roles (admin)
railcode roles <list|create|update|delete|add-member|remove-member|grants|grant|revoke|materialize|effective|catalog> ...
Org roles + the granular grants table (admin)
railcode apps <list|show|access|set-access|add-editor|remove-editor|add-viewer|remove-viewer|transfer|archive|unarchive|delete> ...
Apps + access policy (owner/admin; editors for
access/add-viewer/remove-viewer)
railcode app kv <collections|list|get|set|delete|drop> ...
Manage an app's KV store (owner)
railcode app files <list|download|upload|delete> ...
Manage an app's files (owner)
railcode llm <providers|models> The org's callable models, as apps see them
railcode llm default <model> Set the org default (including railcode/*)
railcode llm connect chatgpt Bill LLM calls to a ChatGPT plan (admin)
Signs in with a login of its own; --from-codex
reuses ~/.codex/auth.json (shared token, see help)
railcode analytics <app> [--range 1d|7d|30d|90d] Per-app pageview analytics
railcode logs <connector|service-connector|llm|email|agent> [filters]
Org observability logs (admin)Postgres connections can use an SSH bastion. Set config.ssh to
{host, port?, username, host_key} (the bastion's trusted OpenSSH public host key),
and pass --ssh-key-file ./bastion.pem to connections create. Alternatively put
the key contents in credentials.ssh_private_key. The optional key passphrase is
credentials.ssh_passphrase; the database password remains credentials.password.
Use --config-file and --credentials-file to load JSON settings. The database
host is resolved from the bastion. See Postgres SSH setup.
MotherDuck is a native data connection: use --kind motherduck with config
{region,database} and credentials {token} (a read scaling token). Its supported
Postgres wire endpoint executes DuckDB SQL; saved queries and railcode db query
use the existing grant/audit path. See MotherDuck setup.
In cloud mode the LLM discovery commands include the managed railcode provider
and its public aliases (railcode/sonnet-4.5, railcode/haiku-4.5). CLI output
never exposes the platform transport behind those aliases.
The members, roles, apps, analytics, logs, connections, and the
connector admin subcommands mirror the web console's admin surfaces — everything
an owner/admin can do in the dashboard is available from the CLI. They all target
your saved org and are gated server-side by the same capabilities as the console
(a builder gets 403; app list/show/access follow the per-app access policy).
signupworks without a prior CLI install:npx -y railcode signup. With interactive stdin and stdout it offers human or agent setup;--humanskips that choice. Without a terminal, or with--agent, it printshttps://railcode.dev/auth.mdand exits without requests, installation, or credential changes. Terminal detection is not agent identity detection. The human TUI offers email registration, existing-account login, or Google (when configured). Google opens browser signup and prints commands to return through browser CLI login. Email signup masks passwords and verification codes, supports resend, and creates or joins an eligible organization. Then it runs the onboarding survey and shows a prominent Signup complete panel with numbered next steps: runnpm install --global railcode@latest, runnpx --yes skills add Railcode-HQ/railcode-skills --global(choose agents in its prompts), then ask a coding agent to create and deploy a private welcome app using the Railcode skill. Signup does not install packages, scaffold, or deploy. Viewers finish at membership and CLI login. The API origin follows the same flag/env/saved/default precedence as login. Progress is saved under~/.railcode/signup/, separately for each instance and user (or underRAILCODE_HOME), so Ctrl-C can be resumed withsignup --human. Rerunning after completion shows the setup instructions again. Passwords and access JWTs are not persisted; only the personal token goes in the normal protected config. Local installation failures happen outside signup and their diagnostics remain visible in the user's terminal.loginopens your browser against the server URL (defaulthttps://api.railcode.app) to authorize the CLI, then mints a personal API token, resolves your organization, and saves everything to~/.railcode/config.json(dir0700, file0600). A short-lived localhost callback catches the code automatically: the authorize page probes it and only navigates there once the CLI answers. When it can't be reached — browser on another machine, or one that won't follow a link to127.0.0.1— the Railcode page shows the code instead (with a copy button); paste it into the waiting prompt. Pasting the.../railcode-cli/callback?state=...&code=...address works too, if a browser does end up on an unreachable page: the CLI reads the code out of it, checking thestatematches this login run.--paste/--no-browserskips the callback entirely and goes straight to the code page — the right default for SSH/headless boxes.login --setup-token <token>is the non-interactive onboarding path (no TTY or browser — used by the dashboard's copied setup prompt). A setup token (rc_setup_...) is a short-lived (~10 min), ONE-TIME bootstrap credential, not a personal API token: the CLI exchanges it once for a personal API token and writes the same config as a browser login. If it's expired or already used, generate a fresh prompt from the dashboard.init <app> [dir]scaffolds a directory.diris optional and defaults to./<app>; pass.to scaffold into the current directory, or any path to scaffold into it. The target need not be empty (same-named files are overwritten), but an existingrailcode.jsonis preserved unless you pass--force. By default it creates a React + Vite + Zustand starter — a personalized "Welcome to Railcode,<name>" home wired to every SDK capability: identity/team, a live to-dos list and file uploads (work with zero setup), and SQL, saved queries, service connectors, and LLM cards that call the real thing when configured and fall back to a labeled sample otherwise. Hovering a name or card title reveals the exact SDK call behind it; every write fires a toast with the call that just ran. Use--template staticfor a no-buildindex.html(loads/_api/sdk.jsand demosawait me()+db.collection().put/get) plusrailcode.jsonwith"dist": ".".deployreadsrailcode.json({ app, build?, dist? }), runs the resolved build command when needed, then uploads the output dir by app slug. On the first successful deploy for a slug, the server creates the app as part of that deploy; a failed first deploy does not leave a visible not-deployed app behind. The live URL is printed after success. It also uploads the project source alongside the built files, and sends the deploy number this folder is based on — see Source,pull, and the version marker.--no-sourceskips the source tree;--forcedeploys over a base someone else has moved past;--privatesets access to private for this deploy only.pull [<deploy>]downloads the source stored with a deploy — the live one by default, or the number the dashboard's deploy history shows (#3— numbers count from 1 per app).--app <slug>picks the app when there's norailcode.jsonhere,--dir <path>writes somewhere other than the current folder, and--forceoverwrites local files that differ. Files the deploy doesn't contain are never deleted. This requires a server with deploy source history endpoints; older servers keep accepting deploys, butpullreports that source pull is not supported yet.appsmanages apps and their access policy. Grants come in three tiers: owner, editor, and member (viewer). An editor candeploy, read/revert deploy history,pullsource, read analytics, read the access policy, and add/remove individual viewers — but not delete, archive, transfer, change the access mode or the editor list, or useapp kv/app files(those stay owner/admin). Editors can open the app in every access mode and keep the grant across mode changes, so the editor list is not an audience list;railcode apps accessgroups grants by tier to keep that clear. Use the atomicadd-editor/remove-editor(andadd-viewer/remove-viewer, restricted mode only) to change one person without rewriting the whole policy; onset-access, omitting--editorsleaves the editor list alone while--editors ""clears it.transferdemotes the previous owner to editor rather than cutting them off.ci githubwires the app up to deploy from GitHub Actions, in one command. It resolves the app fromrailcode.jsonand the repository from theoriginremote, mints a deploy token, hands it to GitHub as the repository secretRAILCODE_API_TOKENvia theghCLI (over stdin — the plaintext never reaches your screen, your shell history, or an argv), and writes.github/workflows/railcode-deploy.yml. Withoutgh, or with--no-secret, it prints the token once plus the exactgh secret setcommand.--repo owner/nameand--branch <name>override what it detects;--forceoverwrites an existing workflow file. Nothing is added torailcode.json. The workflow runsdeploy --force, treating the selected branch as the deployment authority so a committed.railcodecannot block later pushes. Setup also ignores.railcodeand removes it from Git tracking while keeping the local file (or warns if Git refuses due to staged changes). Commit the workflow,.gitignore, and staged marker removal together. CI can overwrite deployments made outside this branch, so use one deployment path for the app.tokenmanages those deploy tokens directly:create(plaintext shown once;--name <label>,--expires-in-days <n>),list,revoke <prefix>. The app comes from--apporrailcode.json. A deploy token can deploy that one app and nothing else — it cannot read data, list apps, revert a deploy, or mint another token. It is EDIT-tier to create, every owner/editor/admin of the app can revoke any of them, and it stops working if whoever created it loses edit rights on the app. Long-lived by default: an expiring CI credential breaks a pipeline with no warning, so a lifetime is opt-in.dbinspects the org's data connectors (per-org Postgres) and runs ad-hoc read-only SQL. It works straight afterrailcode login— no app and norailcode.jsonrequired — since connectors are org-scoped; it hits the app-less/api/organizations/{org}/data/*plane with your login token.railcode db list(aliasesls,connections) prints each connector'sname+engine(--jsonfor the raw array).railcode db query "<sql>"(aliassql) runs SQL against--connection(defaultdefault) and prints a table + row count;--engine <postgres|bigquery|turso|motherduck>is inferred from the connector list when omitted,--params '<json-array>'binds$1, $2, …placeholders (SQL is never interpolated),--file <path>reads SQL from a file, and--jsonprints the raw{ columns, rows, rowcount, truncated }envelope.agentmanages org-scoped managed agents when your permissions allow it. Manifests are JSON or YAML files in the same shape the API stores underagent.manifest.railcode agent listlists agents;--jsonprints rawAgentOut[].railcode agent show <name|uuid>prints one agent;--manifestprints only its manifest JSON.railcode agent pull <name|uuid> --output agent.jsonwrites the existing manifest for editing.railcode agent create --file agent.yamlcreates an agent;update <agent> --file agent.yamlreplaces an existing agent manifest;delete <agent> --yesarchives it.--visibility <org|personal>oncreate/update/testsets or changes who the agent belongs to (defaultorgon create; omitted onupdateleaves it alone). Apersonalagent is invokable and manageable by its creator alone — no grant makes it shared — andpersonal -> orgis blocked once created. Requiresagent:create(personal) oragent:create_org(org).agent show/listprintvisibility, andshowprintsOwner:for a personal agent.railcode agent test --file agent.yaml --input '{"k":"v"}'runs a draft manifest without saving;railcode agent run <agent> --input '{"k":"v"}'invokes a saved agent and prints the result. Use--tracefor the step trace and--jsonfor the raw run detail.railcode agent schedule set <agent> --cron "0 9 * * *" --timezone UTCcreates or updates the agent's one schedule.schedule show,pause,resume,delete --yes, andrun-nowoperate on that single schedule.
connectorworks with connectors: org resources with an owner and an access mode, pointing at a third-party service. A connector is either an HTTP API (called by method + path) or an MCP server / linked account (called by tool name). Distinct fromdb/connections, which are SQL data connectors.- Link one you own.
railcode connector cataloglists every provider this deployment can connect and how (api_key,oauth,token,mcp_oauth,mcp_token).railcode connector link <provider> [--method <id>] [--name <n>] [--token <t>] [--credential key=value ...] [--label "[email protected]"]creates it; an OAuth method prints a URL to open and then waits for the link to complete (--no-waitto skip).railcode connector add-mcp <name> <url> [--auth none|token|oauth]adds an arbitrary MCP server, andrailcode connector create --own ...an arbitrary HTTP API. All three need theconnector:createpermission, which every role holds by default. - Per-user.
railcode connector link <provider> --per-user [--name <n>]creates a connector that holds no credential: each member runsrailcode connector connect <name>to sign in with their own account (OAuth providers only), and an app that calls it runs on the caller's account once they approve that app on the connect page it sends them to.connector status <name> [--app <slug>]shows your link and whether you approved an app;connector disconnect <name>removes your account and every approval.link <provider>refuses to add a separate row when a per-user connector for that provider exists (--ownoverrides); crons cannot use one. - Call one.
railcode connector listshows what you can reach (kind, mode, owner, access, account);connector tools <name> [--refresh]prints an MCP/linked account's callable tools with their input schemas;connector call <name> <tool> [--args '<json>']runs one;connector fetch <path> --connector <name> [--method] [--body]proxies an HTTP request;connector docs <name>prints the API documentation an agent would read. - Share one. A row you create is restricted — yours alone — until you open
it up.
connector access <name>shows the policy;connector access <name> organization|restrictedsets the mode;connector share <name> --user <email>or--role <name>(optionally--pattern 'GET /v1/balance',--pattern 'TOOL /send_email', default everything) shares it;connector unsharetakes it back, and the next call is refused immediately.connector transfer <name> --to <email>hands it over,connector relink <name>re-runs an expired OAuth link, andconnector promote <name>(admin) makes it org-managed. An OAuth-linked row cannot be transferred or promoted — the token is your own account. - Admin.
connector list --admin,connector native,connector enable <key> --credentials '<json>',connector create,connector delete, andconnector endpoint-groups <name>manage org-wide rows and native presets.
- Link one you own.
A successful deploy can finish before a new organization's HTTPS certificate is
ready. In that case railcode deploy prints Preparing secure connection and
labels the URL as HTTPS pending. The deploy and source
are saved normally. Open the dashboard to see when the app is ready; no redeploy
is needed.
Configuration
~/.railcode/config.json:
{ "apiUrl": "...", "email": "...", "apiToken": "rc_...", "tokenPrefix": "rc_...", "orgUuid": "...", "orgSlug": "..." }API URL resolution: --api-url > RAILCODE_API_URL > saved config > prompt.
Set RAILCODE_API_TOKEN to override the saved token in CI. On a 401, the saved
token is cleared and you're prompted to railcode login again.
In CI there is no saved config at all, so the org uuid has to come from the
environment too: set RAILCODE_API_TOKEN, RAILCODE_API_URL and
RAILCODE_ORG_UUID. Only the first is a secret; the other two are identifiers.
railcode ci github writes all three for you.
Staying up to date
The CLI keeps itself current. When you run a command in an interactive terminal it
checks the npm registry — at most once every 6 hours — for a newer railcode
release within your current major and, if there is one, installs it globally
(with whichever package manager it detects: npm/pnpm/yarn/bun) and tells you:
railcode: a new version is available — 0.1.26 → 0.1.27. Updating…
railcode: updated to 0.1.27. It takes effect on your next command.It never jumps across a major version (a breaking 2.0.0 is left for you to
install deliberately), the check is best-effort (a registry/network hiccup is
silently ignored — it never blocks your command), and it's skipped in
non-interactive/CI runs so automation never self-mutates a global install. The
last_checked_for_updates timestamp lives in ~/.railcode/update-check.json, and
all of the above is printed to stderr so --json output stays clean.
Environment overrides:
RAILCODE_NO_UPDATE=1— turn auto-update off entirely.RAILCODE_UPDATE_DRY_RUN=1— check and print the command it would run, without installing (also forces the check even off-TTY; handy for scripts and testing).RAILCODE_REGISTRY_URL(falls back tonpm_config_registry) — point the check at a different npm-compatible registry.
railcode.json
{
"app": "dashboard",
"build": "npm run build",
"dist": "dist"
}build and dist are optional. Set "dist": "." for no-build static apps.
Deploy output resolution:
railcode.jsondistwins.- Otherwise
railcode.jsonbuildruns and uploadsdist/. - Otherwise a
package.jsonwith abuildscript runs<pm> run buildand uploadsdist/, where<pm>is the project's package manager — detected from apackageManagerfield or lockfile (pnpm/yarn/bun), defaulting tonpmwhen there's no lockfile. - Otherwise a root
index.htmlcan be deployed interactively; for CI set"dist": ".".
Source, pull, and the version marker
A deploy uploads two things: the built files that get served, and the
project source so it can be pulled back later. The source tree is the project
folder minus anything its .gitignore excludes, minus a built-in exclude list,
minus the resolved build-output directory. The built-in list (matched by exact
name at any depth) covers vendored deps and build output (node_modules,
.venv, venv, dist, build), VCS internals (.git, .hg, .svn),
framework/tool caches (.vite, .next, .nuxt, .svelte-kit, .astro,
.turbo, .parcel-cache, .cache, .output, .wrangler, .vercel,
.netlify, __pycache__, .pytest_cache, .mypy_cache, .ruff_cache,
.tox, .eggs, .ipynb_checkpoints, coverage, .nyc_output), agent/local
tooling state (.gstack, .playwright-mcp), editor and OS debris (.vscode,
.idea, .DS_Store, Thumbs.db, desktop.ini), env files (.env,
.env.local, .env.development.local, .env.production.local — .env.example
still ships), package-manager debug logs, and the .railcode marker. If that
tree is over the upload limit the deploy stops with a message naming the limit —
add what doesn't belong to .gitignore, or pass --no-source.
On servers without deploy source history support, the extra deploy fields are
ignored for compatibility: deploy still works, but no marker is written and
railcode pull reports that the server does not support source pull yet.
railcode pull brings a deploy's source back down. With no argument it pulls
whatever is live; railcode pull 412 pulls that deploy. It writes into the current
folder (or --dir <path>), never deletes files the deploy doesn't contain, and
stops rather than overwriting local files that differ unless you pass --force.
Both commands leave a version marker behind — .railcode, one small JSON file
recording the instance, org, app and deploy number this folder now matches:
{ "api_url": "https://api.railcode.app", "org_uuid": "…", "app": "demo",
"app_uuid": "…", "deploy": 412 }The next deploy from that folder sends the number as its base version, and the
server rejects the deploy (409) if the app has moved past it — so two people
working from the same pull can't silently erase each other. The rejection says who
moved it; resolve it with railcode pull && railcode deploy, or go over it on
purpose with railcode deploy --force.
Because the marker is written after every successful deploy — including the
first deploy of a brand-new app — plain edit-deploy iteration stays protected
without pulling. It is scoped to one app: a copied project folder, or one
re-pointed at another app or instance, sends no base version rather than the wrong
one. Keep it out of git (railcode init gitignores it for you): it is sync state
about your folder, so a colleague's git clone must not inherit it.
