---
title: "Getting started"
description: "Create an Outer server, define a schema, enable auth, add a procedure, and start with automatic migrations."
---

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

# Getting started

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](https://pglite.dev) — 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:

```bash
npx giget@latest gh:ilhajs/outer/templates/minimal my-outer-app
cd my-outer-app
npm install
npm run dev
```

The 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](https://srvx.h3.dev), no frontend.                        | `npx giget@latest gh:ilhajs/outer/templates/minimal`     |
| `ilha`        | Full-stack starter: Outer as a [Nitro](https://nitro.build) server entry, [Ilha](https://ilha.build) + 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](https://neon.tech) 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.

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

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

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

## Project structure

```txt
my-outer-app/
├── src/
│   ├── schema.ts   # schema("1.0.0")...build() versions
│   └── index.ts    # await new Outer(...).schema(...).auth(...).procedure(...).start()
└── package.json
```

Keep schema versions in their own file. You add versions over time and never edit old ones — or derive the next version with [`.extend()`](/guide/schema#extend-a-previous-version).

## Next steps

- [Schema](/guide/schema) — define tables, columns, and relations
- [Migrations](/guide/migrations) — register versions and apply them
- [Resources](/guide/resources) — generate CRUD for a table
- [Auth](/guide/auth) — Better Auth, sessions, and API keys
- [Permissions](/guide/permissions) — guard procedures and resources
- [Procedures](/guide/procedures) — write the endpoints CRUD does not cover
- [Plugins](/guide/plugins) — package procedures, routes, and middleware
- [Database](/guide/database) — query with Kysely and the Sola ORM
- [Deployment](/guide/deployment) — pick a database and a host

Source: https://outer.now/guide/getting-started/index.mdx
