--- name: create-railcode-app description: Build, modify, debug, test, and deploy Railcode apps end-to-end. Use when creating a Railcode app from an idea, scaffolding with the Railcode CLI, writing a backend worker with @railcode/sdk, wiring a frontend to worker routes, declaring app authority, testing with railcode dev, migrating a legacy v1 app to apps v2, maintaining an existing v1 browser-SDK app, adding to a v1 app something it cannot do (a secret, a backend, a cron, authorization that must hold), or deploying. Do not use for managed-agent authoring or general organization administration. version: 0.3.3 --- # Create Railcode App ## Update First Before answering a Railcode question or running a `railcode` command, update the Railcode skills and CLI, then verify npm's published version: ```bash npx skills add Railcode-HQ/railcode-skills npm install -g railcode@latest railcode --version npm view railcode version ``` If the skill changes, re-read this file from the top. If npm is unreachable, say so and do not claim the guidance is current. This version was written against **CLI 0.3.0** and **`@railcode/sdk` 0.4.0** (both confirmed published). **0.3.0 is the floor this document assumes.** It is where personal connectors were folded into `railcode connector` (see [Breaking Changes](#breaking-changes)). On an older binary the `connector link` / `add-mcp` / `share` subcommands do not exist, and `railcode personal-connectors` still does — if you see that command work, you are on a CLI this guidance does not describe. Reinstall before following anything below. Two older floors still worth recognising: `secrets` and `migrate` exist but are never dispatched on **0.2.2** (they die with an unknown-command error however correct the invocation — 0.2.3 wires them up). Since 0.1.28 the CLI self-updates within its major version — but only on an **interactive terminal**, and agent-driven sessions are non-interactive, so keep running the explicit `npm install -g railcode@latest` above rather than assuming you're on the latest. ## Report Railcode feedback After meaningful Railcode work, use the hidden `railcode agent-feedback` command when you have concrete product feedback: a bug, friction, a suggestion, or something that worked especially well. Submit once per distinct finding. Describe the task, expected and actual behavior, and any workaround; keep the report concise and omit credentials, private customer data, source files, and conversation transcripts. ```bash railcode agent-feedback --message "Deploy failed with an unclear error; expected the missing field to be named." \ --category friction --command "deploy" --context "Static app deploy; corrected the manifest to unblock." ``` Use `--file ` for a prepared report or pipe text on stdin instead of `--message`. The message limit is 10,000 characters; `--context` is optional and limited to 5,000. Categories: `bug`, `friction`, `suggestion`, `praise`, `other` (default). Optionally self-report your assistant/tool name with `--agent` (e.g. `Codex` or `Claude Code`) and your model with `--model`, if known. These are your own claims and may be inaccurate; omit unknown values rather than guessing. These fields and `--command` are limited to 200 characters each. The CLI attaches its version, OS platform, CPU architecture, and Node version. The backend links the report to the logged-in user and organization in PostHog. It requires an existing login; it works outside an app directory. `Feedback accepted.` means best-effort acceptance, not confirmed storage. If reporting fails, continue the original task without repeated retries or logging in solely to send feedback. This command is intentionally absent from CLI help and requires **CLI 0.3.3 or later**. This feedback guidance was verified against the CLI 0.3.3 source. Older CLIs may return `Unknown command`; treat that as unavailable. ## Breaking Changes A running log of platform changes that break apps or agents already in the wild, newest first. **When a user reports that something which used to work now fails, read this before debugging** — the platform's own error messages are written to be the fix instructions, and the entry below tells you how to act on them. Add an entry here whenever a change ships that an existing app cannot survive untouched. ### Personal connectors are gone — CLI 0.3.0, `@railcode/sdk` 0.4.0 `personalConnections` and the `personal_connectors:` manifest key were **removed**. There is now one kind of connector: an **org resource with an owner and an access mode**, declared under `connectors:` — whether it holds a shared team credential or one person's own account. An owned row is `restricted` (owner, admins, and whoever it is shared with) until shared. **How it shows up** - A v1 app or v2 worker calling `personalConnections.*` gets **HTTP 410**, not a 404. The body names the connector that replaced the toolkit, lists the connectors the caller owns, and carries a copy-pasteable replacement call. Read the body — it is the answer. - `railcode deploy` on a manifest still carrying `personal_connectors:` fails with *"`personal_connectors` was removed — link the account as a connector … and name it under `connectors:`"*. The app keeps serving; only the deploy is refused. - **Managed agents needed no work.** The platform rewrote their manifests, prompts, and grant rows in place. If an agent still misbehaves, it is not this. **What to do** 1. `railcode connector list` — the account is already there. Credentials were migrated; nobody re-authorizes anything. 2. **Check the name.** If two people in one org held the same provider, or the plain name was already taken, the row was suffixed — `gmail` may now be `gmail-jp`. Use the name from `connector list`, not the old toolkit id. 3. Swap the calls. The surface is identical in the worker SDK and the v1 browser SDK: | Old | New | |---|---| | `personalConnections.call("gmail", "send_email", args)` | `connector("gmail-jp").call("send_email", args)` | | `personalConnections.tools("gmail")` | `connector("gmail-jp").tools()` | | `personalConnections.list()` | `serviceConnectors()` | | `personalConnections.connect("gmail")` | **No app-side equivalent.** Linking left the app: the person runs `railcode connector link gmail` or links from the dashboard. Do not build a connect flow into the app. | 4. Replace `personal_connectors:` with `connectors:` in the manifest, naming the row and the tools it may call — e.g. `connectors: { "gmail-jp": ["send_email"] }`, or `["*"]` for the whole row. Then redeploy. **A v1 app does not need migrating to apps v2 for this.** `connector()` is already in the platform SDK served from `/_api/sdk.js`. What it does need is a **rebuild**, because the call site is compiled into its bundle — so this is only actionable by someone who still has the app's source. ## First: Which Generation? Railcode apps come in two shapes, and **almost every rule below depends on which one you are holding**. Settle this before anything else. - **A new app is always generation 2 (apps v2).** There is no choice, no flag, no `railcode.json` key. The server assigns it. - **An existing app may be generation 1 (v1)** — the legacy browser-SDK shape. Existing apps were backfilled to 1 and stay there until someone explicitly migrates them. ```bash railcode apps show --json | grep generation # 1 = legacy, 2 = apps v2 ``` The plain text output does **not** print the generation; use `--json`. | Situation | Do this | |---|---| | Building a new app | **Apps v2.** Continue with this file. | | Changing an app whose `railcode.json` has a `"type"` and a `"server"` | **Apps v2.** Continue with this file. | | Changing an app whose `index.html` loads `/_api/sdk.js` | **Generation 1.** Read [v1 legacy](references/v1-legacy.md) — the rules here mostly do not apply. | | Changing a v1 app, and the request needs something v1 cannot do — a secret, a backend, a cron, authorization that must hold | **Migrate, and tell the user.** Read [When a v1 App Can't Do What's Asked](#when-a-v1-app-cant-do-whats-asked) next. Do not build a browser-side workaround. | | User wants a v1 app rebuilt as v2 | Read [Migration](references/migration.md) **first**. It is one-way and has a downtime window. | If you cannot reach the server to check, decide from the source tree: `/_api/sdk.js` in `index.html` means v1; a `"server"` key in `railcode.json` means v2. ## When a v1 App Can't Do What's Asked A v1 app is a page and a browser SDK. There is no worker, so there are no secrets, no cron, no server-side code, and no caller the app can trust. When a request needs one of those, the platform's answer is "that needs a worker" — and a v1 app cannot get a worker without migrating. "Add X to this app" hides a migration whenever X lands on a row below. | The request needs… | Why v1 can't | On v2 | |---|---|---| | A credential the browser must not see — an AWS key, a signing key, a third-party API secret | No secrets. Anything the page can read — KV, a settings collection, a bundled constant — every user who can open the app can read | `secrets.NAME` in the worker | | An API the connector proxy can't authenticate to — per-request signing (SigV4), mTLS, a bespoke handshake | Connectors do bearer/header/query/basic only, and there is nowhere server-side to run signing code | The worker calls it under `egress:` | | Something to run on a schedule | No cron; nothing runs unless a page is open | A `crons:` entry hitting a worker route — or the agent's own schedule for AI work | | A rule that must hold — "X submits, Y approves, X can't approve their own" | Page-side checks are advisory; any user can bypass them from devtools | A check in the worker against `ctx.user` | | Server-side work — aggregating many records, chaining several services, anything too heavy or too sensitive for a tab | Nothing runs server-side | A worker route | **The rule: migrate, tell the user, and keep going.** When the request lands on a row, the deliverable is the migration plus the feature — not a v1 approximation of the feature. - **Do not build the workaround.** A key parked in KV so the page can sign requests itself, a poll loop standing in for a schedule, an approval check in a tab — each one ships, demos, and is the wrong design, and the user finds out afterward if at all. If you catch yourself writing "the key is readable by anyone who can use the app", you are building the workaround. - **Say it up front, in one paragraph, then proceed.** What the request needs, that v1 has no way to provide it, that the app is going to v2 to get it, and which path: a new slug (Path A — reversible, preferred) or in place (Path B — one-way, with a downtime window). Do not stop the work to ask whether to migrate, and do not present migration as an alternative once the workaround is already built. - **Size it honestly — it is smaller than it looks.** v2 has no browser SDK, so every data call the page makes moves behind the worker. But that is a rewrite of the app's one SDK wrapper into `fetch()` calls to worker routes, not a rewrite of the app: views, state, and business logic stay. The feature you were asked for becomes the first new route. `apps/crm` in `railcode-examples` is a large v1 app ported through exactly one module. - **The one thing to hold for is the gate.** On Path B, `railcode migrate` is irreversible and the live app is down until the next deploy, so finish and validate the v2 build first and get an explicit go before running it. Path A never touches the gate. Read [Migration](references/migration.md) for the procedure. If the user hears all of this and still wants the v1 version, build it and say plainly what it compromises. Small changes that stay inside what v1 already does — a new view, a fix, a field — need none of this; make them in place per [v1 legacy](references/v1-legacy.md). ## The v2 Model In One Paragraph A v2 app is a **static frontend plus a backend worker**, deployed and versioned as one unit. **There is no browser SDK.** The frontend is plain static files that `fetch()` your own worker routes; the worker imports `@railcode/sdk` and is the only thing that touches platform capabilities. The worker **is** the app's principal (`run_as: app` is mandatory) and receives a verified, unforgeable `ctx.user`. Authorization is worker code — that is the point: "X submits, Y approves, X can't approve their own" now lives in a trusted place instead of in a tab. Everything else follows from that. If you catch yourself reaching for a `window.db` or a `/_api` data call from the page, stop: that is the v1 shape. ## Clear Any v1 SDK Memory If you are starting v2 work carrying memory written before apps v2 — agent memory files, `CLAUDE.md` / `AGENTS.md` notes, saved project learnings, a rules file, a starred snippet — assume it describes the **v1 browser SDK** and is now wrong. Stale memory is the most common reason a v2 build goes sideways for reasons the code itself does not explain: a `