supersam/docs/operations/supabase-email-otp-auth.md

2.8 KiB

Supabase Email OTP Auth

This guide covers the whitelist-only email OTP login flow for the app.

What changes in this flow

  • Users sign in with a one-time code sent to email.
  • The app only accepts accounts that already exist in auth.users.
  • The app reads the app role from public.users -> public.roles.
  • Self-signup should be disabled if you want whitelist-only access.

Supabase dashboard settings

  1. Open your Supabase project.
  2. Go to Authentication and confirm email OTP / email login is enabled.
  3. Check the email provider settings so OTP messages are deliverable.
  4. Disable self-signup if the project should stay whitelist-only.
  5. Make sure the two allowed emails exist in Supabase Auth before testing login.

Frontend environment

The frontend only needs these variables in .env.local:

VITE_SUPABASE_URL=...
VITE_SUPABASE_ANON_KEY=...

Do not put SUPABASE_SERVICE_ROLE_KEY in the frontend. If an Edge Function needs secrets, add them in the Supabase dashboard or via supabase secrets.

Seed the allowed users

Create these Auth users first:

  • skylanguage@yandex.ru
  • mk7029953@yandex.ru

The public.handle_new_user() trigger will create the matching row in public.users when a new auth.users row is inserted. It already reads raw_user_meta_data ->> 'role', so if you set metadata during user creation, the role can be applied automatically.

If the user already exists in auth.users, bind the role in public.users with SQL:

update public.users u
set role_id = r.id
from public.roles r
where u.email = 'skylanguage@yandex.ru'
  and r.name = 'admin';

update public.users u
set role_id = r.id
from public.roles r
where u.email = 'mk7029953@yandex.ru'
  and r.name = 'logistician';

If a user exists in Auth but the profile row is missing in public.users, recreate it manually:

insert into public.users (id, email, name, role_id, last_login)
select
  au.id,
  au.email,
  coalesce(au.raw_user_meta_data ->> 'name', split_part(au.email, '@', 1)),
  r.id,
  timezone('utc', now())
from auth.users au
join public.roles r on r.name = case
  when au.email = 'skylanguage@yandex.ru' then 'admin'
  when au.email = 'mk7029953@yandex.ru' then 'logistician'
  else 'manager'
end
where au.email in ('skylanguage@yandex.ru', 'mk7029953@yandex.ru')
on conflict (id) do update
set email = excluded.email,
    name = excluded.name,
    role_id = excluded.role_id,
    last_login = excluded.last_login;

Sanity checks

  • Send an OTP to skylanguage@yandex.ru.
  • Send an OTP to mk7029953@yandex.ru.
  • Confirm admin reaches the admin area after sign-in.
  • Confirm logistician reaches the logistics area after sign-in.
  • Confirm an email outside the whitelist is rejected.
  • Confirm the app shows an error if Auth has a session but public.users has no matching profile row.