Adapters

Adapters are thin layers that translate between a platform's native request handling and pracht's Web Request/Response interface. pracht ships adapters for Cloudflare Workers, Vercel Edge Functions, and Node.js.

Architecture

Every adapter follows the same request flow:

Platform request (Node / CF / Vercel)
  → Convert to Web RequestIs this a static asset?  → Yes: serve from dist/client/
  → Is this a prerendered page?  → Yes: serve static HTML (Node checks ISG staleness)
  → Delegate to handlePrachtRequest()
  → Convert Web Response back to platform response

Adapters also preserve route and shell document headers for prerendered HTML so static SSG/ISG responses match dynamic document responses.

Cloudflare Workers

Deploy to Cloudflare's global edge network. Static assets are served from the ASSETS binding, dynamic routes are handled by the Worker, and regenerated ISG HTML is stored in the Workers Cache API with ASSETS as the build-time fallback.

Setup

vite.config.ts
import { defineConfig } from "vite";
import { pracht } from "@pracht/vite-plugin";
import { cloudflareAdapter } from "@pracht/adapter-cloudflare";

export default defineConfig({
  plugins: [pracht({ adapter: cloudflareAdapter() })],
});
package.json
{
  "dependencies": {
    "@pracht/core": "*",
    "@pracht/adapter-cloudflare": "*"
  }
}

Build output

Running pracht build with the Cloudflare adapter emits:

dist/
  client/          // static assets served via ASSETS binding
    assets/
    index.html     // SSG pages
  server/
    server.js      // Worker bundle

Prerendered HTML receives document headers from the generated _pracht/headers.json asset.

Keep your wrangler.jsonc in the project root so you can add bindings without the build overwriting them.

Exporting Durable Objects and other primitives

Wrangler discovers Durable Objects, Workflows, Queues, and similar primitives from named exports on the Worker entry. Point the adapter at a dedicated module that re-exports them:

vite.config.ts
import { defineConfig } from "vite";
import { pracht } from "@pracht/vite-plugin";
import { cloudflareAdapter } from "@pracht/adapter-cloudflare";

export default defineConfig({
  plugins: [
    pracht({
      adapter: cloudflareAdapter({
        workerExportsFrom: "/src/cloudflare.ts",
      }),
    }),
  ],
});
src/cloudflare.ts
export { Counter } from "./workers/counter.ts";

Keep the matching bindings and migrations in wrangler.jsonc.

WebSockets

Cloudflare is the one adapter that can serve WebSocket upgrades, because a Durable Object can own a connection for longer than a request. Serve the handshake from an API route and forward it to the object:

src/api/ws.ts
import type { BaseRouteArgs } from "@pracht/core";

export async function GET({ context, request, url }: BaseRouteArgs) {
  if (request.headers.get("upgrade") !== "websocket") {
    return new Response("Expected a WebSocket upgrade", { status: 426 });
  }

  const { CHAT_ROOM } = context.env as { CHAT_ROOM: DurableObjectNamespace };
  const room = url.searchParams.get("room") ?? "lobby";
  return CHAT_ROOM.get(CHAT_ROOM.idFromName(room)).fetch(request);
}
src/workers/chat-room.ts
import { DurableObject } from "cloudflare:workers";

export class ChatRoom extends DurableObject {
  override async fetch(request: Request) {
    const { 0: client, 1: server } = new WebSocketPair();
    this.ctx.acceptWebSocket(server); // hibernation-aware
    return new Response(null, { status: 101, webSocket: client });
  }

  override webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) {
    for (const peer of this.ctx.getWebSockets()) peer.send(String(message));
  }
}

Pracht returns the 101 exactly as the handler produced it — copying it would drop the webSocket handle, since that property is a Cloudflare extension to ResponseInit rather than part of the fetch standard. Upgrades work in pracht dev too, because workerd serves dev for this adapter.

Cross-origin upgrades are rejected by default: browsers do not apply CORS to WebSocket, so the check that guards mutations guards handshakes as well.

Accessing Cloudflare bindings

The env object is passed through to your loaders and API routes via the context:

// src/routes/dashboard.tsx
export async function loader({ context }: LoaderArgs) {
  // context.env is the Cloudflare env object
  const user = await context.env.DB.prepare("SELECT * FROM users WHERE id = ?")
    .bind(userId)
    .first();
  return { user };
}

Deploy

pracht build
npx wrangler deploy

Vercel Edge Functions

Deploy using Vercel's Build Output API v3. SSG pages are served from the static file system; SSR and ISG routes go through the Edge Function.

Setup

// vite.config.ts
import { vercelAdapter } from "@pracht/adapter-vercel";
pracht({ adapter: vercelAdapter() })

// package.json
"@pracht/adapter-vercel": "*"

Static prerendered routes receive document headers through the generated Build Output headers config.

Build output

.vercel/
  output/
    config.json    // routes, rewrites, headers
    static/        // SSG pages served from the filesystem
    functions/
      render.func/ // Edge Function for SSR/API routes and webhook bridge
      pricing.func/
      pricing.prerender-config.json

Deploy

pracht build
npx vercel deploy --prebuilt

Node.js

Run pracht as a standard Node.js HTTP server. The adapter handles static file serving, ISG stale-while-revalidate, request translation, and the generated dist/server/server.js entry boots the production server directly.

Prerendered HTML receives document headers from dist/server/headers-manifest.json.

Setup

// vite.config.ts
import { nodeAdapter } from "@pracht/adapter-node";
pracht({ adapter: nodeAdapter() })

// package.json
"@pracht/adapter-node": "*"

Deploy

pracht build
node dist/server/server.js
// Server listening on http://localhost:3000

WebSockets

Node's http.Server delivers upgrade requests to its upgrade event rather than to the request handler, so a handshake never reaches pracht. Attach a WebSocket server to the same HTTP server instead — the generated entry exports handler, and only starts a server of its own when run as the process entrypoint:

import { createServer } from "node:http";
import { WebSocketServer } from "ws";
import { handler } from "./dist/server/server.js";

const server = createServer(handler);
const wss = new WebSocketServer({ noServer: true });

server.on("upgrade", (req, socket, head) => {
  // Check req.headers.origin yourself — this bypasses pracht entirely, so
  // pracht's same-origin protection does not apply.
  wss.handleUpgrade(req, socket, head, (ws) => wss.emit("connection", ws, req));
});

server.listen(3000);

Context Factory

Adapters inject platform-specific values into loaders and API routes via a context factory. With generated entries, point the adapter at a module that exports createContext:

vite.config.ts
nodeAdapter({ createContextFrom: "/src/server/context.ts" });
cloudflareAdapter({ createContextFrom: "/src/server/context.ts" });
vercelAdapter({ createContextFrom: "/src/server/context.ts" });
src/server/context.ts
// Node: inject a database pool
export function createContext({ request }: { request: Request }) {
  return {
    db: pool,
    ip: request.headers.get("x-forwarded-for"),
  };
}

// Cloudflare receives { request, env, executionContext }.
// Vercel receives { request, context }.

The context object is available as args.context in every loader, middleware, and API route handler.

Writing a Custom Adapter

A custom adapter exports a factory function that returns a PrachtAdapter object:

import type { PrachtAdapter } from "@pracht/vite-plugin";

export function myAdapter(): PrachtAdapter {
  return {
    id: "my-platform",
    serverImports:
      'import { handlePrachtRequest, resolveApp, resolveApiRoutes } from "@pracht/core";',
    createServerEntryModule() {
      return `
export default async function handle(request) {
  return handlePrachtRequest({
    app: resolvedApp,
    registry,
    request,
    apiRoutes,
    clientEntryUrl: clientEntryUrl ?? undefined,
    cssManifest,
    jsManifest,
  });
}
`;
    },
  };
}

At the runtime level, an adapter also typically needs to:

  1. Accept a platform request and convert it to a Web Request
  2. Check for static assets -- serve files from dist/client/ with appropriate headers
  3. Check for prerendered pages -- serve SSG/ISG HTML (with staleness checking for ISG when the platform supports it)
  4. Delegate dynamic requests to handlePrachtRequest() from pracht
  5. Convert the Web Response back to the platform's response format
  6. Provide a context factory for platform-specific values
  7. Export an entry module generator for the Vite plugin
ℹ️

See the source of @pracht/adapter-cloudflare or @pracht/adapter-node in the monorepo for a concrete reference implementation.