Search docs

Jump between documentation pages.

Browse docs

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.

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.

One request through Supabase
  1. 01clientHTTP requestGET /users/:id
  2. 02zodValidated inputparams.id is a uuid
  3. 03supabasePostgREST queryfrom("users").select(...).eq(...)
  4. 04responseTyped body200 UserSchema | 404
Zod validates the request, the handler calls the fetch-based PostgREST client off state.supabase, the destructured error is mapped to problem+json, then the response schema checks the body. The same client runs on Node.js and every edge runtime.

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.