The open-source backend for the agentic internet.
Define a procedure once and serve it as typed RPC, REST, OpenAPI, and an MCP tool your agents can call. Real Postgres with pgvector, running on the same box as your app — no managed cloud to rent.
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
Deploys to
Full-power Postgres and an agent-ready API, with nothing to run but your app.
pgvector, out of the box
PGlite is real embedded Postgres with pgvector bundled in. Vector search on a $4 VPS — no managed tier, no add-on to switch on.
One router. Four surfaces.
Define a procedure once and it's served as typed RPC, REST, an OpenAPI spec, and an MCP tool. No second definition to keep in sync.
Your API, your agent's toolbox
`.mcp()` hands agents your shipped business logic — not raw database access — inheriting the exact permissions your app already enforces.
Schema to SSR, no HTTP hop
`outer.client()` calls procedures in-process during server rendering. Same types, no serialization, no localhost round-trip.
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.
Everything a small backend needs, none of the infra.
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() mounts Better Auth directly. Declare public, authenticated, owner, or admin per procedure — ownership checks happen automatically.
.auth({ secret: process.env.AUTH_SECRET! })
// the session is resolved once per request — no middleware
.procedure("user.me", (base) =>
base.handler(({ context }) => context.user),
{ permission: "authenticated" },
)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) });
}
}),
)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.files() mounts upload, download, listing, and attachments over the storage you already have — no signed-URL dance, no bucket SDK leaking into your handlers.
- Private by defaultOnly the uploader reads a file. Everyone else gets a 404, never a 403.
- Bytes stay yoursLocal disk on a VPS, R2, S3, or Vercel Blob — the database holds only metadata.
- Typed, like the restPass a File to the SDK and the request becomes multipart on its own.
// server — one call mounts the whole upload surface
.files({ maxBytes: 10 * 1024 * 1024 })
// client — no FormData, no upload endpoint to write
const { url } = await client.file.upload({
file,
attach: { table: "post", entityId: post.id },
});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.Install it, write your schema, ship.
Schema, auth, permissions, files, realtime, and a four-surface API are in the box today. A dashboard and typed vector columns are next. MIT licensed, no telemetry, nothing phoning home — the whole thing runs on hardware you already pay for.