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
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¶
Seed Users¶
supabase/seed.sql creates two users after supabase db reset:
| Password | Role | |
|---|---|---|
admin@admin.com |
admin |
Admin (all domains) |
user@user.com |
user |
Regular user |
Email Testing¶
Local emails are captured by Inbucket (not actually sent): http://localhost:54324
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