Custom repositories
Domain methods with defineRepository and extend, and the escape hatches below them.
defineRepository
Put domain queries next to the table they belong to. defineRepository
returns a plugin that adds methods to one table only:
import { defineRepository, defineSupabase } from "better-supabase";
const base = defineSupabase(schema).use(tenant()).use(softDelete());
const customers = defineRepository(base, "customers", (repo) => ({
active: () =>
repo.findMany({ where: { status: "active" }, orderBy: { name: "asc" } }),
byKvk: (kvk: string) => repo.findFirst({ where: { kvk } }),
}));
export const betterSupabase = base.use(customers);const rows = await ctx.db.customers.active().orThrow();repo is the repository as your app sees it: every plugin (tenant filters,
soft delete, restore()) applies, and payload types flow through. The
methods are typed on db.customers and don't exist on other tables.
Each table can have one defineRepository; the plugin is named
repository:<table>, and redefining a built-in method such as findMany
throws when the repository is first used.
extend
For methods that only one module needs, extend a repository in place:
const customers = ctx.db.customers.extend((repo) => ({
withOpenInvoices: () =>
repo.findMany({ where: { invoices: { some: { status: "open" } } } }),
}));extend returns a new object; ctx.db.customers is unchanged.
Escape hatches
Each layer has a way out when the repository doesn't cover a query:
| Need | Use |
|---|---|
| A PostgREST feature the repository lacks | db.$client, the supabase-js client (database column names) |
| A database function | db.$rpc(name, args, { returns }) |
| Raw SQL under RLS | ctx.sql from /postgres |
| A different backend | an Executor passed to betterSupabase.connect() |
| Rewriting queries everywhere | a plugin with transformQuery |
Last updated on