Skip to content

Next.js and Supabase, where the data layer is the product.

A monorepo where the app and the background jobs are just consumers of a data layer. Every query and every authorization check lives there, with Postgres and deny-by-default RLS as a backstop.

Open on GitHubMIT licence · free to use

Use it when

You're starting a Postgres SaaS that needs background jobs, and you want authorization in server code where it can be reviewed — not scattered across SQL policies.

On MongoDB instead of Postgres? template-next-mongo instead →

What's included

Done from the start.

  • The data layer

    @ketryon/db holds both Supabase clients, every DTO and every query. Every function takes a session, never a user id.

  • RLS as a second lock

    Deny-by-default on every table, with pgTAP tests. Reads go under RLS, writes through the data layer.

  • Background jobs

    Trigger.dev tasks on named queues with concurrency limits, and every run saved to a job_run table.

  • An example to build from

    An orders feature with a page, server actions, an API route and token-refreshing middleware.

  • Lint as a guard

    ESLint stops the app from importing the Supabase client or the jobs' privileged entry point.

  • Types from the database

    pnpm db:types generates the types from the schema after every migration.

The principles

The rules it keeps.

  1. 1

    The index file exports no client. A new query goes into the data layer, where it gets reviewed.

  2. 2

    Every data-layer function takes a session, never a user id.

  3. 3

    getUser(), never getSession(), for anything security-relevant.

  4. 4

    Middleware is for redirects, not authorization — assume it can be bypassed.

  5. 5

    Keyset pagination with a cap. No OFFSET.

  6. 6

    Every job saves its outcome, because a vendor dashboard only remembers a few weeks.

Get started

pnpm install
cp .env.example apps/web/.env.local
cp .env.example packages/jobs/.env
pnpm db:start     # local Supabase (needs Docker)
pnpm db:reset     # migrations + seed
pnpm db:types
pnpm dev
pnpm jobs:dev     # separate terminal

Structure

supabase/          migrations, seed, pgTAP RLS tests
packages/db/       @ketryon/db  — Supabase clients, DTOs, every query
packages/jobs/     @ketryon/jobs — Trigger.dev tasks
apps/web/          Next.js 16 — calls the DAL, owns no database code

Environment variables

  • NEXT_PUBLIC_SUPABASE_URL
  • NEXT_PUBLIC_SUPABASE_ANON_KEY
  • SUPABASE_SERVICE_ROLE_KEY
  • TRIGGER_PROJECT_REF
  • TRIGGER_SECRET_KEY

Copy .env.example into the app and into the jobs package. The service-role key is server-only.

Good to know

  • The SQL — migrations, seed and the pgTAP tests — hasn't been run against a real database here. Run pnpm db:start, db:reset, db:test and db:lint before trusting it.
  • Type-check and lint pass with Next 16.4, React 19.3 and Zod 4.6.

Built with

  • Next.js 16
  • React 19
  • Supabase
  • Postgres
  • Trigger.dev
  • Zod 4
  • Tailwind CSS 4
  • pnpm workspaces

Let's build
the next thing.

You talk to me, not a salesperson. The first call is free.

— Kenny

psst, it takes 15 min

Write, and I reply the same working day.