---
title: "API reference"
description: "Every option you can pass to new Outer(...), every builder method including .start() and .use(), the schema() builder, and the built server."
---

> Documentation Index
> Fetch the complete documentation index at: https://outer.now/llms.txt
> Use this file to discover all available pages before exploring further.

# API reference

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](/guide/schema), [Permissions](/guide/permissions), and [Procedures](/guide/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)`

```ts
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](/guide/database#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()`](#filesconfig), which throws without it. See [File uploads](/guide/files#storage-adapters).                                                                                                 |
| `secrets`   | `OuterSecrets`                                                            | no       | Runtime-agnostic accessor for env secrets and bindings, surfaced as `context.secrets`. Build it with `fromSchema` / `fromEnv` / `fromRecord`. See [Secrets](/guide/secrets).                                                                                                   |
| `kv`        | `OuterKV`                                                                 | no       | Key/value store, surfaced as `context.kv`. Pass any [unstorage](https://unstorage.unjs.io) instance — `createStorage(...)`, Nitro's `useStorage()`, Cloudflare KV, or Vercel Runtime Cache. Supports TTL via `setItem(key, value, { ttl })`. See [Key/value store](/guide/kv). |
| `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()`](/guide/database#live-queries) — 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()`](#schema-builder). 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](/guide/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()`](#filesoptions) registers. Requires an `OuterStorage` on the constructor or in `config`. Full guide: [File uploads](/guide/files).

| 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.

```ts
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](/guide/secrets).

```ts
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`](#outerplugin). Can appear anywhere before `.start()` / `.build()`. Double-registering the same `plugin.name` throws. Full guide: [Plugins](/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()`](#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).

```ts
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()`](#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()`](#useplugin). Full guide: [Plugins](/guide/plugins).

```ts
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

```ts
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](/guide/auth#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()`](#filesconfig) 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](/guide/schema#enums). |

### `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:

```ts
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](/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](/guide/kv).           |
| `StandardSchemaV1`     | The [Standard Schema](https://standardschema.dev) 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:

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

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

Source: https://outer.now/guide/api-reference/index.mdx
