# Use Supabase with DaloyJS

[Supabase](https://supabase.com) is a hosted Postgres + auth + storage platform. The official [`@supabase/supabase-js`](https://supabase.com/docs/reference/javascript) client is fetch-based, so it runs on every runtime DaloyJS supports, Node.js, Bun, Deno, and Cloudflare Workers.

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.

**Diagram: One request through Supabase**

1. **HTTP request** (client) - GET /users/:id
2. **Validated input** (zod) - params.id is a uuid
3. **PostgREST query** (supabase) - from("users").select(...).eq(...)
4. **Typed body** (response) - 200 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.ts";

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

Add the `declare module` block to the same module that creates the client, not to a separate `.d.ts` file. Declaration files are exempt from type-checking when `skipLibCheck` is on (the scaffolded default), so a broken import inside a `.d.ts` fails silently and `state.supabase` degrades to `any`.

```ts
// src/db/supabase.ts (same module as the plugin above)
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.ts";

const app = new App();
app.use(secureHeaders());
app.register(supabasePlugin);

const UserSchema = z.object({
  id: z.uuid(),
  email: z.email(),
  name: z.string().nullable(),
});

app.get(
  "/users/:id",
  {
    operationId: "getUser",
    request: { params: z.object({ id: z.uuid() }) },
    responses: {
      200: { description: "Found", body: UserSchema },
      404: { description: "Not found" },
    },
  },
  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.post(
  "/users",
  {
    operationId: "createUser",
    request: { body: z.object({ email: z.email(), name: z.string().optional() }) },
    responses: { 201: { description: "Created", body: UserSchema } },
  },
  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.ts";

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](/docs/errors):

```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](/docs/orm/prisma), [Drizzle](/docs/orm/drizzle), [Sequelize](/docs/orm/sequelize), or the [ODM overview](/docs/odm) if you are on a document database.

For other managed Postgres / MySQL hosts, [Neon](/docs/databases/neon), [PlanetScale](/docs/databases/planetscale), [Turso](/docs/databases/turso), [Cloudflare D1](/docs/databases/cloudflare-d1), and [Aurora DSQL](/docs/databases/aurora-dsql): see the [database hosting overview](/docs/databases).

---

Source: https://daloyjs.dev/docs/orm/supabase