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

OptionDefaultDescription
port3000Port 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

OptionDefaultDescription
runtime'nodejs'Vercel serverless function runtime. Use 'edge' for Edge Functions.
regions['iad1']Deployment regions for serverless functions
splitfalseSplit 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

OptionDefaultDescription
name"catmint-app"Worker name used in wrangler.jsonc
routes["/*"]Worker route patterns
compatibilityDateBuild dateCloudflare Workers compatibility date
maxBodySize1048576Maximum 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

AdapterWhat 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
VercelNo 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:

PlatformConfiguration
Node.js.env files or system environment variables
VercelVercel dashboard or vercel env CLI
Cloudflarewrangler.toml or Cloudflare dashboard
Dockerdocker run -e flags or Docker Compose

Remember that env.private variables are only available on the server. Only variables accessed via env.public (prefixed with PUBLIC_) are embedded in the client bundle at build time.

Adapter Comparison

AdapterPackageRuntimeUse Case
Node.js@catmint/adapter-nodeNode.jsSelf-hosted, Docker, VMs, any Node.js environment
Vercel@catmint/adapter-vercelServerlessVercel platform with automatic scaling
Cloudflare@catmint/adapter-cloudflareEdgeGlobal edge deployment with low latency
None (static)--NoneFrontend-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:

  1. Serve client assets (hashed, immutable) — no middleware
  2. Serve static routes without middleware ancestors — fast path, no middleware
  3. Execute middleware chain via the SSR entry
  4. If middleware short-circuits, return its response (e.g., 401, 403, redirect)
  5. If middleware passes, proceed to the route handler
  6. 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 of dist/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

AdapterMiddleware ExecutionStatic + Middleware Handling
catmint startRuns in the built-in production serverReads from dist/prerendered/
Node.jsRuns in the standalone Node.js serverReads from dist/prerendered/
Vercel (Node.js)Runs in the serverless functionReads from dist/prerendered/
Vercel (Edge)Runs in the Edge functionInlined via prerendered.js
CloudflareRuns in the WorkerInlined 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(),
});