Supabase
Bookygram uses Supabase for hosted PostgreSQL, Supabase Auth, and Realtime Broadcast. Storage, Edge Functions, Postgres Changes, and the Data API stay off. This page explains where each part sits and the boundaries that keep it that way.
What Supabase provides
| Capability | Used for | Used by |
|---|---|---|
| Postgres | System of record for users, profiles, media, and the scaffolded booking schema | API, through pg |
| Auth | Email-and-password sign-in and access tokens | Website (session), API (verification) |
| Migrations | Ordered SQL history in supabase/migrations/ | Supabase CLI |
| Realtime Broadcast | Best-effort event notifications on private channels | API (publish), website (one user's channel) |
| Data API (PostgREST) | Not used; stays disabled | Nobody |
Postgres
The API is the only application process that queries the database. It connects with the pg driver using DATABASE_URL and does not use @supabase/supabase-js or PostgREST for data. See Database connection for the pool lifecycle, Core user and media schema for identity and media tables, and Booking persistence schema for booking state. Booking API flows are not wired to that schema yet.
Schema changes are SQL files under supabase/migrations/, applied with the Supabase CLI (the supabase package in apps/api). The CLI uses DIRECT_URL when it needs a non-pooler connection; the API never reads it. See Manage database migrations.
Auth
Supabase Auth identifies the person. The application owns everything else.
- The website signs a visitor in with email and password through
@supabase/supabase-js, using the public project URL and anon key. - The browser sends the session's access token to the API as
Authorization: Bearer <token>. - The API verifies the token locally with
joseand hands the verified user id to theusersslice, which maps it to an application user UUID.
How the API verifies tokens
The API derives everything from SUPABASE_URL:
- the issuer is
<SUPABASE_URL>/auth/v1; - the signing keys are fetched from
<SUPABASE_URL>/auth/v1/.well-known/jwks.jsonand cached byjose.
A token is accepted only when all of these hold:
- the signature matches a published key (
ES256orRS256); - the issuer is the configured project;
- the token has not expired;
- the audience and the
roleclaim are bothauthenticated; - the
subclaim is a non-empty string.
Anything else, including a missing header, a service-role token, an anonymous token, or a token from another issuer, returns 401 Unauthorized without detail. Token verification needs no anon key or service role key, and it does not call Supabase on each request. Separately, the process calls the Realtime publish endpoint before it listens. For identity, see Authentication. For notifications, see Realtime event channels.
What the website may do
The website's Supabase client is used for sessions only. It persists and refreshes the session, and does not read tokens from the URL. A guard wraps its fetch and rejects any request to /rest/v1, so application data cannot be read or written through the Data API. If the public URL or anon key is missing, sign-in fails with an "auth is unavailable" error instead of failing at startup. See Sign in on the website.
Identity stays provider-independent
The Supabase user id is stored as an opaque identity_provider_id on the application user. The user's own UUID is what other tables reference, so replacing the auth provider means remapping that one column. Live events are addressed by that application UUID, not by the Supabase Auth user id.
Realtime
Supabase Realtime Broadcast carries recorded event notifications. The API publishes them with the server secret. No feature command publishes an event yet. The website can subscribe only to the signed-in application user's private channel, using a short-lived token signed by the project's current ES256 key. Postgres Changes are not used, and browsers do not read business tables through Realtime. See Realtime event channels and Live user events.
Configuration
| Variable | Process | Visibility | Purpose |
|---|---|---|---|
DATABASE_URL | API | Private | Runtime Postgres connection |
DIRECT_URL | Supabase CLI | Private | Direct connection for migrations |
SUPABASE_URL | API | Private | Project origin for token verification |
NUXT_PUBLIC_SUPABASE_URL | Website | Public | Project origin for sign-in |
NUXT_PUBLIC_SUPABASE_ANON_KEY | Website | Public | Anon (publishable) key for the browser auth client |
REALTIME_PUBLISH_URL | API | Private | Realtime broadcast endpoint |
REALTIME_SECRET_KEY | API | Private | Server credential used to publish broadcasts |
REALTIME_JWT_SECRET | API | Private | Private half of the project's current ES256 signing key |
NUXT_PUBLIC_REALTIME_URL | Website | Public | Realtime websocket URL, with no secrets |
SUPABASE_URL must be an absolute origin with no credentials, path, query, or trailing slash. Production requires HTTPS. Never place the service role key, the publish secret, or the signing key in any web variable. See Environment variables for the full reference.
Boundaries
- Browsers and Telegram apps never query Postgres or the Data API.
- Authorization is an application decision: a valid token proves identity, and the API's use cases decide what that user may do.
- Realtime is limited to Broadcast on the private channels described above. Storage and Edge Functions are not part of the current system. Objects live in S3 and durable background work runs on SQS.