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.
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
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! })
.middleware(async ({ context, next }) => {
const session = await context.auth.api
.getSession({ headers: context.headers });
return next({ context: { user: session?.user } });
})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.jsNo 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.