Account sync is available only in hosted builds. Guest and self-hosted builds continue to use the existing newtab-config localStorage entry and do not require Supabase.
- Create or link the production Supabase project.
- Apply the migrations in
supabase/migrations/with the Supabase CLI or the SQL editor. - In Authentication → Hooks, enable the Before User Created Postgres hook and select
public.hook_restrict_sync_beta_signup. - Keep email signup enabled. The hook rejects new users whose normalized email is not in
public.sync_beta_allowlist; existing users can continue signing in. - Confirm that anonymous sign-ins and password login are not exposed by the app.
Never expose a secret or service-role key to Vite. The browser needs only the project URL and publishable key; row-level security protects config rows.
The desired hook is also declared in supabase/config.toml. The database-only CI workflow cannot apply hosted Auth service configuration: supabase config push uses the Supabase Management API and requires account-level authentication. The dashboard step is therefore a one-time production bootstrap while CI remains restricted to the project database URL.
- Verify
thenewtab.appin Resend and publish the requested SPF and DKIM DNS records. - Configure Supabase custom SMTP with a sender such as
newtab <login@thenewtab.app>. - Replace the Supabase magic-link email body with
supabase/templates/login-code.html, which displays{{ .Token }}. - Keep the OTP length at six digits and configure the desired expiry and rate limits in Supabase Auth.
- Send test codes to at least two email providers before enabling the production build.
Normalize beta emails to lowercase before inserting them:
insert into public.sync_beta_allowlist (email)
values ('person@example.com');Removing an allowlist row does not revoke an existing account. To revoke access, ban or delete the user from Authentication → Users.
Super admins see an Invite users button on the home screen. It adds emails to public.sync_beta_allowlist through the invite_sync_beta_user RPC and lists existing invites. No email is sent; tell the invitee they can sign in. Removing an invite still requires SQL.
The flag lives in public.user_profiles and can only be set with SQL. The user must have signed in at least once:
insert into public.user_profiles (user_id, is_super_admin)
select id, true from auth.users where email = 'admin@example.com'
on conflict (user_id) do update set is_super_admin = true;To remove the flag, set is_super_admin = false for that row. The invite RPCs check the flag on every call, so changes take effect immediately.
Set these build-time variables in the hosted deployment:
VITE_HOSTED=true
VITE_SUPABASE_URL=https://PROJECT.supabase.co
VITE_SUPABASE_PUBLISHABLE_KEY=sb_publishable_...
If either Supabase value is absent, account UI and network access remain disabled. This is also the rollback path; guest configs are unaffected.
For the repository Docker image, pass the same values as build arguments:
docker build -f docker/Dockerfile \
--build-arg VITE_HOSTED=true \
--build-arg VITE_SUPABASE_URL=https://PROJECT.supabase.co \
--build-arg VITE_SUPABASE_PUBLISHABLE_KEY=sb_publishable_... \
-t newtab .The Release Please workflow applies unapplied migrations before deploying the hosted Cloudflare build. It performs a dry run followed by supabase db push; Cloudflare deployment does not start if either step fails. Docker publishing remains independent because the default Docker image is local-only.
Add these repository secrets under GitHub → repository Settings → Secrets and variables → Actions → Secrets → New repository secret:
| Secret | Value |
|---|---|
SUPABASE_DB_URL |
The production project's Session pooler connection string from Supabase project → Connect, with [YOUR-PASSWORD] replaced by the URL-encoded database password. |
Add these repository variables under GitHub → repository Settings → Secrets and variables → Actions → Variables → New repository variable:
| Variable | Value |
|---|---|
VITE_SUPABASE_URL |
The project URL, such as https://abcdefgh.supabase.co. |
VITE_SUPABASE_PUBLISHABLE_KEY |
The project's publishable browser key from Project Settings → API Keys. This value is intentionally public and protected by RLS. |
Do not use the Supabase secret/service-role key for VITE_SUPABASE_PUBLISHABLE_KEY. Vite embeds VITE_ values into the public browser bundle.
The migration workflow intentionally uses a project-specific database URL instead of a Supabase personal access token. The URL grants PostgreSQL administration access to this one project but does not grant Supabase account-management access or access to other projects. Treat it as a sensitive production secret.
Use the Session pooler string because GitHub-hosted runners support IPv4. It normally resembles:
postgresql://postgres.PROJECT_REF:URL_ENCODED_PASSWORD@REGION.pooler.supabase.com:5432/postgres
If the database password contains reserved URL characters such as @, :, /, ?, #, or %, percent-encode the password portion before saving the URL. Keep schema changes in supabase/migrations/; direct production changes through the SQL or Table Editor can cause migration history drift.
For the initial setup, apply the schema from a trusted local shell before releasing the account UI:
- Export the production Session pooler connection string as
SUPABASE_DB_URLwithout committing it to a file. - Run
npx --yes supabase@2.113.0 db push --db-url "$SUPABASE_DB_URL" --dry-runand review the pending migration. - Run
npx --yes supabase@2.113.0 db push --db-url "$SUPABASE_DB_URL" --yesto apply it. - Unset
SUPABASE_DB_URLwhen finished. - Add beta emails to
public.sync_beta_allowlistand enable the auth hook before releasing the hosted app.
Route support@thenewtab.app to a monitored mailbox. For a verified deletion request:
- Remove the email from
public.sync_beta_allowlist. - Delete the user from Authentication → Users.
- Confirm the matching
public.user_configsrow was removed by the foreign-key cascade.
The service stores the account email in Supabase Auth, the allowlisted email in the private beta table, and the complete config JSON in public.user_configs. It does not provide end-to-end encryption.
- Confirm an unlisted email is rejected and an allowlisted email receives a six-digit code.
- Sign in from two browsers and verify saves appear after refocusing the other browser.
- Disconnect one browser, save locally, reconnect, and retry sync.
- Sign out and confirm the browser's original guest config returns.
- Use two test users to confirm neither can select or update the other's
user_configsrow. - Confirm a non-admin does not see Invite users, and that calling
invite_sync_beta_userdirectly returns a permission error.