Use Supabase with DaloyJS
Supabase is a hosted Postgres + auth + storage platform. The official @supabase/supabase-js client is fetch-based, so it runs on every runtime DaloyJS supports — Node.js, Bun, Deno, Cloudflare Workers, and Vercel Edge.
Treat Supabase as a platform client, not a traditional ORM: you are composing PostgREST, auth, storage, and realtime APIs rather than mapping tables through model classes.
1. Install
pnpm add @supabase/supabase-js2. Generate database types
Use the Supabase CLI to generate a fully typed schema:
pnpm dlx supabase login
pnpm dlx supabase gen types typescript --project-id <your-ref> --schema public > src/db/supabase.types.ts3. Create a Supabase plugin
Create a long-lived service-role client for server-to-server calls. For per-request, user-scoped clients (RLS), instantiate inside the handler with the caller's JWT.
// src/db/supabase.ts
import { createClient, SupabaseClient } from "@supabase/supabase-js";
import type { App } from "@daloyjs/core";
import type { Database } from "./supabase.types";
export type Db = SupabaseClient<Database>;
export const serviceClient: Db = createClient<Database>(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
{ auth: { persistSession: false, autoRefreshToken: false } },
);
export const supabasePlugin = {
name: "supabase",
register(app: App) {
app.decorate("supabase", serviceClient);
},
};4. Augment app state types
// src/types/state.d.ts
import type { Db } from "../db/supabase";
declare module "@daloyjs/core" {
interface AppState {
supabase: Db;
}
}5. Use it in routes
// src/server.ts
import { z } from "zod";
import { App, secureHeaders } from "@daloyjs/core";
import { serve } from "@daloyjs/core/node";
import { supabasePlugin } from "./db/supabase";
const app = new App();
app.use(secureHeaders());
app.register(supabasePlugin);
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string().nullable(),
});
app.route({
method: "GET",
path: "/users/:id",
operationId: "getUser",
request: { params: z.object({ id: z.string().uuid() }) },
responses: {
200: { description: "Found", body: UserSchema },
404: { description: "Not found" },
},
handler: async ({ params, state }) => {
const { data, error } = await state.supabase
.from("users")
.select("id,email,name")
.eq("id", params.id)
.maybeSingle();
if (error) throw error;
return data
? { status: 200, body: data }
: { status: 404, body: { type: "about:blank", title: "Not found", status: 404 } };
},
});
app.route({
method: "POST",
path: "/users",
operationId: "createUser",
request: { body: z.object({ email: z.string().email(), name: z.string().optional() }) },
responses: { 201: { description: "Created", body: UserSchema } },
handler: async ({ body, state }) => {
const { data, error } = await state.supabase
.from("users")
.insert(body)
.select("id,email,name")
.single();
if (error) throw error;
return { status: 201, body: data };
},
});
await app.ready();
serve(app, { port: 3000 });Per-request, RLS-aware clients
For row-level security, derive a client from the caller's bearer token in a hook so each handler gets a Supabase client scoped to that user.
import { createClient } from "@supabase/supabase-js";
import type { Database } from "./db/supabase.types";
app.use({
beforeHandle({ headers, state }) {
const auth = headers["authorization"];
state.supabaseUser = createClient<Database>(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!,
{ global: { headers: { Authorization: auth ?? "" } } },
);
},
});Auth: validating Supabase JWTs
import { HttpError } from "@daloyjs/core";
app.use({
async beforeHandle({ headers, state }) {
const token = headers["authorization"]?.replace(/^Bearer\s+/i, "");
if (!token) throw new HttpError(401, { title: "Missing token" });
const { data, error } = await state.supabase.auth.getUser(token);
if (error || !data.user) throw new HttpError(401, { title: "Invalid token" });
state.user = data.user;
},
});Realtime, storage, and edge functions
The same supabase client exposes storage, functions, and realtime. Use them inside handlers exactly the same way — DaloyJS doesn't care.
Mapping Supabase errors
Translate PostgrestError codes into typed framework errors so they serialize as problem+json:
import { HttpError } from "@daloyjs/core";
if (error?.code === "23505") {
throw new HttpError(409, { title: "Resource already exists" });
}
if (error) throw new HttpError(500, { title: error.message });Compare with Prisma, Drizzle, Sequelize, or the ODM overview if you are on a document database.
For other managed Postgres / MySQL hosts — Neon, PlanetScale, Turso, Cloudflare D1, and Aurora DSQL — see the database hosting overview.