Skip to content
mdlake

Choose a hosting model

The three hosting models of a generated site (static, server and split), how the kit decides which one a site is in, and how to check and change it. For developers.

On this page

A generated site starts as plain static pages. Most sites never need more. When a site needs code that runs on a server (a contact form, a webhook, a database), the kit moves it to one of two other hosting models. This page explains the three models, how the kit tells which one a site is in, and what to run when you want to change.

The hosting model decides what the build produces, which Azure resources you pay for and which pipeline files the kit writes. Azure is the only deployment target the kit generates files for today.

The three models

StaticServerSplit
What it isPages only. The build writes plain files to dist/.Pages plus on-demand routes in src/pages/api. Astro itself is the server.Static pages plus a separate backend in api/ (Node or .NET).
Runs onAzure Static Web AppsA Linux App Service running Node 22 (node dist/server/entry.mjs)Static Web Apps (Standard plan) for the pages, and a second Linux App Service for the backend, linked so /api/* goes to it
Build outputdist/dist/client (pages) and dist/server (the Node server)dist/ for the site, api/dist (Node) or a dotnet publish folder for the backend
Server code lives inNowhere. There is none.src/pages/api/** and src/server/**api/ only
Typical useMarketing sites, docs, handbooksA contact form, one or two small endpointsSeveral routes, background work, a database, or an API a team owns and tests on its own
Cost shapeFree plan for staging, Standard plan in the generated production parametersOne App Service plan (B1 by default)Static Web Apps Standard plus one App Service plan (B1 by default) plus a Key Vault
How you get thereIt is the defaultnpx mdlake add-form or npx mdlake add-api-routenpx mdlake backend init --runtime node (or dotnet)

Important

Do not add a server route to solve something a static site can already do (a link, a mailto:, an embed the client asked for). A server route changes the hosting and its cost. The skills ask for your approval before they switch a site to a more expensive model.

When static is enough

Choose static when every page can be built ahead of time from the lake. Search on a docs site is still static: Pagefind writes its index into dist/pagefind during the build.

When server is the right step

Choose server when you need a handful of endpoints and want to stay in one project and one language. Forms are the common case: the kit’s form handler runs as an Astro route. Server pages are still built ahead of time. Only the files in src/pages/api run on every request.

When to split

Choose split when the server needs outgrow what a few Astro routes should carry. That means background work, a database, many routes, or an API that a team maintains and tests separately. The backend is a normal project (Hono on Node, or an ASP.NET Core minimal API on .NET 10) with its own tests, and the site calls it through a typed client generated from its OpenAPI document. See A separate backend.

Split is always a decision. The kit never moves a site to split because of what the code looks like.

How the kit decides

The kit looks at the routes first, then at one recorded decision.

  1. It scans src/pages for files that contain export const prerender = false. That line marks a route as on-demand: it runs on the server for each request.
  2. If there is at least one such route, the site is server. If there is none, the site is static.
  3. A site is split only when .mdlake/hosting.json says so. The file gets that value from npx mdlake backend init. Routes never make a site split.

Server code is allowed in two places only:

  • src/pages/api/** for on-demand routes.
  • src/server/** for helpers that those routes import.

An on-demand route anywhere else, or a route file in src/pages/api without export const prerender = false, fails the check. Pages, layouts and components never run on the server per request.

The record in .mdlake/hosting.json

npx mdlake prepare writes the tier to .mdlake/hosting.json. You do not run it by hand: npm run dev, npm run build and the checks run prepare first. Commit the file. A static site that never had a server route has no file.

A server site:

JSON
{
  "tier": "server",
  "adapter": "node",
  "onDemand": [
    "src/pages/api/forms/[name].ts",
    "src/pages/api/newsletter.ts"
  ]
}

A split site:

JSON
{
  "tier": "split",
  "onDemand": [],
  "backend": {
    "runtime": "node",
    "path": "api"
  }
}
FieldMeaning
tierstatic, server or split.
adapternode on the server tier. The kit adds @astrojs/node in standalone mode itself.
onDemandThe route files that opt out of prerendering. Empty on a split site.
backend.runtimenode or dotnet. Split only.
backend.pathThe folder of the backend project. It is api.

The file belongs to the kit. Do not edit it by hand. If it is out of date, run npx mdlake prepare and commit the result.

See which model a site is in

Bash
npx mdlake hosting
Tier: split (Node backend in api/)
.mdlake/hosting.json: split
present .github/workflows/deploy.yml
present azure-pipelines.yml
present infra/main.bicep

On a server site the output also lists the on-demand routes. present or absent tells you which pipeline and Bicep files exist.

Check that everything agrees

npx mdlake check-hosting is the check:hosting step of npm run check. It compares the tier with everything that depends on it and prints what to run when something does not match.

Bash
npx mdlake check-hosting

For every tier it checks that:

  • the tier recorded in .mdlake/hosting.json matches the routes (a missing or outdated file is an error that tells you to run npx mdlake prepare);
  • no on-demand route is outside src/pages/api, and no file in src/pages/api would be built as a static file;
  • astro.config.* sets no adapter of its own that fights the tier (a static or split site must not set adapter; a server site must use @astrojs/node);
  • the GitHub workflow, the Azure DevOps pipeline and infra/main.bicep, when they exist, are written for the same tier (and, on split, the same backend runtime). A site without pipelines yet gets a note, not an error.

For a split site it also checks that:

  • there are no on-demand Astro routes and nothing under src/pages/api, because Static Web Apps sends every /api/* request to the backend;
  • the backend project exists (api/package.json for Node, an api/*.csproj for .NET);
  • api/openapi.json exists and is valid JSON;
  • the generated client in src/api/generated exists and was generated from the current api/openapi.json;
  • no file in src calls /api with a hand-written fetch;
  • every form in src/forms is served by the backend.

Here is real output for a split site that has a new form the backend does not know yet:

error   api/openapi.json does not exist. It is the contract between the backend and the site: run `npm run api:openapi` (it builds the backend and writes it), commit it, then `npm run api:generate`.
error   The form "newsletter-signup" (src/forms/newsletter-signup.ts) is not served by the Node backend, so POST /api/forms/newsletter-signup/ would answer 404. add an import and an entry to api/src/forms.ts.
Hosting: split tier (Node backend in api/), 2 errors

The command exits with 1 when there is at least one error, and 0 otherwise. Warnings and notes do not fail it.

Change the model

FromToHow
StaticServernpx mdlake add-form <name> or npx mdlake add-api-route <name>. Then npx mdlake hosting init --ci ... to write the server pipelines and Bicep.
ServerSplitnpx mdlake backend init --runtime node --from-server (or dotnet). Move your own routes to api/. Then npx mdlake hosting init --ci ....
ServerStaticDelete the routes in src/pages/api, run npx mdlake prepare, commit .mdlake/hosting.json, and run npx mdlake hosting init --ci ... so the pipelines and Bicep are static again.
SplitAnything elseThere is no command to undo backend init. A site has one backend, and the split record in .mdlake/hosting.json is never derived from the routes, so leaving split is manual. Remove api/, src/api/generated/ and the record in .mdlake/hosting.json. Remove what backend init added to the site’s package.json: the scripts api:openapi, api:generate, check:api and dev:all, check:api in the check script, the dependency zod and the dev dependency orval (keep them if your site uses them), and for a Node backend the api entry in workspaces. Remove api from exclude in tsconfig.json. Then run npx mdlake prepare and npx mdlake hosting init --ci ..., and let npm run check list what is left.

After any change, npm run check tells you what is left. The pipelines and Bicep are generated files for one tier, so they must be written again. hosting init also names modules from the other tier that are left behind in infra/modules. Delete them when you have not edited them. See Azure and Bicep.

Note

Moving from server to split is explicit. backend init refuses on a site with server routes unless you pass --from-server, and it prints what would change first.

What’s next