Search docs

Jump between documentation pages.

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

ts
pnpm add @supabase/supabase-js

2. Generate database types

Use the Supabase CLI to generate a fully typed schema:

ts
pnpm dlx supabase login
pnpm dlx supabase gen types typescript --project-id <your-ref> --schema public > src/db/supabase.types.ts

3. 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.

ts
// 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

ts
// src/types/state.d.ts
import type { Db } from "../db/supabase";

declare module "@daloyjs/core" {
  interface AppState {
    supabase: Db;
  }
}

5. Use it in routes

ts
// 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.

ts
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

ts
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:

ts
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.