sdxc
SearchCtrlK

[ 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 fetch to 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 make writes 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:

EntryStarted byHands over to
bootstrap/worker.ts fetchan HTTP requestthe router, app/initializers/router.tsx
bootstrap/worker.ts queue, scheduleda queued job or a cron triggerthe job dispatcher, app/initializers/dispatcher.ts
bootstrap/cli.tssdxc run … on your machinethe command line, app/initializers/cli.ts
bootstrap/console.tssdxc run consoleBun'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

A request through an sdxc app: the Worker's fetch builds the router, the router's global middleware publishes the log, database, models and mailer, the route contract picks the route, lazy() imports the controller, the controller's page chain adds the session, access, language, honeypot and renderer, and the action returns the responsefetch(request)global middleware: log, trace, rate limit,database, models, mailmatch routes/web.ts or routes/api.tslazy() imports the controllerits chain: page or api middlewarethe action's ResponseResponsebootstrap/worker.tsbootstrap/worker.tsRouterRouterController moduleController module
  1. The Worker builds the router. fetch calls createWebRouter() from app/initializers/router.tsx and forwards the request.

  2. 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 previews bun dev serves at /_emails, and the mailer. sdxc routes prints it at the top.

  3. A contract picks the route. routes/web.ts declares the pages and routes/api.ts the endpoints; each entry is a URL pattern and a method, and the typed href() every link uses comes from the same declaration.

  4. lazy() loads one controller. Every route is mapped through lazy(), so a request imports only the controller it reaches, and a cold start pays for one module.

  5. 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 declares app/http/middleware/api.ts: CORS and access. Only requests that render a page pay for a page's middleware.

  6. The action answers. It reads what the chain published on ctx, validates its input with a schema from app/http/validators/, calls the models or a service, and returns a Response, usually ctx.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 ctxPublished byIn
ctx.loglog(logger)everything
ctx.db, ctx.modelsdatabase(…), models(registry, …)everything
ctx.emailmailer({ transport, from, mail, queue })everything
ctx.ip, ctx.userAgentgetClientIP(), userAgent()requests
ctx.session, ctx.intlthe page chainpages
ctx.accessaccess in the page and api chainsrequests
ctx.render, ctx.assetsrender(…), assets(…) in the page chainpages

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
              • initializers
                • jobs
                  • mcp
                    • models
                      • policies
                        • services
                          • test
                          • config
                            • database
                              • factories
                                • migrations
                                  • seed.ts
                                • resources
                                  • layouts
                                    • locales
                                    • routes
                                      • web.ts
                                      • api.ts
                                    • cloudflare.config.ts
                                    If the code…It goes in
                                    is the Worker, browser, command-line or REPL entry pointbootstrap/
                                    builds the one instance of something: router, database, email registryapp/initializers/
                                    answers HTTP: a controller, a middleware, an input schemaapp/http/controllers, middleware, validators
                                    runs off the request, as a jobapp/jobs/
                                    is a tool or resource for the MCP endpointapp/mcp/
                                    is an email the app sendsapp/emails/, declared in app/emails/index.ts
                                    is a command for sdxc runapp/commands/
                                    reads or writes stored state, or is a rule about one modelapp/models/, as the model or its method
                                    is a rule across models, or about noneapp/services/
                                    decides who may do whatapp/policies/
                                    is a URL contractroutes/
                                    is a migration or a test factorydatabase/migrations/, database/factories/
                                    is the rows a new local database starts withdatabase/seed.ts
                                    is a view, layout, stylesheet or translationresources/
                                    is static configuration: site settings, security headersconfig/
                                    is a test helperapp/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

                                    Written by Sergio Xalambrí. Follow @sergiodxa for new packages, or sponsor the work.