Outer is a TypeScript backend framework built on Kysely, oRPC, and Better Auth. You describe your database, auth, and API in one builder chain, and get back a fetch-compatible handler you can mount anywhere.
By default it runs on PGlite — real Postgres, embedded, writing to local disk — so you need no external services to start.
Before you begin
You need Node.js 20 or later and a package manager. The template installs everything else.
Create a server
Scaffold a project and start it:
npx giget@latest gh:ilhajs/outer/templates/minimal my-outer-app
cd my-outer-app
npm install
npm run devThe server listens on http://localhost:3000. Call POST /rpc/user/me to check that it responds.
Pick a different template
| Template | What you get | Command |
|---|---|---|
minimal |
The smallest Outer server — new Outer(...).start() behind srvx, no frontend. |
npx giget@latest gh:ilhajs/outer/templates/minimal |
ilha |
Full-stack starter: Outer as a Nitro server entry, Ilha + Vite on the frontend. | npx giget@latest gh:ilhajs/outer/templates/ilha |
cloudflare |
Outer on Cloudflare Workers — a Durable Object’s SQLite for data, R2 for uploads, instead of PGlite. | npx giget@latest gh:ilhajs/outer/templates/cloudflare |
vercel-neon |
Outer on Vercel Functions — Neon Postgres for data, Vercel Blob for uploads, instead of PGlite. | npx giget@latest gh:ilhajs/outer/templates/vercel-neon |
Write your first server
Every Outer app is one chain. Read it top to bottom: a schema version, auth, then the procedures your clients call. .start() builds the server and applies migrations in one step.
import { Outer } from "@outerjs/server";
import { schema } from "@outerjs/server/schema";
import { pglite } from "@outerjs/server/pglite";
const v1_0 = schema("1.0.0")
.auth() // Better Auth tables: user, session, account, verification
.table("post", (t) => ({
id: t.serial().primaryKey(),
title: t.text(),
userId: t.text().references("user", "id"),
}))
.build();
const server = await new Outer({ name: "My API", baseUrl: "http://localhost:3000", db: pglite() })
.schema(v1_0)
.auth({ secret: process.env.AUTH_SECRET! })
.procedure("user.me", (base) => base.handler(({ context }) => context.user), {
permission: "authenticated",
})
.start();
serve({ fetch: (req) => server.handle(req) });This server exposes POST /rpc/user/me plus the Better Auth routes under /api/auth/**. .auth() resolves the session once per request, so context.user is already there — you never write a getSession middleware.
Order the chain correctly
Call the methods in this order: .schema() → .middleware() → .procedure() → .start() (or .build()). Each call types the next one, so a procedure only sees the tables and context fields declared above it.
.auth(), .openapi(), .mcp(), .admin(), .files(), and .use() can appear anywhere before .start() / .build().
Run migrations
.start() calls build() then migrateToLatest() and throws if migration fails. Prefer it for self-hosted apps:
const server = await new Outer({ db: pglite() }).schema(v1_0).start();Use .build() when you need manual control — for example a deploy-time script, or logging the migration result:
const server = new Outer({ db: pglite() }).schema(v1_0).build();
const { error, results } = await server.migrator.migrateToLatest();
if (error) throw error;On serverless, run migrations from a deploy script instead of the request path — see Deployment.
Project structure
my-outer-app/
├── src/
│ ├── schema.ts # schema("1.0.0")...build() versions
│ └── index.ts # await new Outer(...).schema(...).auth(...).procedure(...).start()
└── package.jsonKeep schema versions in their own file. You add versions over time and never edit old ones — or derive the next version with .extend().
Next steps
- Schema — define tables, columns, and relations
- Migrations — register versions and apply them
- Resources — generate CRUD for a table
- Auth — Better Auth, sessions, and API keys
- Permissions — guard procedures and resources
- Procedures — write the endpoints CRUD does not cover
- Plugins — package procedures, routes, and middleware
- Database — query with Kysely and the Sola ORM
- Deployment — pick a database and a host