How Nitro's Deployment Adapters Work — and Why Litro Gets Them for Free
Litro doesn't ship deployment adapters. It doesn't need to. Every deployment target Litro supports — Vercel, Cloudflare Workers, AWS Lambda, Netlify, Deno Deploy, Bun, Node.js standalone — comes from Nitro, the server engine underneath.
This post explains what Nitro presets actually do, which ones work with Litro, and the two edge-specific considerations you need to know about before deploying a Lit SSR app to a Workers environment.
What Nitro Is
Nitro is the server engine that powers Nuxt.js. It's a standalone package (nitropack) that handles routing, middleware, API handlers (via H3), and — most importantly — output targets.
Litro's server is a Nitro server. You configure it directly through nitro.config.ts — Litro doesn't wrap or shadow Nitro's config. The one Litro-specific piece of configuration is the framework adapter, which you select by setting process.env.LITRO_ADAPTER at the top of nitro.config.ts (the scaffolded templates do this for you). Every H3 handler, every middleware, every route rule you configure goes through Nitro. If you've used Nuxt, you've already used Nitro.
Analog (the Angular meta-framework) also builds on Nitro. So does SolidStart. Litro is one of several frameworks that made the same bet: use Nitro for the server layer and inherit everything it provides. For a comparison of how this plays out vs Nuxt specifically, see Litro vs Nuxt.js.
What a Preset Is
A Nitro "preset" is a self-contained output configuration that transforms the server into a form suitable for a specific runtime. When you run a build with a preset, Nitro:
- Bundles the server into a single entry file (or a set of files)
- Transforms imports to match the target runtime's available APIs
- Generates any runtime-specific wrapper code or configuration files
- Copies assets into the output directory structure the target expects
The output of NITRO_PRESET=vercel litro build is a directory structure that matches exactly what Vercel's build infrastructure expects to find. No Litro-specific code is involved — Nitro handles it entirely.
Available Presets
These presets work with Litro today:
| Preset | Command | Output |
|---|---|---|
node (default) | litro build | dist/server/server/index.mjs — Node.js ESM server |
node-cluster | NITRO_PRESET=node-cluster litro build | Clustered Node.js for multi-core use |
vercel | NITRO_PRESET=vercel litro build | .vercel/output/ directory |
cloudflare-workers | NITRO_PRESET=cloudflare-workers litro build | dist/ for wrangler publish |
cloudflare-pages | NITRO_PRESET=cloudflare-pages litro build | dist/ for Cloudflare Pages |
netlify | NITRO_PRESET=netlify litro build | .netlify/ directory |
aws-lambda | NITRO_PRESET=aws-lambda litro build | Lambda handler bundle |
deno-deploy | NITRO_PRESET=deno-deploy litro build | Deno-compatible output |
bun | NITRO_PRESET=bun litro build | Bun-compatible server |
static | litro build --mode static (or litro generate) | Pre-rendered static HTML; the static preset is selected automatically when Litro is in SSG mode |
The static preset is what litro build uses when you configure SSG mode. The others are SSR presets.
You can also set the preset in your config:
// nitro.config.ts
import { defineNitroConfig } from 'nitropack/config';
export default defineNitroConfig({
preset: 'vercel',
});
Or via environment variable, which is useful for CI where you want one config file to target different environments:
NITRO_PRESET=cloudflare-workers litro build
Edge Adapters — Two Things to Know
Deploying to Cloudflare Workers or Vercel Edge requires two configuration additions. Both are in nitro.config.ts.
1. Inline @lit-labs/ssr
Cloudflare Workers and Vercel Edge Functions don't use Node.js module resolution — they run in a V8 isolate. @lit-labs/ssr relies on node: built-ins in some of its internal paths.
By default, Nitro marks @lit-labs/ssr as an external dependency and expects the runtime to provide it. Edge runtimes can't. The fix is to tell Nitro to inline it — bundle it directly into the output:
// nitro.config.ts
export default defineNitroConfig({
preset: 'cloudflare-workers',
externals: {
inline: ['@lit-labs/ssr', '@lit-labs/ssr-client'],
},
});
Without this, you'll get a module-not-found error at runtime on Workers.
2. Streaming API Differences
Litro's adapter contract is renderPage(tag, serverData): AsyncIterable<string>. The framework wraps that iterable with iterableToReadable() (packages/framework/src/adapter/stream.ts) — a small Node.js Readable adapter — and hands it to Nitro's sendStream(). Node Readable is not available in Cloudflare Workers or other edge runtimes that lack Node stream APIs.
For edge targets, wrap the same AsyncIterable<string> in a WHATWG ReadableStream instead:
const ssrIterable = adapter.renderPage(tag, serverData);
// Node.js targets — the default Litro pipeline handles this via iterableToReadable
return sendStream(event, iterableToReadable(ssrIterable));
// Edge targets — wrap in WHATWG ReadableStream
return new Response(new ReadableStream({
async start(controller) {
for await (const chunk of ssrIterable) {
controller.enqueue(new TextEncoder().encode(chunk));
}
controller.close();
},
}));
Litro's built-in page handler uses the Node path. If you're writing a custom handler for an edge target, keep the runtime difference in mind.
Vercel Deployment Example
# Build for Vercel
NITRO_PRESET=vercel litro build
# The .vercel/output directory is ready for deployment
vercel deploy --prebuilt
The output follows Vercel's Build Output API. The SSR function lands in .vercel/output/functions/index.func/, static assets in .vercel/output/static/.
Cloudflare Workers Example
# Build for Cloudflare Workers
NITRO_PRESET=cloudflare-workers litro build
# Deploy with Wrangler
wrangler deploy
After the build, Nitro prints the exact output paths it generated — the Workers preset writes its entry under dist/server/ (Litro's overridden output.dir). Use the path Nitro prints rather than hardcoding it, since Nitro can move files between presets and releases.
A minimal wrangler.toml for a Litro Workers deployment:
name = "my-litro-app"
compatibility_date = "2025-09-01"
# Fill in `main` from the path Nitro prints at the end of the build —
# it sits under `dist/server/` but the exact filename can change between
# Nitro releases and Cloudflare preset variants.
main = "<path-printed-by-nitro>"
[site]
bucket = "<asset-dir-printed-by-nitro>"
Static assets are served by the Worker via KV bindings — Nitro generates the necessary asset manifest automatically. Always copy the main and bucket values from the build output rather than hardcoding them; the layout under dist/server/ is owned by Nitro and may evolve.
Coolify and Self-Hosted Node.js
For self-hosted deployments, the default node preset builds a standalone Node.js server. Litro's docs cover deploying to Coolify and GitHub Pages in detail.
The Node.js output:
litro build
node dist/server/server/index.mjs
The server listens on PORT (default 3000) and serves both the SSR handler and static assets from the same process.
How This Compares to Next.js
Next.js deployment adapters for non-Vercel targets are community-maintained. The official documentation targets Vercel; AWS, Cloudflare, and other adapters are separate packages with their own maintenance burden and compatibility lag.
Nitro's presets are first-party, used by Nuxt.js, and maintained by the UnJS team. When Cloudflare changes its Workers API, the Nitro preset is updated — and Litro inherits the fix without any action on our end.
This is the same shared-foundation argument made on the Litro vs Nuxt.js page. The server layer is not a differentiator. Using the same server as Nuxt means using the same battle-tested deployment infrastructure.
The Framework Maintenance Difference
When you evaluate a new framework, deployment adapter maintenance is often invisible until it isn't. The question isn't "does it have a Cloudflare adapter today?" — it's "who maintains that adapter and what's the upgrade path when the underlying platform changes?"
Litro's answer: the same team that maintains Nuxt's server layer.
Ready to deploy? See Getting Started for the full setup, or check the Coolify deployment guide for a self-hosted walkthrough.
Litro