Typebase: o backend como uma pasta de TypeScript dentro da sua aplicação

Typebase extrai a autorização do seu backend das políticas RLS do Postgres e a move para middleware de TypeScript que o compilador de fato verifica. O schema, as actions e a autenticação vivem como arquivos .ts em uma pasta typebase/ dentro da sua app atual — sem repositório separado, sem dashboard — e o frontend as chama como se fossem funções locais, tipadas de ponta a ponta.

A landing resume em uma linha: “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.” Apareceu como Show HN em 26 de agosto de 2026 e juntou algumas dezenas de votos e uma thread de comentários que vale a pena ler; voltamos a isso no final.

O que é Typebase?

É um framework de backend que vive dentro do seu projeto de frontend. Você adiciona uma pasta typebase/, escreve lá seu schema do Postgres, suas server actions e sua configuração de auth como arquivos TypeScript, e um CLI gera um cliente tipado e faz o deploy do servidor.

A estrutura, tal como aparece no guia de início:

typebase/
├── _generated/          (auto-generated, do not edit)
├── db/
│   ├── schema.ts
│   └── relations.ts
├── actions/
│   ├── queries/
│   │   └── todos.ts
│   └── mutations/
│       └── todos.ts
├── typebase.json
└── env.ts

Debaixo não há uma stack nova: o schema é Drizzle ORM, a autenticação é um envoltório fino sobre better-auth, a validação de entrada aceita qualquer validador compatível com Standard Schema (Zod, Valibot, ArkType) e o banco de dados é Postgres padrão. O realtime funciona via Server-Sent Events. Os frontends suportados são Next.js, SvelteKit, Nuxt e Expo.

O que é RLS no Supabase e por que Typebase quer substituí-lo?

RLS — Row Level Security — é o mecanismo do Postgres que decide, linha por linha, quem pode ler ou escrever o quê. No Supabase é o lugar onde vive boa parte da lógica de autorização: você escreve políticas em SQL e o banco de dados as aplica em cada consulta.

O argumento de Typebase é que esse SQL fica fora do alcance de suas ferramentas. Sua página de comparação diz sobre Supabase que “most logic lives in the database as SQL or RLS policies” e que Supabase “doesn’t have a strong concept of typed server functions”. Sobre os tipos gerados, sustenta que “with Supabase, types are generated from your database schema as a separate step. They can drift out of sync”.

A versão mais contundente da crítica é sobre agentes de código: com 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”.

Convém ser claro sobre o que é isso: é o enquadramento que um provedor faz de seu concorrente, não uma avaliação independente. Mas a observação de fundo é uma que muita gente já enfrentou. Uma política RLS é uma cadeia de SQL que seu editor não faz type check, que sua suite de testes quase nunca executa e sobre a qual seu agente não pode raciocinar como raciocina sobre uma função. A resposta de Typebase é converter a autorização em uma função.

Como se instala Typebase?

Dois comandos para adicioná-lo, um para montar o andaime:

npm install typebase-io
npm install -D typebase-io-cli
npx typebase-io-cli init

init aceita flags — --with-auth, --with-db-publisher e --skip-example — assim você pode gerar o arquivo de auth e o publisher de eventos desde o início em vez de adicioná-los depois.

Depois você vai usar estes três comandos o tempo todo:

npx typebase-io-cli codegen        # regenera o cliente tipado
npx typebase-io-cli db dev push    # aplica mudanças de schema ao banco
npx typebase-io-cli deploy dev     # faz deploy do servidor

Como se define o schema?

Em typebase/db/schema.ts, ao estilo Drizzle, através do export p de Typebase:

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(),
});

Depois npx typebase-io-cli db dev push o aplica. Observe a palavra push: as migrações propriamente ditas estão no roadmap, não entregues. No momento de publicar esta nota, o roadmap lista “allow users to use migrations” como pendente. Se você vai rodar algo com dados reais dentro, esse é o ponto a acompanhar.

Como se escreve uma action?

Uma action é uma cadeia: .input(), .output(), .handler(). Queries e mutations são a mesma primitiva — a documentação é explícita: “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 };
  });

O contexto do handler se monta de acordo com o que exista na sua pasta: db (cliente Drizzle tipado) se existe db/schema.ts, auth se existe auth.ts, env se você configurou variáveis de ambiente, publisher se você tem um publisher, e reqHeaders sempre. Os erros são ServerError com um nome de código de status — NOT_FOUND, UNAUTHORIZED, FORBIDDEN, BAD_REQUEST e outros — cada um mapeado para um status HTTP.

Também existe .stream() no lugar de .handler() quando você quer uma resposta por SSE.

Como funciona a autorização sem RLS?

Com middleware. .use() recebe o contexto e devolve valores que se mesclam no contexto do handler, completamente tipados. O padrão de action protegida que a documentação traz é este:

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,
  };
});

Aí está todo o argumento, em dez linhas. authedAction é um valor sobre o qual você constrói as demais actions, user chega ao handler tipado, e se você renomear um campo do objeto de usuário o compilador te diz cada lugar que quebra. O equivalente em RLS é uma expressão de política dentro do banco que nenhuma parte de sua toolchain lê.

A autenticação em si é um único arquivo, typebase/auth.ts:

import { defineAuth } from 'typebase-io/server';

export const auth = defineAuth({
  trustedOrigins: ['http://localhost:3000'],
  emailAndPassword: {
    enabled: true,
  },
});

A documentação descreve defineAuth como “a thin wrapper around better-auth”: aceita as opções de better-auth exceto database, que Typebase gerencia. Assim socialProviders e o resto da superfície de better-auth estão disponíveis aqui.

Como o frontend chama as actions no Next.js?

Você cria um cliente a partir do tipo Router gerado. Versão simples:

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 || '',
});

Ou a versão com TanStack Query, se você quer cache e hooks em 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 || '',
});

E depois, no Next.js:

// Server Component
const todos = await client.queries.todos.getMany();

// Server Action
await client.mutations.todos.create({ value: value.trim() });

// Client Component, com TanStack Query
const { data: todos } = useQuery(client.queries.todos.getMany.queryOptions());

A autenticação do lado do cliente vem do mesmo pacote:

import { createAuthClient } from 'typebase-io/client/auth/react';
export const authClient = createAuthClient();

authClient.useSession() em client components; getServerSession a partir de typebase-io/client/auth/nextjs no servidor.

As variáveis de ambiente que deploy cria são TYPEBASE_APP_URL_DEV para desenvolvimento e TYPEBASE_APP_URL para produção. O cliente TanStack precisa de uma legível a partir do navegador: NEXT_PUBLIC_TYPEBASE_APP_URL.

É grátis e onde se faz o deploy?

O repositório typebase-io/monorepo é público e está sob licença MIT, e os pacotes se instalam do npm. No momento da publicação desta nota, nem o site nem a documentação publicam uma página de preços.

O deploy vai para Vercel, Cloudflare Workers ou Deno Deploy, com Postgres no Neon. Você também pode gerar o código do servidor e hospedá-lo por conta própria. Ou seja: a infraestrutura é sua e você a paga onde já paga, que é exatamente o ponto que Typebase marca contra Supabase e Convex — “Supabase and Convex host your backend on their infrastructure. Migrating away is a significant amount of work.”

O que ainda falta?

A documentação é inusitadamente direta sobre isso, o que é um bom sinal. No momento da publicação desta nota:

  • Storage e mailers não existem. Ambos figuram como “coming soon (BYO via any TS library)” na página de comparação, e “add storage support” / “add mailer support” aparecem como pendentes no roadmap.
  • As migrações também não estão. O caminho atual é db dev push.
  • O realtime é mais manual que no Convex. Palavras do próprio projeto: “that’s more to write than Convex, and less automatic than subscribing to a table.”
  • Não há broadcast channels nem presence. A comparação diz sem volta que “Typebase has no equivalent for” essas funcionalidades do Supabase.

Contra Convex o argumento é a portabilidade: 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 roda em Postgres padrão que é seu.

Typebase ou Supabase?

Se você está começando um app com Next.js, SvelteKit, Nuxt ou Expo e seu reflexo é Supabase, esta é a alternativa que muda uma coisa concreta: onde vive a autorização. Todo o resto — Drizzle, better-auth, Zod, Postgres — são peças que você provavelmente ia escolher de qualquer forma. O que Typebase agrega é a fiação e o deploy.

Se agora você precisa de armazenamento de ficheiros, email transacional, presence ou um histórico de migrações, ainda não está pronto para você.

Uma última coisa, porque diz algo do projeto. Os depoimentos da landing são falsos, e o site o admite. A thread de Show HN se dividiu ao meio: um comentarista escreveu “I love the (well-disclosed) fake testimonials. Hilarious!” e outro que “fake reviews are a hard no-no imo.” O comentário mais útil da thread foi provavelmente o mais simples: “It’s like Next.js but for backend… But the wheel needs to be reinvented on how to do everything.” Esse é o trade-off real. Você ganha uma forma única e tipada de fazer as coisas, e abre mão de todas as receitas que você já conhecia.

Se lhe interessa essa categoria, recentemente cobrimos InstantDB 1.0, que ataca o mesmo problema do ângulo oposto.