Supabase Setup¶
Supabase serves two roles for the backoffice:
- Auth provider — email OTP and Google OAuth for
client-backoffice - PostgreSQL host — analytics data, config tables, RLS-secured access
Authentication Flow¶
Email OTP Login¶
Key Design Decisions¶
shouldCreateUser: true— supports both pre-provisioned members and new self-serve onboarding usersbefore_user_createdhook — rejects malformed signup events without an email; it is not an authorization boundarybackoffice_users+ RLS — an Auth account alone grants no client data; existing members receive their assigned domains and other users remain in onboarding- Error suppression — frontend always shows the OTP entry regardless of send success/failure, preventing email enumeration
- Code-only template — the Magic Link / OTP template contains
{{ .Token }}and no verification URL, so email security scanners cannot consume the token
Auth Email Delivery¶
Hosted Supabase Auth sends Rose authentication emails through the Cloudflare Email SMTP service configured in Supabase Dashboard → Authentication → SMTP. This replaces Supabase's built-in email service, which is capped at 2 emails/hour and is only intended for project setup/testing.
Supabase Auth remains the source of truth for generating signup, email OTP, invite, email-change, and password-reset tokens. Cloudflare is only the delivery provider: Supabase connects to the Cloudflare SMTP endpoint and sends the rendered Auth email to the user.
Required hosted settings:
- Custom SMTP: enabled, using the Cloudflare Email SMTP credentials.
- Magic Link email template: copy
supabase/templates/email_otp.htmlinto Supabase Dashboard → Authentication → Email Templates → Magic Link. Hosted projects do not read this template fromsupabase/config.toml. - Auth email rate limit:
30emails/hour in Supabase Dashboard → Authentication → Rate Limits (rate_limit_email_sent = 30via the Management API). - Secrets: SMTP password/token lives only in Supabase project settings or secret storage; never commit it to this repo.
Google OAuth¶
Google sign-in is also available. After OAuth, the link_current_user_to_backoffice RPC links auth.users.id to backoffice_users.user_id.
Access Control¶
User Provisioning Flow¶
- Admin creates entry in
backoffice_users(email, display_name, is_admin) - Admin assigns domains via
backoffice_user_domains - User visits backoffice and logs in (email OTP or Google)
- On first login, auth account is created (hook allows it) and linked to
backoffice_users - RLS policies scope all data queries to the user's assigned domains
RLS Architecture¶
| Table | Policy | Scope |
|---|---|---|
conversations |
has_domain_access(site_domain) |
User's assigned domains |
visitors |
has_domain_access(site_domain) |
User's assigned domains |
accounts |
has_domain_access(site_domain) |
User's assigned domains |
messages |
Via conversation's site_domain |
User's assigned domains |
backoffice_users |
Own record or admin | Self + admin |
backoffice_user_domains |
Own assignments or admin | Self + admin |
Admins (is_admin = true) bypass domain scoping and see all data.
before_user_created Hook¶
What It Does¶
A PostgreSQL function (public.before_user_created_hook) that runs inside Supabase's auth flow before a new account is created. It requires a non-empty email, then allows account creation. Authorization remains in backoffice_users and RLS; unprovisioned users can only reach self-serve onboarding.
Migration¶
Created by 20260226163802_add_before_user_created_hook.sql and relaxed for self-serve onboarding by 20260702102821_onboarding_self_serve.sql:
- Creates the hook function
- Restricts EXECUTE to
supabase_auth_adminonly
Production Setup¶
Required: Enable hook in Supabase dashboard
The migration deploys the function, but the hook must also be enabled in the dashboard. This keeps hosted Auth behavior aligned with the versioned self-serve signup validation. Client-data authorization does not depend on the hook.
Steps:
- Go to Supabase Dashboard → Authentication → Hooks
- Enable Before User Created
- Set type: Postgres function
- Set schema:
public - Set function:
before_user_created_hook - Save
Local Development¶
For the full seeding workflow (synthetic data, copy-from-prod, materialized view refresh) see Supabase Seeding. For per-branch isolated DBs see Supabase Preview Branches.
Seed Users¶
supabase/seed.sql creates two users after supabase db reset or just seed-branch:
| Password | Role | |
|---|---|---|
admin@admin.com |
admin |
Admin (all domains) |
user@user.com |
user |
Regular user (sees domains granted via backoffice_user_domains) |
Email Testing¶
Local emails are captured by Inbucket (not actually sent): http://localhost:54324
Local Docker Supabase does not use the hosted Cloudflare SMTP service unless
[auth.email.smtp] is explicitly configured in supabase/config.toml. Keep
local development on Inbucket for normal testing; use the hosted project or a
preview branch when you need to verify Cloudflare SMTP delivery end-to-end.
Hook Configuration¶
The hook is enabled locally via supabase/config.toml:
[auth.hook.before_user_created]
enabled = true
uri = "pg-functions://postgres/public/before_user_created_hook"
This matches production behavior. To test the hook locally:
- Try with an email not in
backoffice_users→ OTP appears in Inbucket and verification routes to onboarding - Try with
admin@admin.com→ OTP appears in Inbucket and verification opens the provisioned backoffice