Skip to content

new Outer()

Construct an Outer instance with a database dialect, optional CORS, rate limits, health, storage, then chain .schema(), .auth(), procedures, and .start().

Updated View as Markdown

Outer is the server builder. You construct it once with new Outer({ db, ... }), chain methods that type the next step, then call .start() or .build() to get a fetch-compatible handler.

import { Outer } from "@outerjs/server";
import { pglite } from "@outerjs/server/pglite";

const outer = new Outer({
  name: "My App",
  baseUrl: "https://api.example.com",
  db: pglite(),
  cors: { origins: ["https://app.example.com"], credentials: true },
});

db is required. Everything else on the constructor is optional. Builder methods such as .auth() and .procedure() come after construction, before .start() / .build().

Pass a database

db is a Kysely dialect plus its family:

Field Required Description
dialect yes Any Kysely Dialect
kind yes "postgres" or "sqlite" — drives DDL and error mapping; must match the dialect
live no Change feed for live queries. pglite() supplies one

For the zero-infra default, use pglite() from @outerjs/server/pglite — embedded Postgres writing to local disk. On serverless hosts, pass another dialect instead — see Deployment.

Other constructor options

Param Guide Role
name API reference OpenAPI title and Hub meta
baseUrl API reference Public origin for Better Auth and OpenAPI servers
cors CORS Cross-origin browsers on /rpc/** and /api/auth/**
rateLimit Rate limiting Caps on /rpc/** and /rest/**
health Health check GET /health probe (on by default)
onError Unexpected errors Unexpected failures only — not deliberate ORPCErrors
storage .files() Object store for upload bytes
secrets Secrets Typed env / bindings as context.secrets
kv Key/value store Cache / ephemeral store as context.kv

The full parameter table lives in the API reference.

CORS

new Outer({
  db: pglite(),
  cors: { origins: ["https://app.example.com"], credentials: true },
});

cors lets browser clients on other origins call /rpc/**, /api/auth/**, and the admin API. Origins are also folded into Better Auth’s trustedOrigins when you call .auth().

Prop Type Default Description
origins string[] Exact Origin matches. ["*"] echoes any origin — for public, unauthenticated APIs
credentials boolean false Sets Access-Control-Allow-Credentials: true for cookie sessions

Combining credentials: true with origins: ["*"] throws at .build() — list origins explicitly. Pair cross-origin cookie auth with credentials: "include" on createClient.

Without cors, no Access-Control-Allow-Origin header is set. Same-origin and non-browser callers are unaffected.

Rate limiting

import { memoryRateLimitStore } from "@outerjs/server";

new Outer({
  db: pglite(),
  rateLimit: {
    max: 120,
    windowMs: 60_000,
    // optional: store: myRedisStore,
  },
});

rateLimit caps /rpc/** and /rest/** per caller. It is off by default. /api/auth/** is excluded — Better Auth has its own limits.

Prop Type Default Description
max number Requests allowed per window, per key
windowMs number Fixed window length in ms
key (event, user) => string user id, else client IP Identifies the caller
skip (event) => boolean Return true to bypass the limit
store RateLimitStore memoryRateLimitStore() Swap Redis/Upstash to share counters across replicas

Over-limit responses are 429 with Retry-After and RateLimit-* headers.

The default store is in-process, so each replica counts separately. Behind a load balancer the effective limit is max × replicas unless you share a store. The IP fallback reads x-forwarded-for / x-real-ip — only trust those behind a proxy you control, or pass key yourself.

Health check

new Outer({ db: pglite(), health: true }); // default
new Outer({ db: pglite(), health: false }); // omit
new Outer({ db: pglite(), health: { path: "/readyz" } });

When enabled (the default), Outer mounts GET /health with a select 1 probe: 200 {"status":"ok","database":"up"}, or 503 when the probe fails. A .route() on the same path wins. Health is not rate limited.

Unexpected errors

new Outer({
  db: pglite(),
  onError: (error, info) => {
    // log to your sink — never log secrets or tokens
    console.error(info.source, error);
  },
});

onError runs for unexpected failures only. Deliberate ORPCError responses (400, 401, 403, 404, 409, …) are application behavior and never call it. info.source is "rpc" | "rest" | "route" | "mcp". The default is console.error; pass () => {} to silence it.

Chain methods after the constructor

Order matters for typing: .schema().middleware().procedure().start() (or .build()).

.auth(), .openapi(), .mcp(), .admin(), .files(), .route(), .resource(), and .use() can appear anywhere before .start() / .build().

const server = await new Outer({ db: pglite() })
  .schema(v1_0)
  .auth({ secret: process.env.AUTH_SECRET! })
  .resource("post", {
    permissions: { list: "public", create: "authenticated" },
    ownerColumn: "userId",
  })
  .procedure("user.me", (base) => base.handler(({ context }) => context.user), {
    permission: "authenticated",
  })
  .start();

export default { fetch: (req: Request) => server.handle(req) };
Call Prefer when
.start() Self-hosted boot — build() + migrateToLatest(), throws on migration failure
.build() Deploy scripts, logging migration results, or running migrations off the request path

See Introduction → Run migrations.

What .start() / .build() returns

A BuiltOuter instance:

Member Description
handle(request) Plain Fetch handler for Bun, Node, srvx, Nitro, Next.js, and others
client(headers?) In-process RPC for SSR — see Client
migrator Apply schema versions when you used .build() instead of .start()
router Input to InferRouter<typeof server> for typed clients
db Same typed context.db as procedures, for seeding and scripts
close() Release the pool / PGlite and rate-limit timers

Next steps

Navigation

Type to search…

↑↓ navigate↵ selectEsc close