One codebase, four platforms: how the adapter switch works
How a single astro.config.ts builds for Vercel, Cloudflare Workers, Netlify and a Node server, and what each platform needs from you.
“Where should we host it?” is a question that should never block a project. The template answers it
with a build-time switch: set DEPLOY_TARGET, or let the platform be detected, and the correct
adapter is loaded. This post walks through the mechanism and the small set of platform
differences it hides.
The switch#
config/adapter.ts exports two functions. resolveDeployTarget() reads DEPLOY_TARGET and
falls back to the variables each platform sets during a build: VERCEL=1, NETLIFY=true, and
WORKERS_CI=1 or CF_PAGES=1 on Cloudflare. When none match, it returns node.
resolveAdapter(target) dynamically imports only the adapter that was chosen, so the other three
never load, and returns it with the options that make the template’s features work:
case 'vercel':
return vercel({ imageService: true, staticHeaders: true });
case 'cloudflare':
return cloudflare({ prerenderEnvironment: 'node' });
case 'netlify':
return netlify({ staticHeaders: true });
case 'node':
return node({ mode: 'standalone', staticHeaders: true });
astro.config.ts awaits the result at the top level and passes it to defineConfig.
What stays the same#
Most of the codebase never knows which platform it is on:
- Pages are prerendered by default; the auth and account routes, the dashboard, the admin area and the API opt in to server rendering.
- The libSQL client picks its HTTP build on Workers and edge runtimes automatically, so database code is identical everywhere.
- Better Auth runs on standard
RequestandResponseobjects. - The Content Security Policy is generated by Astro and either injected as a
<meta>element or delivered as a header when the adapter supportsstaticHeaders.
What differs, and where it lives#
Zero configuration. The engines.node field selects Node 24, staticHeaders delivers the CSP,
and a small integration adds the remaining security headers to the Build Output config.
wrangler.jsonc enables nodejs_compat, which Better Auth needs for AsyncLocalStorage and
which populates process.env. Prerendering runs in Node so the OG image endpoint can use native
modules; on-demand routes run in workerd. Static asset headers come from public/_headers.
netlify.toml sets the publish directory and Node version. The adapter provides the Image CDN
and Blobs; public/_headers covers static assets.
pnpm build:node produces dist/server/entry.mjs, which the included Dockerfile runs. The same
build powers the Playwright suite in CI.
Proving it every time#
The CI workflow builds all four targets on every pull request. It is the cheapest insurance against a dependency update that works on one runtime and breaks another.
Read the per-platform pages under Deploy for the exact dashboard settings and environment variables.