[ Getting started ]
How it fits together
What runs between a request reaching an sdxc app's Worker and its controller answering, what each handler's ctx carries and who puts it there, and the job of every folder the template writes.
Last updated 2026-10-19
After reading this guide, you will know
- The order code runs in, from the Worker's
fetchto the controller's action - What a handler reads from
ctx, and which middleware publishes each part - Which folder each kind of code lives in, and why that is the folder
sdxc makewrites to
An app from sdxc create is a Cloudflare Worker
whose requests are answered by a Remix router. This page assumes you know
both, and covers what the template adds: where each piece is built, the order it runs in, and
the folder each kind of code goes in. Knowing that is what lets you find any file in an
unfamiliar app, and tell where a new one belongs before sdxc make writes it.
Four ways in, one set of services
The app is entered four ways, and each entry point hands over to what app/initializers/ and
app/models/ build:
| Entry | Started by | Hands over to |
|---|---|---|
bootstrap/worker.ts fetch | an HTTP request | the router, app/initializers/router.tsx |
bootstrap/worker.ts queue, scheduled | a queued job or a cron trigger | the job dispatcher, app/initializers/dispatcher.ts |
bootstrap/cli.ts | sdxc run … on your machine | the command line, app/initializers/cli.ts |
bootstrap/console.ts | sdxc run console | Bun's REPL, with db and models on the local file |
The first three each run middleware that publishes the same services, so a controller, a job and
a command all read ctx.db, ctx.models and ctx.log, and a model method you write once works
in all three. bootstrap/ holds the entry points and nothing else, and nothing outside it
imports from it: the CLI's commands never ship in the Worker, and a test builds the router
without starting a Worker.
A request, step by step
The Worker builds the router.
fetchcallscreateWebRouter()fromapp/initializers/router.tsxand forwards the request.The global middleware runs. The router's chain, written inline in
createRouter({ middleware }), is what every page and endpoint needs: the request's log and trace, the client address, the rate limit, the database, the models, the email previewsbun devserves at/_emails, and the mailer.sdxc routesprints it at the top.A contract picks the route.
routes/web.tsdeclares the pages androutes/api.tsthe endpoints; each entry is a URL pattern and a method, and the typedhref()every link uses comes from the same declaration.lazy()loads one controller. Every route is mapped throughlazy(), so a request imports only the controller it reaches, and a cold start pays for one module.The controller's chain runs. A page declares
app/http/middleware/page.ts: the form body, the cross-origin check, the session, the visitor's access, the language, the honeypot, the security headers and the renderer. An endpoint declaresapp/http/middleware/api.ts: CORS and access. Only requests that render a page pay for a page's middleware.The action answers. It reads what the chain published on
ctx, validates its input with a schema fromapp/http/validators/, calls the models or a service, and returns aResponse, usuallyctx.render(<Page />).
What ctx carries
A handler never builds its own services; the middleware publishes each one onto the context,
and the router's type reads them back, so ctx.models is typed by the models you registered.
That is also what lets a test swap one: it runs the same chain with its own database.
On ctx | Published by | In |
|---|---|---|
ctx.log | log(logger) | everything |
ctx.db, ctx.models | database(…), models(registry, …) | everything |
ctx.email | mailer({ transport, from, mail, queue }) | everything |
ctx.ip, ctx.userAgent | getClientIP(), userAgent() | requests |
ctx.session, ctx.intl | the page chain | pages |
ctx.access | access in the page and api chains | requests |
ctx.render, ctx.assets | render(…), assets(…) in the page chain | pages |
A middleware you write with sdxc make middleware publishes its
own value the same way, and --mount adds it to a chain.
A job, step by step
A controller or a command enqueues a job by its declaration in app/jobs/index.ts; Cloudflare
delivers the message to the Worker's queue handler, and the dispatcher in
app/initializers/dispatcher.ts runs its own middleware, the database, the models and the
mailer, before it imports the handler the job is mapped to. A --cron job is enqueued by the
Worker's scheduled handler when a trigger in cloudflare.config.ts fires. Either way the
handler sees ctx.input, ctx.models, ctx.email and ctx.log, and a failure is retried by
the dispatcher. ctx.email.queue() uses the same path: it enqueues the template's email job,
which delivers the message and retries a provider failure.
An email, step by step
Each email is declared once in app/emails/index.ts, with the schema of the props it takes, and
its template is a file beside it, such as app/emails/teams/invite.tsx. The registry in
app/initializers/mail.ts maps each declaration to its template through lazy(), and the
router, the dispatcher and the command line hand that registry to their mailers. A handler
sends by declaration, with the reader's language:
await ctx.email.send(emails.teams.invite, { to, locale, props: { team } });
sdxc make email writes the template, declares it and maps it, and sdxc doctor fails on a
declared email with no template mapped. Send email covers
templates, previews and the transports.
Where each kind of code goes
The template is laid out by kind of code, one folder per kind, and sdxc make writes each kind
to its folder:
links
- bootstrap
- worker.ts
- browser.ts
- cli.ts
- console.ts
- app
- commands
- emails
- http
- controllers
- middleware
- validators
- controllers
- initializers
- jobs
- mcp
- models
- policies
- services
- test
- commands
- config
- database
- factories
- migrations
- seed.ts
- factories
- resources
- layouts
- locales
- layouts
- routes
- web.ts
- api.ts
- cloudflare.config.ts
| If the code… | It goes in |
|---|---|
| is the Worker, browser, command-line or REPL entry point | bootstrap/ |
| builds the one instance of something: router, database, email registry | app/initializers/ |
| answers HTTP: a controller, a middleware, an input schema | app/http/controllers, middleware, validators |
| runs off the request, as a job | app/jobs/ |
| is a tool or resource for the MCP endpoint | app/mcp/ |
| is an email the app sends | app/emails/, declared in app/emails/index.ts |
is a command for sdxc run | app/commands/ |
| reads or writes stored state, or is a rule about one model | app/models/, as the model or its method |
| is a rule across models, or about none | app/services/ |
| decides who may do what | app/policies/ |
| is a URL contract | routes/ |
| is a migration or a test factory | database/migrations/, database/factories/ |
| is the rows a new local database starts with | database/seed.ts |
| is a view, layout, stylesheet or translation | resources/ |
| is static configuration: site settings, security headers | config/ |
| is a test helper | app/test/ |
Group a crowded folder by feature with the same name in every layer, such as
app/jobs/bookmarks/ beside app/services/bookmarks/, and one search for bookmarks/ finds the
whole feature. sdxc doctor's layout check fails on a folder in app/ outside this table,
and on an import from bootstrap/.
Where to go next
Generate files — what each
sdxc makekind writes, and what it edits to wire it in.Wire the router — the router's middleware and mappings, package by package.
Background jobs and cron — the dispatcher and its handlers in full.