Deployment
Catmint uses an adapter system to deploy your application to different platforms. Adapters transform the build output into the format required by your target environment. This guide covers the built-in adapters, the build workflow, and static deployment for frontend-only applications.
Build Workflow
The deployment process has two steps: build and start.
# Build the application
pnpm catmint build
# Start the production server
pnpm catmint start
catmint build compiles
your application using Vite, producing optimized server and client bundles.
The adapter then transforms these bundles into the platform-specific format. catmint start runs the
built application using the configured adapter.
Adapter System
Adapters are configured in catmint.config.ts via
the adapter property. Each
adapter is a function that returns an object implementing the CatmintAdapter interface.
// CatmintAdapter interface
interface CatmintAdapter {
name: string;
adapt(context: AdapterContext): Promise<void>;
dev?(): DevPlatformHelper | Promise<DevPlatformHelper>;
}
The adapt hook runs
after the Vite build completes and transforms the output for the target platform.
The optional dev() hook is called once when the dev server starts. It returns a
DevPlatformHelper that provides per-request platform context during development,
enabling getPlatform(), cookies(), and headers() to work in catmint dev
exactly as they do in production. See Dev Mode Platform Context below.
Node.js Adapter
The @catmint/adapter-node package
builds your application as a standalone Node.js server. This is the most
flexible adapter and works anywhere Node.js runs.
pnpm add -D @catmint/adapter-node
// catmint.config.ts
import { defineConfig } from "catmint/config";
import node from "@catmint/adapter-node";
export default defineConfig({
mode: "fullstack",
adapter: node({
port: 3000,
host: "0.0.0.0",
}),
});
Configuration Options
| Option | Default | Description |
|---|---|---|
port | 3000 | Port the server listens on. Can be overridden by the PORT env var. |
host | '0.0.0.0' | Host the server binds to |
Docker Deployment
The Node.js adapter works well with Docker. Here is a multi-stage Dockerfile that builds and serves the application:
# Dockerfile
FROM node:20-alpine AS base
RUN corepack enable
# Install dependencies
FROM base AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
# Build the application
FROM base AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN pnpm catmint build
# Production image
FROM base AS production
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package.json ./
EXPOSE 3000
CMD ["pnpm", "catmint", "start"]
# Build and run
docker build -t my-catmint-app .
docker run -p 3000:3000 my-catmint-app
Vercel Adapter
The @catmint/adapter-vercel package
transforms the build output into Vercel's Build Output API v3 format for
seamless deployment on the Vercel platform.
pnpm add -D @catmint/adapter-vercel
// catmint.config.ts
import { defineConfig } from "catmint/config";
import vercel from "@catmint/adapter-vercel";
export default defineConfig({
mode: "fullstack",
adapter: vercel(),
});
Configuration Options
| Option | Default | Description |
|---|---|---|
runtime | 'nodejs' | Vercel serverless function runtime. Use 'edge' for Edge Functions. |
regions | ['iad1'] | Deployment regions for serverless functions |
split | false | Split routes into individual serverless functions |
Deploy by connecting your Git repository to Vercel or using the CLI:
# Deploy with Vercel CLI
pnpm catmint build
npx vercel deploy --prebuilt
Cloudflare Adapter
The @catmint/adapter-cloudflare package
builds your application for Cloudflare Workers, enabling edge deployment with
low latency worldwide.
pnpm add -D @catmint/adapter-cloudflare
For workerd dev mode (recommended), also install the Cloudflare Vite plugin:
pnpm add -D @cloudflare/vite-plugin wrangler
// catmint.config.ts
import { defineConfig } from "catmint/config";
import cloudflare from "@catmint/adapter-cloudflare";
const cf = cloudflare({
name: "my-app",
});
export default defineConfig({
mode: "fullstack",
adapter: cf,
vite: {
plugins: [cf.vitePlugin],
},
});
Adding cf.vitePlugin to the Vite plugins enables workerd dev mode -- your RSC and SSR environments run inside the real workerd runtime during catmint dev, giving you full Cloudflare Workers API compatibility. If you omit cf.vitePlugin, the adapter falls back to getPlatformProxy() emulation (only wrangler is required).
See the adapter-cloudflare API reference for details on both modes.
Configuration Options
| Option | Default | Description |
|---|---|---|
name | "catmint-app" | Worker name used in wrangler.jsonc |
routes | ["/*"] | Worker route patterns |
compatibilityDate | Build date | Cloudflare Workers compatibility date |
maxBodySize | 1048576 | Maximum request body size in bytes (default 1 MB) |
kvNamespaces | -- | KV Namespace bindings |
r2Buckets | -- | R2 bucket bindings |
d1Databases | -- | D1 database bindings |
durableObjects | -- | Durable Object bindings |
wrangler | -- | Raw wrangler config merged into the generated file (overrides) |
Cloudflare Bindings
Connect your Worker to Cloudflare platform services by declaring bindings in the adapter options:
import { defineConfig } from "catmint/config";
import cloudflare from "@catmint/adapter-cloudflare";
export default defineConfig({
mode: "fullstack",
adapter: cloudflare({
name: "my-app",
kvNamespaces: [{ binding: "CACHE", id: "abc123" }],
r2Buckets: [{ binding: "UPLOADS", bucketName: "user-uploads" }],
d1Databases: [
{ binding: "DB", databaseName: "mydb", databaseId: "def456" },
],
durableObjects: [{ name: "COUNTER", className: "Counter" }],
}),
});
For bindings not covered by the typed fields (Queues, Vectorize, AI, Hyperdrive, etc.),
use the wrangler passthrough:
cloudflare({
wrangler: {
ai: { binding: "AI" },
queues: {
producers: [{ binding: "MY_QUEUE", queue: "my-queue" }],
},
},
});
Access bindings at runtime via getPlatform():
import { createServerFn } from "catmint/server";
import { getPlatform } from "catmint/server";
interface CloudflarePlatform {
env: { DB: D1Database; CACHE: KVNamespace };
ctx: ExecutionContext;
}
export const getUsers = createServerFn(async () => {
const { env } = getPlatform<CloudflarePlatform>();
const { results } = await env.DB.prepare("SELECT * FROM users").all();
return results;
});
See the adapter-cloudflare API reference for the full bindings configuration format, and getPlatform for accessing platform-specific context from any server code.
Deploy with Wrangler:
pnpm catmint build
npx wrangler deploy --config dist/cloudflare/wrangler.jsonc
The Cloudflare Workers runtime does not support most Node.js built-in modules. During the build, the adapter scans server function sources for incompatible imports (
fs,child_process,node:, etc.) and logs warnings. Ensure your database driver and dependencies are edge-compatible.
Dev Mode Platform Context
When you run catmint dev, the dev server calls each adapter's optional dev() hook
at startup. This hook returns a DevPlatformHelper object that the dev server uses to
populate the request context on every incoming request. This means getPlatform(),
cookies(), and headers() all work during development -- no mocks or workarounds needed.
interface DevPlatformHelper {
/** Return platform context for a single request. */
getPlatform(
request: Request,
nodeReq?: unknown,
nodeRes?: unknown,
): unknown | Promise<unknown>;
/** Clean up resources when the dev server shuts down. */
close?(): void | Promise<void>;
}
Built-in Adapter Dev Behavior
| Adapter | What dev() provides |
|---|---|
| Node.js | { req, res } -- the raw Node.js request and response objects, matching the production shape |
| Cloudflare | { env, ctx, cf, caches } -- locally emulated bindings via wrangler's getPlatformProxy(), or full workerd runtime when cf.vitePlugin is added to the Vite config |
| Vercel | No dev() hook yet -- getPlatform() returns undefined during dev |
The Cloudflare adapter requires wrangler as a dev dependency. If wrangler is not installed, the dev server throws a descriptive error with install instructions.
pnpm add -D wrangler
Bindings configured in the Cloudflare adapter options (kvNamespaces, d1Databases, r2Buckets, durableObjects, wrangler passthrough) are automatically available during catmint dev -- catmint.config.ts is the single source of truth for both development and production. No separate wrangler.toml is needed.
See getPlatform for what each adapter returns and how to type it.
Static Deployment (Frontend Mode)
Applications using mode: 'frontend' produce
a static site with no server component. No adapter is needed. The build output
is a directory of HTML, CSS, and JavaScript files that can be deployed to any
static hosting provider.
// catmint.config.ts
import { defineConfig } from "catmint/config";
export default defineConfig({
mode: "frontend",
// No adapter needed for static sites
});
pnpm catmint build
# Output is in dist/ -- deploy this directory
Static output can be deployed to:
- Cloudflare Pages
- Netlify
- Vercel (static mode)
- GitHub Pages
- AWS S3 + CloudFront
- Any web server or CDN
Environment Variables
Environment variables are handled differently depending on the platform:
| Platform | Configuration |
|---|---|
| Node.js | .env files or system environment variables |
| Vercel | Vercel dashboard or vercel env CLI |
| Cloudflare | wrangler.toml or Cloudflare dashboard |
| Docker | docker run -e flags or Docker Compose |
Remember that
env.privatevariables are only available on the server. Only variables accessed viaenv.public(prefixed withPUBLIC_) are embedded in the client bundle at build time.
Adapter Comparison
| Adapter | Package | Runtime | Use Case |
|---|---|---|---|
| Node.js | @catmint/adapter-node | Node.js | Self-hosted, Docker, VMs, any Node.js environment |
| Vercel | @catmint/adapter-vercel | Serverless | Vercel platform with automatic scaling |
| Cloudflare | @catmint/adapter-cloudflare | Edge | Global edge deployment with low latency |
| None (static) | -- | None | Frontend-only apps, static hosting |
Middleware in Production
Middleware runs automatically in all production environments. Each adapter integrates middleware execution into its request pipeline — no additional configuration is required.
How It Works
During catmint build, all middleware.ts files are compiled into the SSR entry and a hasMiddleware flag is set in the build manifest. Each adapter checks this flag and, when middleware exists, calls executeMiddleware(url, request) before routing to page handlers, endpoints, or server functions.
The production request pipeline:
- Serve client assets (hashed, immutable) — no middleware
- Serve static routes without middleware ancestors — fast path, no middleware
- Execute middleware chain via the SSR entry
- If middleware short-circuits, return its response (e.g., 401, 403, redirect)
- If middleware passes, proceed to the route handler
- Merge middleware-added headers into the final response
Static Routes with Middleware
Routes using staticRoute() that have middleware in an ancestor directory are handled specially:
- At build time, a warning is emitted noting the route loses static-serving performance benefits
- The pre-rendered HTML is placed in
dist/prerendered/instead ofdist/static/ - At runtime, the request passes through middleware first; if middleware allows it, the pre-rendered HTML is served
- Static routes without middleware ancestors continue to be served directly (no middleware overhead)
For edge runtimes (Cloudflare Workers, Vercel Edge), a prerendered.js module is generated at adapter time that inlines the pre-rendered HTML as a pathname-to-string map, since these environments cannot read the filesystem.
Per-Adapter Behavior
| Adapter | Middleware Execution | Static + Middleware Handling |
|---|---|---|
catmint start | Runs in the built-in production server | Reads from dist/prerendered/ |
| Node.js | Runs in the standalone Node.js server | Reads from dist/prerendered/ |
| Vercel (Node.js) | Runs in the serverless function | Reads from dist/prerendered/ |
| Vercel (Edge) | Runs in the Edge function | Inlined via prerendered.js |
| Cloudflare | Runs in the Worker | Inlined via prerendered.js |
Middleware behavior is identical across all environments. The same middleware code runs in dev and production with no changes required.
Writing a Custom Adapter
You can create a custom adapter by implementing the CatmintAdapter interface.
This is useful for deploying to platforms not covered by the built-in adapters.
// my-adapter.ts
import type { CatmintAdapter, DevPlatformHelper } from "catmint/config";
export default function myAdapter(): CatmintAdapter {
return {
name: "my-adapter",
async adapt({ outputDir, clientDir, serverDir, log }) {
// Transform the build output for your platform
log("my-adapter: building...");
},
// Optional: provide platform context during `catmint dev`
dev(): DevPlatformHelper {
return {
getPlatform(request, nodeReq, nodeRes) {
// Return whatever shape your production server provides
return { req: nodeReq, res: nodeRes };
},
};
},
};
}
// catmint.config.ts
import { defineConfig } from "catmint/config";
import myAdapter from "./my-adapter";
export default defineConfig({
mode: "fullstack",
adapter: myAdapter(),
});