Self-hosted · MIT licensed · Alpha

Your backend. Your server. Nobody else's cloud.

A typed backend builder that runs anywhere you can run Node. No dashboard, no vendor SDK, no bill that grows faster than your app.

npx giget@latest gh:ilhajs/outer/templates/minimal my-outer-app
const server = new Outer({ db: pglite() })
  .schema(v1_0)
  .auth({ secret: process.env.AUTH_SECRET! })
  .resource("post", {
    permissions: { create: "authenticated", update: "owner" },
    ownerColumn: "userId",
  })
  .build();

serve({ fetch: (req) => server.handle(req) });

Built entirely on tools you already trust

Typed SQL queries
Powered by Kysely
Type-safe API calls
Powered by oRPC
Sessions & user accounts
Powered by Better Auth
Zero-infra database
Powered by embedded Postgres
Why leave a hosted BaaS

You already have a server. You don't need to rent a second backend on top of it.

Your data lives in someone else's database, behind their dashboard and their billing.

Your data lives in your Postgres, on your infra. `.outer/pglite` is a folder you own.

Auth is a black box you configure through a settings UI.

Auth is Better Auth — real code you can read, extend, and call directly from `context.auth`.

REST/GraphQL client code is generated after the fact and quietly drifts from your schema.

The client type is inferred straight from your server. If it compiles, it matches.

Scaling past the free tier means picking a new pricing plan.

Scaling means giving the box you already pay for more CPU and RAM.

What's in the box

Everything a small backend needs, none of the infra.

Typed schema, auto-generated CRUD

One .resource() call turns a table into list, get, create, update, delete — with clean 409/400/404 errors instead of raw 500s.

.resource("post", {
  permissions: { create: "authenticated", update: "owner" },
  ownerColumn: "userId",
})
// -> post.list, post.get, post.create,
//    post.update, post.delete
Auth and permissions, built in

.auth() mounts Better Auth directly. Declare public, authenticated, owner, or admin per procedure — ownership checks happen automatically.

.auth({ secret: process.env.AUTH_SECRET! })
.middleware(async ({ context, next }) => {
  const session = await context.auth.api
    .getSession({ headers: context.headers });
  return next({ context: { user: session?.user } });
})
Realtime without extra infrastructure

An async generator in a .procedure() handler is all it takes to stream updates over SSE, with resumable delivery via withEventMeta — no message broker to run.

.procedure("post.live", (base) =>
  base.handler(async function* ({ signal }) {
    for await (const row of postEvents.subscribe("created", { signal })) {
      yield withEventMeta(row, { id: String(row.id) });
    }
  }),
)
Deploy to a VPS, or go serverless

pglite() ships a zero-infra embedded Postgres for a VPS or Coolify box. Need serverless instead? Swap in any Kysely Dialect with the same builder chain.

$ npm run deploy

# templates/cloudflare   -> wrangler deploy
# templates/vercel-neon  -> vercel deploy --prod
# any other host         -> node dist/server.js

No codegen step. No client SDK to regenerate. Just types.

InferRouter<typeof server> hands your server's exact procedure types to @outerjs/sdk. Rename a field on the server and every caller turns red before you ship — not after a customer reports it.

import type { InferRouter } from "@outerjs/server";
import { createClient } from "@outerjs/sdk";

const client = createClient<InferRouter<typeof server>>({
  baseUrl: "http://localhost:3000",
}).build();

const me = await client.user.me();
// ^ typed end-to-end. No codegen, nothing to keep in sync.

Stop paying for infrastructure you already own.

Install the package, write your schema, and ship. Outer is alpha software — the API can still move — but it's MIT licensed, has no telemetry, and never phones home. Your server, your rules.