How Railcode works
A proxy in front of every deployment, a zero-config SDK in every backend function, and a format any JavaScript stack can target. The ten-minute technical tour.
Railcode is a deployment platform for internal software. You deploy JavaScript apps to us: a frontend and a backend function. On paid plans, you can also define managed agents in YAML, and we run them for you.
Around those we provide everything you need to build complete apps: data and file storage, an LLM gateway, managed connectors to your databases and SaaS tools, and a mail service. This page is the ten-minute tour of how the pieces fit.
The Railcode proxy
We run a proxy in front of every deployment. That one decision does most of the work. Every project on Railcode is behind company auth by default. Every access to an external resource (an LLM, a connector, a data source) is permissioned and logged in one place.
Here is what happens when someone opens the finance-tracking app. The request passes two
gates before the app is served. Neither gate is a formality: passing the first buys you
nothing at the second.
Gate 1: belonging. Is this person a member of the org? The proxy resolves the request to an org before it resolves it to an app. Someone outside the org never reaches the second question.
Gate 2: this resource. Do they have access to this app? Membership alone opens nothing. They need access to this app, either granted to them directly or carried by one of their roles.
Gate 1 is about the org; gate 2 is about the app. Here are the same two gates deciding four requests:
member
her Finance role carries it
member
granted to them directly, no role
member
no grant, no role that carries one
not a member here
never asked
The difference between Maya and Sam is the whole of gate 2: a role that carries access, or a grant made directly to them. Either route is enough on its own. Ali shows that membership is not access. And Rita never reaches gate 2 at all. The gates stop at the first failure, so someone from another org learns nothing about which apps exist.
A user who fails either gate sees a 404 rather than a 403. A 403 would confirm that the app exists.
You set who passes gate 2 per app (organization, private, or restricted). You set roles
once for the whole org, and every app reads the same ones. See
app access and
roles and grants.
The Magic SDK
Because apps run behind the proxy, every user who lands on an app has the right to be there. Two useful things follow.
First, your frontend can call your backend function without an API key. There is nothing to leak, because there is nothing to hold.
Second, your backend function receives context about the caller: who they are, whether they
are an admin, and which roles they hold. We call it ctx.user. The proxy verified it, so app
code cannot fake it.
On top of that, every backend function gets a zero-config SDK. Import what you need from
@railcode/sdk, and everything Railcode manages is there. You never set up a connection
string, a credential in an environment variable, or a provider SDK.
Here is a route that triages a Sentry issue. Only Engineering may call it. It reads the issue through a governed connector, asks the LLM gateway for a priority, stores the answer, and returns the queue:
import { Hono } from "hono";
import { ctx, db, connector, llm } from "@railcode/sdk";
const app = new Hono();
app.post("/api/triage", async (c) => {
const user = ctx.user;
if (!user?.roles.some((r) => r.name === "Engineering")) {
return c.json({ error: "You must be part of the Engineering team to call this endpoint" }, 403);
}
const { issueId } = (await c.req.json()) as { issueId: string };
// The connector holds the Sentry credential. This backend function never sees it.
const issue = await (await connector("sentry").fetch(`/issues/${issueId}/`)).json();
const result = await llm.generate({
messages: [{
role: "user",
content: `Assign a priority (P0-P4) to this Sentry issue. Reply with the level only.\n\n${JSON.stringify(issue)}`,
}],
});
const priority = result.text.match(/P[0-4]/)?.[0] ?? "P4";
await db.collection("issues").put(issueId, { issueId, priority, triaged_by: user.id });
const queue = await db.collection("issues").query().order("priority", "asc").page(1, 100);
return c.json(queue);
});
export default app;The route never handles a Sentry token, an OpenAI key, or a database URL. There is no auth middleware to write. The role check is the only line about who is allowed, and it is written against a caller the platform vouched for.
Everything that route does runs under the app's declared authority. Its manifest.yaml
names the LLM and the sentry connector endpoints it uses. A deploy can never grant itself
more than the person deploying it already holds. See
the manifest.
Basically, we take care of the annoying bits so you can focus on the fun part: building.
Apps
We offer Hono + Vite and TanStack templates out of the box (railcode init). You can also use
any stack or JavaScript framework you like, as long as it builds into a static frontend and
a single ES module backend function.
The backend uses the same format as Cloudflare Workers, so most stacks can already target it:
export default {
async fetch(request) {
return new Response("hello");
},
};Point railcode.json at the built frontend directory and the built backend module.
railcode deploy ships both as one unit; the two halves activate together and roll back
together. See the app model for the templates and the bring-your-own rules.
A note on Next.js
You can deploy a Next.js app by building it with OpenNext and targeting our backend
format, but we don't recommend it. Next.js is tightly coupled to Vercel's features and
generally does not work well outside Vercel. If you like React, the hono+vite template
(React + Vite on the front, Hono on the back) or tanstack will be much easier.
Managed agents
On paid plans we also run managed agents for you: server-side AI that you define in YAML and
we execute. Each run gets a code sandbox and a durable, auditable history. Use an agent
whenever the work has to read or write a file, run code, take minutes, run on a schedule, or
answer from Slack. Anything shorter (summarize, classify, draft while the user waits) belongs
in the backend function's own llm. See managed agents.
You own the code
You build with the tools you already use: Claude Code, Codex, Cursor, or a plain editor. The
code is yours. It lives in your repo, runs locally with railcode dev, and goes on GitHub if
you want it there. We ship a CLI, and a skill your coding agent can pick up, to deploy and
manage projects end to end.
Where to go next
- Ship something: the quickstart takes about five minutes.
- Learn the vocabulary: core concepts.
- See the whole SDK: the backend SDK reference.