Supabase Setup¶
Supabase serves two roles for the backoffice:
- Auth provider — magic link and Google OAuth for
client-backoffice - PostgreSQL host — analytics data, config tables, RLS-secured access
Authentication Flow¶
Magic Link Login¶
Key Design Decisions¶
shouldCreateUser: true— required so pre-provisioned users (added tobackoffice_usersbefore their first login) can get their first magic linkbefore_user_createdhook — gates account creation to emails inbackoffice_usersonly, preventing transactional email waste- Error suppression — frontend always shows "check your inbox" regardless of success/failure, preventing email enumeration
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, magic-link, 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.
- 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 (magic link 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 any new account is created. It checks if the email exists in backoffice_users with is_active = TRUE. If not, it returns a 403 and no email is sent.
Migration¶
Created by 20260226163802_add_before_user_created_hook.sql:
- Grants
supabase_auth_adminSELECT onbackoffice_users - 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.
Without this, shouldCreateUser: true allows any email to create an account and receive a magic link.
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
Stale User Cleanup¶
If the hook was not enabled while shouldCreateUser: true was deployed, unauthorized accounts may exist in auth.users. These bypass the hook on future logins (it only fires on user creation).
Preview stale users:
SELECT au.id, au.email, au.created_at
FROM auth.users au
WHERE NOT EXISTS (
SELECT 1 FROM public.backoffice_users bu
WHERE bu.email = au.email
AND bu.is_active = TRUE
);
Delete after review:
DELETE FROM auth.users au
WHERE NOT EXISTS (
SELECT 1 FROM public.backoffice_users bu
WHERE bu.email = au.email
AND bu.is_active = TRUE
);
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 logging in with an email NOT in
backoffice_users→ no email in Inbucket - Try with
admin@admin.com→ magic link appears in Inbucket