Skip to content

API reference

Every option you can pass to new Outer(...), every builder method including .start() and .use(), the schema() builder, and the built server.

Updated View as Markdown

Complete reference for @outerjs/server: the Outer constructor, every builder method and its config, the schema() builder, plugins, and the object .build() / .start() returns.

For task-oriented explanations, start with the guides — Schema, Permissions, and Procedures.

Package entry points

Import the core builder from the root and the rest from subpath entries:

Import Members
@outerjs/server Outer, ORPCError, mcp, liveIterable, memoryRateLimitStore, and all context types
@outerjs/server/pglite pglite
@outerjs/server/schema schema, timestamps, parseSet, toSet, InferDB, SchemaResult, and the table types
@outerjs/server/secrets fromEnv, fromRecord, fromSchema, memorySecrets, OuterSecrets, StandardSchemaV1
@outerjs/server/storage fromUnstorage, fromS3, memoryStorage, OuterStorage

The schema, secrets, and storage members are only importable from their subpaths — the root exports the builder and context types.

new Outer(params)

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 },
});
Param Type Required Description
name string no Display name. Used as the OpenAPI spec title and reported by _admin.meta.
baseUrl string no Public origin of the server. Used as Better Auth’s default baseURL and as servers[].url (<baseUrl>/rest) in the OpenAPI spec.
db { dialect: Dialect; kind: "postgres" | "sqlite"; live?: LiveProvider } yes A Kysely Dialect plus its family. kind drives DDL generation, Better Auth’s schema, and error mapping, so it must match the dialect. Optional live enables live queries. Use pglite() for the zero-infra default.
cors CorsConfig no Cross-origin browser callers allowed to reach /rpc/**, /api/auth/**, and the admin API. Merged into Better Auth’s trustedOrigins when .auth() is used. Without it, no Access-Control-Allow-Origin header is set.
storage OuterStorage no Object store for file bytes. Surfaced as context.storage and used by .files(), which throws without it. See File uploads.
secrets OuterSecrets no Runtime-agnostic accessor for env secrets and bindings, surfaced as context.secrets. Build it with fromSchema / fromEnv / fromRecord. See Secrets.
kv OuterKV no Key/value store, surfaced as context.kv. Pass any unstorage instance — createStorage(...), Nitro’s useStorage(), Cloudflare KV, or Vercel Runtime Cache. Supports TTL via setItem(key, value, { ttl }). See Key/value store.
onError (error, info) => void no Called for unexpected failures only, never for deliberate ORPCError responses (400, 401, 403, 404, 409, …), which are application behavior. info.source is "rpc" | "rest" | "route" | "mcp". Defaults to console.error; pass () => {} to silence it.
health boolean | { path?: string } no Mounts GET /health with a select 1 probe — 200 {"status":"ok","database":"up"}, or 503 when the probe fails. Enabled by default; false omits it. A .route() on the same path wins. Not rate limited.
rateLimit RateLimitConfig no Per-caller limit on /rpc/** and /rest/**. Off by default. /api/auth/** is excluded, since Better Auth has its own.

RateLimitConfig

Prop Type Default Description
max number Requests allowed per window, per key.
windowMs number Window length in ms. Fixed window, not sliding.
key (event, user) => string user id, else client IP Identifies the caller. The default falls back to x-forwarded-for / x-real-ip.
skip (event) => boolean Return true to bypass the limit for a request.
store RateLimitStore memoryRateLimitStore() Swap in Redis or Upstash to share counters across replicas.

Over-limit requests get 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. If your proxy strips both forwarded-IP headers, every anonymous caller shares one bucket — pass key explicitly there.

The header fallback is only trustworthy behind a proxy you control: x-forwarded-for / x-real-ip are plain request headers, so if traffic can reach the server directly a caller spoofs a fresh value per request and never hits the limit. In that setup, set key to something you can trust — a verified client IP or an API-key id.

CorsConfig

Prop Type Default Description
origins string[] Allowed origins, matched exactly against the request’s Origin header. ["*"] echoes any origin — for public, unauthenticated APIs.
credentials boolean false Sets Access-Control-Allow-Credentials: true, needed for cookie-based auth from another origin. Combining it with origins: ["*"] throws at .build() — it would let any site make authenticated requests with a visitor’s cookies. List origins explicitly.

pglite(config?)

Embedded, zero-infra Postgres from @outerjs/server/pglite. Returns a { dialect, kind: "postgres" } pair to pass as db.

Prop Type Default Description
dataDir string <cwd>/.outer/pglite Data directory, created if missing. Pass a memory:// URL for an in-memory instance, which suits tests.

Bundled extensions

pglite() loads two PGlite extensions into the instance it builds. It returns { dialect, kind, live, client }live backs Sola’s live queries, and client is the PGlite instance for anything neither the query builder nor Sola covers.

Extension Package Reachable from
vector @electric-sql/pglite-pgvector Plain SQL — CREATE EXTENSION IF NOT EXISTS vector runs before any query
live @electric-sql/pglite/live context.db.query.<table>.live() — wired up as a LiveProvider

Both packages are optional peers you install yourself: bun add @electric-sql/pglite @electric-sql/pglite-pgvector.

Builder methods

Call them in this order: .schema().middleware().procedure().start() (or .build()).

.auth(), .openapi(), .mcp(), .admin(), .files(), .route(), .resource(), and .use() can appear anywhere before .start() / .build(). .resource() needs a prior .schema() that defines its table.

.schema(schemaResult)

Registers a schema version built with schema(). Every call becomes one migration step, and the latest version types context.db. It takes a single SchemaResult argument and no options.

.auth(config)

Enables Better Auth and mounts ALL /api/auth/**. It narrows context.auth to non-null and resolves the session once per request into context.user and context.session — no getSession middleware, and no extra query however many procedures or checks read it.

AuthConfig is Better Auth’s full BetterAuthOptions minus database, which Outer wires from your db dialect:

Prop Type Required Description
secret string yes Signs and encrypts sessions, cookies, and tokens. Use at least 32 random characters in production.
baseURL BetterAuthOptions["baseURL"] no Defaults to the constructor’s baseUrl. Accepts Better Auth’s DynamicBaseURLConfig ({ allowedHosts, fallback?, protocol? }) for preview or dynamic domains.
trustedOrigins string[] no Merged with cors.origins from the constructor. CORS origins are always trusted, regardless of call order.
…rest BetterAuthOptions no Everything else Better Auth accepts: emailAndPassword, socialProviders, plugins, session, and so on.

.middleware(mw)

Adds an oRPC middleware to the base context. Whatever it returns via next({ context }) is merged into context — and into the types — for every procedure defined after it.

.procedure(name, cb, options?)

Registers an RPC procedure. Dot notation nests namespaces: "user.me" is served at POST /rpc/user/me. The callback receives the oRPC base builder, so chain .input(), .output(), and .handler() on it. Names must be unique, and the _admin namespace is reserved.

options guards the procedure without a hand-written check:

Prop Type Default Description
permission "public" | "authenticated" | "admin" | (args) => boolean "public" "authenticated" returns 401 when signed out. "admin" and function permissions return 403.
roles string[] ["admin"] Roles accepted by "admin".

There is no "owner" — that needs a row, which a bare procedure has none of. Use .resource(), or check inside the handler. Any non-public permission is registered at .build(), so forgetting .auth() throws at startup instead of failing per request.

.route(method, path, handler)

Mounts a raw H3 route for webhooks and custom REST endpoints, alongside your RPC routes. handler(event, context) receives the H3 event and the same context procedures get (headers, db, auth, user, session, storage), including the already-resolved session. Raw routes are registered before /rpc/** and win on overlapping paths.

.resource(name, options?)

Generates typed CRUD procedures — list, get, create, update, delete — for a table defined in the latest .schema(). Throws at build time if the table does not exist.

ResourceOptions:

Prop Type Default Description
permissions ResourcePermissions all "public" Per-action rules — see below.
ownerColumn string Column storing the creator’s user ID. Required when any permission is "owner". When create requires a session and this is set, the current user’s ID is injected into inserts.
listLimit { default?: number; max?: number; maxSkip?: number } 50 / 100 / 10000 Default and max rows per list call. maxSkip caps the offset so deep pagination cannot force full scans.
includable string[] [] Relations callers may include on list and get. Included rows are not checked against the related resource’s permissions, so only allow relations that are safe to expose.
writable string[] all columns Allowlist of columns callers may set on create/update; every other column is server-controlled and dropped from the input. Mutually exclusive with readonly.
readonly string[] [] Denylist of columns stripped from create/update input, leaving the rest writable. Mutually exclusive with writable.
live boolean | { revalidateMs?: number | false; max?: number } off Registers {name}.live, which is list as a stream, gated by the list permission with owner scoping included. revalidateMs (default 30000) re-checks that permission while the stream is open; max (default 100) caps subscriptions.

ResourcePermissions — each of list, get, create, update, and delete accepts:

Value Meaning
"public" No restriction (the default).
"authenticated" Requires a signed-in session.
"admin" Requires the admin role. user.role may be comma-separated.
"owner" Requires the signed-in user to own the row (ownerColumn). Not valid for create; on list it scopes results to the caller’s rows.
PermissionFn Custom check: ({ context, row? }) => boolean | Promise<boolean>.

.openapi(config?)

Mounts GET /openapi.json and a plain-JSON REST surface under /rest/** matching the spec. Requires the optional peers @orpc/openapi and @orpc/zod.

Prop Type Default Description
enabled boolean true Pass import.meta.env.DEV or similar to gate the spec to non-production.

.mcp(config?)

Serves the router as an MCP server over Streamable HTTP, so agents can call procedures as tools. Requires the optional peers orpc-mcp and @orpc/zod.

Exposure is opt-in per procedure: only those tagged with the mcp meta helper (mcp.tool(), mcp.resource(), mcp.prompt(), re-exported from @outerjs/server) are visible. The _admin namespace and file.* routes never are. Dots become underscores in tool names, so post.search is called as post_search.

Prop Type Default Description
enabled boolean true when called Gate the endpoint behind an env flag without removing the call.
path string /mcp Where the endpoint is mounted.
serverInfo { name?, version? } instance name + version Identity reported during initialize.
instructions string Guidance returned to the client during initialize.
enableDnsRebindingProtection boolean false Reject Origin and Host outside the allowlists.
allowedOrigins / allowedHosts string[] Exact-match allowlists used when protection is on.

.files(config?)

Mounts the upload surface — file.upload, list, get, delete, attach, detach, plus GET /files/:id — over the tables schema().files() registers. Requires an OuterStorage on the constructor or in config. Full guide: File uploads.

Prop Type Default Description
storage OuterStorage the constructor’s storage Overrides the instance-wide store for files only.
maxBytes number 10 * 1024 * 1024 Larger uploads are rejected with 413. Bytes are buffered in memory, so this is a real ceiling.
accept string[] all types Allowed MIME types. Wildcards such as "image/*" are accepted.
permissions { upload, list, get, delete } upload/list "authenticated", get/delete "owner" Same vocabulary as .resource(). "owner" makes the download route answer 404 — not 403 — to non-owners.
path string containing :id "/files/:id" Path of the download route.
roles string[] ["admin"] Roles that bypass ownership.

OuterStorage

Three methods, so core depends on no storage vendor. fromUnstorage(storage), fromS3(client, commands, bucket), and memoryStorage() ship with the package; anything else is an object literal.

type OuterStorage = {
  get(key: string): Promise<Uint8Array | null>;
  set(key: string, bytes: Uint8Array): Promise<void>;
  delete(key: string): Promise<void>;
};

OuterSecrets

Runtime-agnostic accessor for env secrets and bindings, passed as new Outer({ secrets }) and surfaced as context.secrets. fromSchema(schema, env) (validates a Zod / Standard Schema and infers the types), fromEnv() (process.env), fromRecord(record) (a typed env object), and memorySecrets(values?) ship with the package. Full guide: Secrets.

type OuterSecrets<T = Record<string, string | undefined>> = {
  get<K extends keyof T & string>(key: K): T[K];
  require<K extends keyof T & string>(key: K): NonNullable<T[K]>; // throws when unset/empty
  readonly all: T;
};

.admin(config?)

Enables the admin API — meta and schema introspection, migration status, and table CRUD — under the reserved _admin namespace at /rpc/_admin/**. Every admin procedure requires a signed-in session with an admin role, so you must also call .auth() or .build() throws.

Prop Type Default Description
listLimit { default?: number; max?: number } 50 / 200 Default and max rows per _admin.data.list call.
roles string[] ["admin"] Roles granted admin access. Match Better Auth’s admin plugin adminRoles if you customize it.

.use(plugin)

Registers an OuterPlugin. Can appear anywhere before .start() / .build(). Double-registering the same plugin.name throws. Full guide: Plugins.

.build()

Finalizes the chain and returns a BuiltOuter. Throws if any resource permission or .admin() requires auth but .auth() was never called. Does not run migrations — call migrator.migrateToLatest() yourself, or use .start() instead.

.start()

Convenience for the common build() + migrateToLatest() pair. Returns a Promise<BuiltOuter> and throws if migration fails. Prefer this on self-hosted startups; keep .build() when you need manual migration control (deploy scripts, logging the result).

const server = await new Outer({ db: pglite() }).schema(v1_0).start();

BuiltOuter

Member Description
handle(request) Plain Fetch handler (Request => Promise<Response>). Plug it into srvx, Bun.serve, Next.js route handlers, and others.
client(headers?) In-process router client that skips HTTP entirely, for SSR. Pass the incoming request’s Headers — or a function returning them, evaluated per call — so permissions see the caller’s session.
migrator Kysely migrator over the registered schema versions. Prefer .start() on boot; call await outer.migrator.migrateToLatest() when you need the result object or a separate deploy step.
router The internal oRPC router. Use InferRouter<typeof outer> for type-safe client generation.
db The same typed context.db handed to procedures (Kysely plus query and transact), for out-of-band work such as seeding after migrations.
close() Releases the database pool — including the embedded PGlite — and any rate-limit timers. Call it from your SIGTERM/SIGINT handler, and in tests that build more than one instance. Idempotent.

OuterPlugin

Extension point for third-party and app-specific features. Pass an instance to .use(). Full guide: Plugins.

type OuterPlugin = {
  name: string;
  configure?(): void;
  validate?(ctx: PluginContext): void;
  build?(ctx: PluginContext): PluginResult | void;
};

type PluginResult = {
  procedures?: Record<string, AnyProcedure>;
  routes?: Array<{ method: string; path: string; handler: (event, context) => unknown }>;
  middleware?: Array<(event, next) => Promise<unknown>>;
};
Hook When Purpose
configure At .use() time Fail fast on bad config.
validate During build, after Outer’s built-in checks Reject the build (for example if .auth() is missing).
build After built-in procedures (admin, files) are assembled Return procedures, routes, and/or middleware to mount.

Plugin middleware runs after CORS and rate-limit middleware, before routes. PluginContext exposes resources (read-only), base (oRPC builder), schemas, and name.

schema() builder

import { schema, timestamps } from "@outerjs/server/schema";

export const v1_0_0 = schema("1.0.0")
  .auth()
  .table("post", (t) => ({
    id: t.serial().primaryKey(),
    title: t.text(),
    body: t.text().nullable(),
    authorId: t.text().references("user", "id"),
    meta: t.jsonb().nullable(),
    ...timestamps(t),
  }))
  .relation("user", (rel) => rel.hasMany("post", { from: "id", to: "authorId" }))
  .build();

schema(version)

Starts a schema version. version is a free-form string, such as "1.0.0", shown in the OpenAPI spec and the admin API.

.auth()

Registers the Better Auth core tables (user, session, account, verification) with the admin plugin’s fields (user.role, banned, banReason, banExpires, and session.impersonatedBy) plus their relations.

Extend an auth table by re-declaring extra columns with .table("user", ...) after this call — columns merge, and yours win on name collisions. Never remove or rename the core auth columns.

Option Type Default Description
roles string[] Declares the recognized role set, narrowing user.role. A user may hold several at once, since Better Auth stores them comma-separated. Open when omitted.
apiKeys boolean false Adds the apikey table required by @better-auth/api-key — see API keys.

session.userId and account.userId cascade on delete, so removing a user is never blocked by their sessions.

.files(options?)

Registers a file metadata table for blobs held in an object store — the counterpart to .auth() for uploads. Columns: id (primary key), key (unique, the storage key the bytes live under, so a blob can never be double-registered), name, type (MIME), size, userId (nullable, references user), plus timestamps(t). Exported as FileTables.

Prop Type Default Description
owner boolean true Adds file.userId plus the user hasMany file and file belongsTo user relations, so it requires .auth(). Pass false for files with no per-user owner; the column and relations are omitted.
attachTo string[] [] Tables files can attach to. Each name x gets a pivot x_file (fileId, entityId, nullable role, integer position) and a manyToMany relation both ways. Unknown names are a type error.

The schema stops there: it does not read, write, or serve the bytes. That is .files() on the builder chain.

.table(name, define)

Defines a table, or extends one — re-declaring merges columns, and the later definition wins. define receives the column builder t:

Column type TS type (select) Notes
t.serial() number Database-generated; omitted from create inputs.
t.text() string
t.varchar() string
t.integer() number
t.boolean() boolean
t.timestamp() Date Resource inputs take ISO strings; outputs are Date | string depending on the dialect.
t.bigint() string 64-bit. A string, since a JS number loses precision past 2^53.
t.decimal() string Exact numeric (numeric, or text on SQLite). A string, so cents never pass through a float.
t.real() number Approximate. Do not store money in it.
t.date() Date Calendar date, no time component.
t.jsonb() unknown
t.uuid() string
t.bytes() Uint8Array Raw bytes (bytea or BLOB).

Column modifiers chain, and each returns the column:

Modifier Effect
.nullable() Allows NULL. Optional in inserts and typed | null.
.primaryKey() Marks the primary key, which drives the where type of resource get, update, and delete.
.unique() Adds a unique constraint.
.index() Non-unique index named {table}_{column}_idx. A no-op on .unique() columns.
.default(value) Literal default, quoted for you by column type: .default("user"), .default(false), .default(0). Defaulted columns are optional in resource create inputs.
.defaultSql(expr) Raw SQL default, emitted verbatim: .defaultSql("CURRENT_TIMESTAMP").
.references(table, column, actions?) Foreign key. actions takes onDelete and onUpdate"cascade" | "set null" | "restrict" | "no action". Without one, deleting a referenced row fails with an FK violation.
.enum(values, options?) Restricts a text or varchar column to a fixed set, narrowing the TS type to the union. { multiple: true } stores a comma-separated set — see Schema.

timestamps(t)

Returns createdAt and updatedAt timestamp columns with .defaultSql("CURRENT_TIMESTAMP"). Spread it into a .table() column object.

.relation(fromTable, define)

Declares a relation used by context.db.query (Sola) and by resource include. define receives a chain with:

Kind Signature
hasMany rel.hasMany(to, { from, to })
hasOne rel.hasOne(to, { from, to })
belongsTo rel.belongsTo(to, { from, to })
manyToMany rel.manyToMany(to, via, { from, to, pivotFrom, pivotTo })via is the pivot table; pivotFrom and pivotTo are the pivot columns referencing each side.

.extend(previous)

Deep-merges another SchemaResult’s tables and relations into this builder. Existing builder columns win on collision (same as re-declaring via .table()). Relations are concatenated and deduped by identity. Call it anywhere in the chain:

const v1_1 = schema("1.1.0")
  .extend(v1_0)
  .table("post", (t) => ({ tags: t.text().nullable() }))
  .build();

Full guide: Schema → Extend a previous version.

.build()

Freezes and returns the SchemaResult you pass to .schema().

Type helpers

Export Description
InferRouter<T> Extracts the oRPC router type from an Outer or BuiltOuter instance — the input to oRPC client creation.
OuterAdminRouter { _admin: AdminRouter } — the router shape for a type-safe admin-only client: createClient<OuterAdminRouter>({ baseUrl }).
InferDB<T> Maps a schema’s tables to Kysely row types.
AuthTables The table shape .auth() registers on the schema builder.
FileTables The table shape schema().files() registers — the file table plus any attachTo pivots.
OuterStorage The three-method object store contract accepted by new Outer({ storage }).
OuterSecrets<T> The env accessor contract accepted by new Outer({ secrets })get, require, all. Build it with fromSchema / fromEnv / fromRecord.
OuterKV The key/value store contract accepted by new Outer({ kv }) — the slice of unstorage’s Storage most apps use. See Key/value store.
StandardSchemaV1 The Standard Schema interface fromSchema accepts (Zod, Valibot, ArkType).
OuterRpcContext<TDB> The base procedure context: { headers, db, auth?, user, session, storage?, secrets?, kv? }. context.db is Kysely plus query (Sola) and transact(fn).

context.user and context.session

After .auth(), both are resolved once per request and shared with every procedure, raw route, and permission check. Their types narrow from optional to SessionUser | null and UserSession | null, so you skip the ?. dance:

.procedure("user.me", (base) => base.handler(({ context }) => context.user))

Both are null for anonymous callers, and always null when you never called .auth().

Navigation

Type to search…

↑↓ navigate↵ selectEsc close