Framework adapters
adaptersWire MainSDK into Next.js, Hono or Express with one import, so every request gets a trace, a context, ordered hooks, problem-detail errors and the sign-in routes.
adaptersFree in your app, Apache-2.0Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.
Stability The promise an interface ships with.
- Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
- Beta Usable in production; may change before it becomes stable.
- Experimental May change or go away; needs an explicit opt-in in main.yaml.
An adapter connects MainSDK to the web framework you already run. One line in your server gives every request a trace, a request context, ordered hooks and problem-detail errors.
At a glance
import { Hono } from "hono";
import { mainsdk } from "mainsdk/hono";
const app = new Hono();
app.use("*", mainsdk()); // trace, context, hooks, problem details, sign-in routeshello.test.ts
import { createTestApp } from "@mainsdk/testing";
import { withMainSDK } from "mainsdk/next";
const app = createTestApp({ yaml: "schema: 1\nproject: prj_test\n" });
const handler = withMainSDK(async () => Response.json({ ok: true }), { app });
const res = await handler(new Request("http://app.test/hello")); // res.status is 200Calls
mainsdk.proxy()withMainSDK(handler)mainsdkAction(handler)mainsdk()mainsdk.errors()worker({ fetch })worker(handler)withPrisma(client)withDrizzle(db)
Guarantees
Whatever the driver.
- Every request carries a trace, tenant, user, agent and region through AsyncLocalStorage
- Hooks run in a fixed order and a failing hook never changes the answer
- Errors become RFC 9457 problems and unknown errors never leak detail
- Health probes, shutdown draining and inbound protection stay in front of your handlers
- No peer dependency on the framework and no network call to MainSDK Cloud
- A market overlay answers config.get for the market of the request, with no redeploy
- A Prisma or Drizzle query is decided by the access policy and audited before it runs
Drivers
4 drivers. Select one to see its line in main.yaml.
adapters:
driver: nextChange this one line to swap providers; your calls stay the same.Next.js App Router integration
Codes
| Code | Where it appears | Meaning |
|---|---|---|
MAIN-DATA-0001 | Checkmainsdk check, before deploy | Raw database client used outside MainSDK |
Features
Nothing here has shipped yet. Each feature lands in its phase; the spec ticket builds it.
| Feature | Phase | Milestone | Spec ticket |
|---|---|---|---|
| Request context and trace propagation | Phase 1 | 0.2 | adapters#request-context |
| Ordered request hooks | Phase 1 | 0.2 | adapters#lifecycle |
| Next.js adapter | Phase 1 | 0.2 | adapters#next-adapter |
| Hono adapter | Phase 1 | 0.2 | adapters#hono-adapter |
| Express adapter | Phase 1 | 0.2 | adapters#express-adapter |
| Node worker adapter | Phase 1 | 0.2 | adapters#worker-adapter |
| Schedule tick route for serverless deploys | Phase 1 | 0.2 | adapters#schedule-tick |
| Cloudflare Workers entry | Phase 1 | 0.2 | adapters#cloudflare-adapter |
| Prisma and Drizzle integration | Phase 1 | 0.2 | adapters#orm-integration |
| Adapter overhead budgets | Phase 1 | 0.2 | adapters#adapter-budgets |
| Driver marketplace with conformance | Phase 3 | 1.2 | driver-marketplace |
Quick start
import { Hono } from "hono";
import { mainsdk } from "mainsdk/hono";
const app = new Hono();
app.use("*", mainsdk());
app.get("/hello", (c) => c.json({ ok: true }));Call /hello with traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 and every log record the route writes carries that trace id. Without the header the adapter starts a new one. Inside any handler, requestContext() from mainsdk/adapters returns the context.
Configuration
schema: 1
project: my-app
auth:
routes: { base: /_mainsdk/auth }What you get
An adapter connects MainSDK to the web framework you already run. One line in your server gives every request a trace, a request context (tenant, user, agent, region), health probes, shutdown draining, inbound protection, ordered hooks and RFC 9457 errors. It also mounts the browser sign-in routes when auth is configured. Adapters add no dependency: they read the framework's request and response shapes, so nothing is installed next to your framework.
Next.js, Hono and Express
import { withMainSDK } from "mainsdk/next";
export const GET = withMainSDK(async (req, ctx) => Response.json({ trace: ctx.traceId }));import { mainsdk } from "mainsdk/next";
export const proxy = mainsdk.proxy();import { mainsdkAction } from "mainsdk/next";
export const save = mainsdkAction(async (input: { name: string }, ctx) => ({ saved: input.name }));import express from "express";
import { mainsdk } from "mainsdk/express";
const app = express();
app.use(mainsdk());
app.get("/hello", (_req, res) => res.json({ ok: true }));
app.use(mainsdk.errors()); // last: turns a thrown error into a problemCloudflare Workers
The main = "src/worker.ts" that mainsdk deploy config writes into wrangler.toml imports the Workers entry. fetch runs through the same layers as the other adapters; scheduled runs every schedule whose cron matches the event, in process, with no HTTP call.
import { Hono } from "hono";
import { worker } from "mainsdk/cloudflare";
const app = new Hono();
app.get("/hello", (c) => c.json({ ok: true }));
export default worker({ fetch: app.fetch });Schedules on serverless
When deploy is in main.yaml, every adapter answers POST /_mainsdk/jobs/tick?schedule=<name> itself, with the bearer in deploy.tick.secret, so Vercel cron can wake a schedule. Without a deploy section the path reaches your own routes.
Job workers
import { worker } from "mainsdk/worker";
declare function buildReport(orderId: string): Promise<void>;
export default worker(async (input: { orderId: string }, ctx) => {
// ctx.traceId is the producer's trace; ctx.user.id is a pseudonym; ctx.retry is 0 on the first attempt
await buildReport(input.orderId);
});Register it with jobs.on("report", mod.default). Every log record of the attempt carries the producer's trace_id, user.id and retry. buildReport is your own function.
Market overlays
main.<market>.yaml holds only what differs in one market. The adapter reads the market of each request from resolve (or a context hook) and config.get answers from that market's overlay, so one deployment serves every market. Files are read at start and again on config.refresh(); a broken file keeps the last good overlay. A pushed value still wins over an overlay, and an overlay wins over config.values.
import { config } from "mainsdk";
import { mainsdkAction } from "mainsdk/next";
import { headers } from "next/headers";
export const pay = mainsdkAction(
async () => config.get("payments.driver"), // "razorpay" in market in, from main.in.yaml
{ headers, resolve: (req) => ({ market: req.headers.get("x-market") ?? undefined }) },
);Prisma and Drizzle
Wrap the client once. Every query is decided by access for the person in the request context before it runs, and the decision is audited. A denied query never reaches the database and answers MAIN-ACCESS-0001 (403).
import { PrismaClient } from "@prisma/client";
import { withPrisma } from "mainsdk/prisma";
import { withDrizzle } from "mainsdk/drizzle";
declare const pool: object; // your own connection pool
declare function drizzle(pool: object): object; // from your own drizzle driver
export const prisma = withPrisma(new PrismaClient()); // orders.read, orders.create, orders.update, orders.delete
export const db = withDrizzle(drizzle(pool)); // same actions, per tableThe action is <model or table>.<read|create|update|delete>; a raw query is db.raw. An operation the wrapper does not know is decided as <model>.<operation>, so it is denied unless a role grants it. Pass subject when roles do not come from the tenant. mainsdk check reports a raw client with MAIN-DATA-0001. drizzle and pool are your own.
Hooks
Pass hooks to any adapter. They run in a fixed order: context, before, your handler, after, then error (only when something threw) and finally. A failing before skips the handler and runs error and finally. A hook that throws is logged and never changes the answer.
import { Hono } from "hono";
import { mainsdk } from "mainsdk/hono";
declare const metrics: { done(traceId: string): void };
declare function warmCaches(): Promise<void>;
declare function flushQueues(): Promise<void>;
const app = new Hono();
app.use("*", mainsdk({
hooks: {
context: (ctx, req) => ({ segment: req.headers.get("x-segment") ?? undefined }),
finally: (ctx) => metrics.done(ctx.traceId),
},
startup: async () => warmCaches(), // once, before /readyz says ready
shutdown: async () => flushQueues(), // after draining, before exit
}));metrics, warmCaches and flushQueues are your own.
Errors
A MainError thrown in a handler is answered as application/problem+json with its code, fix and trace_id. Any other error becomes MAIN-CORE-0500 with no message, path or stack. Probes (/healthz, /readyz), the draining 503 and load shedding come first, so a failing handler never hides health.
Sign-in routes
When auth is configured, every adapter answers the routes of the browser client under auth.routes.base (default /_mainsdk/auth). Set authRoutes: false to keep them off. Routes for a method missing from auth.methods answer 404 MAIN-AUTH-0084. See the guide Add sign-in to a page.
Calls
| Call | What it does |
|---|---|
mainsdk() | Hono or Express middleware (mainsdk/hono, mainsdk/express) |
mainsdk.errors() | Express error handler, mounted last |
withMainSDK(handler) | Next.js route handler wrapper |
mainsdkAction(handler) | Next.js server action wrapper |
mainsdk.proxy() | Next.js proxy.ts or middleware.ts |
worker({ fetch }) | Cloudflare Workers entry (mainsdk/cloudflare) with fetch and scheduled |
worker(handler) | Job consumer wrapper (mainsdk/worker) |
withPrisma(client) | Prisma client whose queries are decided and audited (mainsdk/prisma) |
withDrizzle(db) | Drizzle database whose queries are decided and audited (mainsdk/drizzle) |
Errors and checks
| Code | Cause | Fix |
|---|---|---|
MAIN-CORE-0500 | A handler threw an error that is not a MainError | Read the log record with the same trace id |
MAIN-ACCESS-0001 | The policy denies the query a wrapped Prisma or Drizzle client made | Grant the action to the role, or stop the query |
MAIN-DATA-0001 | check found a Prisma or Drizzle client used without its wrapper | Wrap it with withPrisma or withDrizzle |
MAIN-AUTH-0080 | A sign-in route got the wrong method or body | Call it as the guide shows |
MAIN-AUTH-0084 | The route belongs to a method auth.methods does not list | Add the method or stop calling the route |
MAIN-WEB-0002 | A cross-site POST to a sign-in route | Post from your own origin |
Swapping drivers
The drivers are the frameworks: Next.js is the default, then Hono, Express and Cloudflare Workers. Each is its own import (mainsdk/next, mainsdk/hono, mainsdk/express, mainsdk/cloudflare) and they behave alike, so moving a server from one to another changes the import and the wiring line, not your handlers or your main.yaml.
Testing
createTestApp() gives the adapter an app with in-memory drivers, and the handler takes a standard Request, so no framework needs to run. Pass the test app as app and assert on the answer and on app.log.records.
import { createTestApp } from "@mainsdk/testing";
import { withMainSDK } from "mainsdk/next";
const app = createTestApp({ yaml: "schema: 1\nproject: prj_test\n" });
const handler = withMainSDK(async () => Response.json({ ok: true }), { app });
const res = await handler(new Request("http://app.test/hello")); // res.status is 200Next
- Browser sign-in: the routes every adapter mounts.
- Logs: the records that carry the trace id.
- Add sign-in to a page
For agents
Goal: wire MainSDK into this server framework.
Touch: the server entry file (one app.use or one exported proxy) and, for Next.js, the route files you want wrapped.
Do not: write your own trace, error or health handling next to the adapter, or install a framework package for it.
Verify: call a route with a traceparent header and check the log record; npx mainsdk check.
Stop and ask if: the framework is not Next.js, Hono or Express.Was this page helpful?