Typebase pulls authorization from your Postgres RLS policies and moves it to TypeScript middleware that the compiler actually checks. The schema, actions, and authentication live as .ts files in a typebase/ folder inside your current app—no separate repo, no dashboard—and the frontend calls them as if they were local functions, end-to-end typed.
The landing sums it up in one line: “Write actions, a database schema, and auth as TypeScript files in a typebase/ folder inside your app. Your frontend calls them like local functions, end-to-end typed, zero REST boilerplate.” It appeared as Show HN on August 26, 2026 and gathered a few dozen upvotes and a comment thread worth reading; we’ll get back to that at the end.
What is Typebase?
It’s a backend framework that lives inside your frontend project. You add a typebase/ folder, write your Postgres schema, server actions, and auth configuration as TypeScript files there, and a CLI generates a typed client and deploys the server.
The structure, as it appears in the getting started guide:
typebase/
├── _generated/ (auto-generated, do not edit)
├── db/
│ ├── schema.ts
│ └── relations.ts
├── actions/
│ ├── queries/
│ │ └── todos.ts
│ └── mutations/
│ └── todos.ts
├── typebase.json
└── env.ts
Underneath there’s no new stack: the schema is Drizzle ORM, authentication is a thin wrapper around better-auth, input validation accepts any validator compatible with Standard Schema (Zod, Valibot, ArkType), and the database is standard Postgres. Realtime runs over Server-Sent Events. Supported frontends are Next.js, SvelteKit, Nuxt, and Expo.
What is RLS in Supabase and why does Typebase want to replace it?
RLS—Row Level Security—is the Postgres mechanism that decides, row by row, who can read or write what. In Supabase it’s where much of the authorization logic lives: you write policies in SQL and the database applies them on every query.
Typebase’s argument is that SQL falls outside the reach of your tools. Its comparison page says of Supabase that “most logic lives in the database as SQL or RLS policies” and that Supabase “doesn’t have a strong concept of typed server functions.” On generated types it argues that “with Supabase, types are generated from your database schema as a separate step. They can drift out of sync.”
The sharpest version of the complaint is about code agents: with Supabase, “much of the business logic lives in SQL and Row Level Security policies, where the agent can’t lean on the TypeScript compiler at all.”
It’s worth being clear about what this is: it’s the framing one vendor does of its competitor, not an independent evaluation. But the underlying observation is one many people have already made. An RLS policy is a string of SQL that your editor doesn’t typecheck, that your test suite almost never exercises, and that your agent can’t reason about the way it reasons about a function. Typebase’s answer is to turn authorization into a function.
How do you install Typebase?
Two commands to add it, one to scaffold:
npm install typebase-io
npm install -D typebase-io-cli
npx typebase-io-cli init
init accepts flags—--with-auth, --with-db-publisher, and --skip-example—so you can generate the auth file and event publisher from the start instead of adding them later.
After that you’ll use these three commands all the time:
npx typebase-io-cli codegen # regenerates the typed client
npx typebase-io-cli db dev push # applies schema changes to the database
npx typebase-io-cli deploy dev # deploys the server
How do you define the schema?
In typebase/db/schema.ts, Drizzle-style, through Typebase’s p export:
import { p } from 'typebase-io/db';
export const todos = p.pgTable('todos', {
id: p.integer().primaryKey().generatedAlwaysAsIdentity(),
value: p.varchar({ length: 255 }).notNull(),
completed: p.boolean().notNull(),
createdAt: p.timestamp().notNull().defaultNow(),
});
Then npx typebase-io-cli db dev push applies it. Notice the word push: proper migrations are on the roadmap, not delivered. At the time this note was published, the roadmap lists “allow users to use migrations” as pending. If you’re going to run something with real data in it, that’s the point to watch.
How do you write an action?
An action is a chain: .input(), .output(), .handler(). Queries and mutations are the same primitive—the documentation is explicit: “there’s no technical difference between a ‘query’ and a ‘mutation’ in Typebase. They’re both actions, and the distinction is purely organizational.”
import { ServerError } from 'typebase-io/server';
import { z } from 'zod';
import { action } from '../../_generated/server.ts';
export const getOne = action
.input(z.object({ id: z.number() }))
.output(z.object({ id: z.number(), value: z.string(), completed: z.boolean() }))
.handler(async ({ db, input }) => {
const todo = await db.query.todos.findFirst({
where: { id: input.id },
});
if (!todo) throw new ServerError('NOT_FOUND');
return { id: todo.id, value: todo.value, completed: todo.completed };
});
The handler’s context is built based on what exists in your folder: db (typed Drizzle client) if db/schema.ts exists, auth if auth.ts exists, env if you configured environment variables, publisher if you have a publisher, and reqHeaders always. Errors are ServerError with a status code name—NOT_FOUND, UNAUTHORIZED, FORBIDDEN, BAD_REQUEST, and others—each mapped to an HTTP status.
There’s also .stream() instead of .handler() when you want an SSE response.
How does authorization work without RLS?
With middleware. .use() receives the context and returns values that merge into the handler’s context, fully typed. The pattern for a protected action that comes in the documentation is this:
export const authedAction = action.use(async ({ reqHeaders, auth }) => {
if (!reqHeaders) {
throw new ServerError('UNAUTHORIZED');
}
const sessionData = await auth.api.getSession({
headers: reqHeaders,
});
if (!sessionData?.session || !sessionData?.user) {
throw new ServerError('UNAUTHORIZED');
}
return {
user: sessionData.user,
};
});
There’s the whole argument, in ten lines. authedAction is a value on which you build the other actions, user arrives at the handler typed, and if you rename a field on the user object the compiler tells you every place it breaks. The equivalent in RLS is a policy expression inside the database that no part of your toolchain reads.
Authentication itself is a single file, typebase/auth.ts:
import { defineAuth } from 'typebase-io/server';
export const auth = defineAuth({
trustedOrigins: ['http://localhost:3000'],
emailAndPassword: {
enabled: true,
},
});
The documentation describes defineAuth as “a thin wrapper around better-auth”: it accepts better-auth options except database, which Typebase manages. So socialProviders and the rest of the better-auth surface are available here.
How does the frontend call actions in Next.js?
You create a client from the generated Router type. Simple version:
import { createRouterClient } from 'typebase-io/client';
import type { Router } from '../../typebase/_generated/server';
export const client = createRouterClient<Router>({
url: process.env.TYPEBASE_APP_URL_DEV || process.env.TYPEBASE_APP_URL || '',
});
Or the version with TanStack Query, if you want caching and hooks in client components:
import { createTanstackQueryClient } from 'typebase-io/client';
import type { Router } from '../../typebase/_generated/server';
export const client = createTanstackQueryClient<Router>({
url: process.env.NEXT_PUBLIC_TYPEBASE_APP_URL || '',
});
And then, in Next.js:
// Server Component
const todos = await client.queries.todos.getMany();
// Server Action
await client.mutations.todos.create({ value: value.trim() });
// Client Component, with TanStack Query
const { data: todos } = useQuery(client.queries.todos.getMany.queryOptions());
Client-side authentication comes from the same package:
import { createAuthClient } from 'typebase-io/client/auth/react';
export const authClient = createAuthClient();
authClient.useSession() in client components; getServerSession from typebase-io/client/auth/nextjs on the server.
The environment variables that deploy creates are TYPEBASE_APP_URL_DEV for development and TYPEBASE_APP_URL for production. The TanStack client needs one readable from the browser: NEXT_PUBLIC_TYPEBASE_APP_URL.
Is it free and where does it deploy?
The typebase-io/monorepo repository is public and under the MIT license, and packages are installed from npm. At the time of publishing this note, neither the site nor the documentation publish a pricing page.
Deployment goes to Vercel, Cloudflare Workers, or Deno Deploy, with Postgres on Neon. You can also generate the server code and host it yourself. That is: the infrastructure is yours and you pay for it where you already pay for it, which is exactly the point Typebase makes against Supabase and Convex—“Supabase and Convex host your backend on their infrastructure. Migrating away is a significant amount of work.”
What’s still missing?
The documentation is unusually direct about this, which is a good sign. At the time of publishing this note:
- Storage and mailers don’t exist. Both are listed as “coming soon (BYO via any TS library)” on the comparison page, and “add storage support” / “add mailer support” appear as pending items on the roadmap.
- Migrations aren’t here either. The current path is
db dev push. - Realtime is more manual than in Convex. The project’s own words: “that’s more to write than Convex, and less automatic than subscribing to a table.”
- There are no broadcast channels or presence. The comparison plainly states that “Typebase has no equivalent for” those Supabase functions.
Against Convex the argument is portability: Convex “uses a proprietary database. Your data is tied to their platform, and you can’t use the SQL knowledge, tooling, or ecosystem you already have.” Typebase runs on standard Postgres that’s yours.
Typebase or Supabase?
If you’re starting a Next.js, SvelteKit, Nuxt, or Expo app and your first instinct is Supabase, this is the alternative that changes one concrete thing: where authorization lives. Everything else—Drizzle, better-auth, Zod, Postgres—are pieces you’d probably pick anyway. What Typebase adds is the wiring and the deploy.
If today you need file storage, transactional email, presence, or a migration history, it’s still not ready for you.
One last thing, because it says something about the project. The testimonials on the landing are fake, and the site admits it. The Show HN thread split in two: one commenter wrote “I love the (well-disclosed) fake testimonials. Hilarious!” and another said “fake reviews are a hard no-no imo.” The most useful comment in the thread was probably the plainest: “It’s like Next.js but for backend… But the wheel needs to be reinvented on how to do everything.” That’s the real trade-off. You gain one coherent and typed way of doing things, and you give up all the recipes you already knew.
If you’re interested in this category, we recently covered InstantDB 1.0, which attacks the same problem from the opposite angle.