| ← Back to API Documentation | Main Documentation |
Authentication Flows
Created: Original date unknown
Updated: Tue 29 Jul 2025 07:36:15 CEST - Renamed from authentication-flows.md and added Jekyll front matter
Updated: Thu 11 Jun 2026 17:06:39 CEST - Password recovery flow and Supabase redirect allowlist documented
Updated: Thu 11 Jun 2026 21:37:14 CEST - Sign-up e-mail verification now redirects to window.location.origin; allowlist requirements extended
Updated: Thu 11 Jun 2026 21:45:34 CEST - Custom auth e-mail webhook documented; orphaned send-password-reset edge function removed; send-verification-email restricted to webhook payloads
Updated: Mon 15 Jun 2026 - Reconciled auth e-mail delivery + sign-up verification sections from local edits
Updated: Mon 06 Jul 2026 22:36:36 CEST - Auth e-mails now render the 6-digit OTP code alongside the action link (iOS verifyOTP path)
Updated: Tue 07 Jul 2026 08:45 CEST - Auth e-mails rebranded to the design system (shared layout, hosted logo, no red); Resend API errors now surface as hook failures
Document Version: 1.6 - Brand layout + Resend error propagation; OTP code added to auth e-mails; delivery section documents both verification paths (send-verification-email webhook); legacy send-password-reset function deleted
Security Classification: Internal Technical Documentation
Target Audience: Backend Developers, Frontend Developers, Security Engineers
Author: Paul Wisén
Plings relies on Supabase Auth and JWTs to secure the GraphQL API. The key points below are distilled from architecture/api-security-guidelines.md.
Login & Token Issuance
- User authenticates via email/password, magic-link or OAuth provider managed by Supabase.
- Supabase returns an access token (JWT) and a refresh token.
- The frontend stores tokens in memory (or secure storage on mobile) and attaches
Authorization: Bearer <jwt>to every HTTP or WebSocket request.
Custom Claims Enrichment
An Auth webhook injects additional, namespaced claims so that Row-Level Security (RLS) can evaluate them efficiently:
{
"role": "org_member", // guest | org_member | manufacturer_issuer | system_owner
"org_id": "acme-123", // current tenant context (UUID)
"org_role": "admin" // member | admin | owner within the tenant
}
GraphQL Context Resolution
On each request the backend:
async def get_context(request):
token = request.headers.get("Authorization", "").split("Bearer ")[-1]
user = resolve_user_from_token(token) # Supabase helper
return { "request": request, "user": user }
The user object is then available in every resolver for fine-grained checks.
Token Refresh
The client automatically uses Supabase’s refresh endpoint to rotate tokens before expiry. WebSocket connections include the latest token via the connectionParams payload and reconnect when refreshed.
Logout
- Frontend clears tokens from memory / storage.
- Active WebSocket connections are closed.
Password Recovery Flow (Plings-Web)
- User requests a reset (AuthModal → “Forgot password?”). The app calls
supabase.auth.resetPasswordForEmail(email, { redirectTo: window.location.origin + '/reset-password' }). - The e-mail link signs the user in with a recovery session and redirects to
/reset-password, where a new password is set viasupabase.auth.updateUser({ password }). On success the user stays signed in and is taken to/dashboard. - Safety net:
AuthContextlistens for thePASSWORD_RECOVERYauth event and forces navigation to/reset-password, even if Supabase falls back to the Site URL. - Signed-in users can change their password from the profile menu (“Change password”).
No current password is required (Supabase
updateUsersemantics).
Auth E-mail Delivery (Custom Branded E-mails)
All Supabase Auth e-mails (sign-up verification, password recovery, etc.) are delivered
through a single edge function, supabase/functions/send-verification-email, wired up as
the Auth send-email hook (hook_send_email_url in supabase/config.toml). The hook
posts the user and an email_data object carrying both a token_hash and the plain
6-digit OTP token; the function builds the
/auth/v1/verify?token=...&type=...&redirect_to=... action link from token_hash,
picks a branded template by email_action_type (signup, recovery, or generic), and
sends via Resend.
Every template renders both verification paths, because the two clients verify differently:
- Web clicks the action link (
token_hash→ GET/auth/v1/verify). - iOS asks the user to type the 6-digit code and calls
verifyOTP(email:token:type:)— so the e-mail must renderemail_data.token. (Regression note, July 2026: the templates originally rendered only the link, which made app signup/recovery impossible — any typed code failed with “Fel eller utgången kod”. The OTP block is load-bearing; do not remove it.)
Notes:
- The function runs with
verify_jwt = false(the Auth hook does not send a user JWT). It therefore accepts only the webhook payload shape (user+email_data) and rejects anything else with400— caller-supplied recipient/URL would otherwise make it an open mail relay. - A legacy
send-password-resetedge function (orphaned, never invoked by the frontend, and broken: it triggered Supabase’s own reset e-mail and sent a second Resend e-mail whose link lacked a token) was deleted from the repo and the Supabase project in June 2026. Password reset e-mails flow through the send-email hook like all others. - All templates share one brand layout (
brandEmailLayout): teal-gradient header with the hosted white horizontal logo (www.plings.io/static/brand/…), teal/light-green accents perdocs/brand/design-system.md— deliberately no red anywhere; password reset is a routine action, not an alarm. - The Resend SDK resolves with
{ error }instead of throwing on API errors; the function checks it and returns500(generic body) so a delivery failure shows up as a hook failure in the auth logs instead of a silent “success”. - Recommended hardening (not yet implemented): verify the hook’s webhook signature
(standardwebhooks +
SEND_EMAIL_HOOK_SECRET) inside the function, so payload shape is not the only gate.
Sign-Up E-mail Verification (Plings-Web)
AuthContext.signUp calls supabase.auth.signUp(..., { emailRedirectTo: window.location.origin + '/' }),
so the verification link returns the user to whichever environment they signed up from
(production, localhost, or a preview deploy) instead of always landing on https://plings.io/.
Required Supabase configuration
Auth → URL Configuration → Redirect URLs must include an entry for every environment and flow:
Password recovery:
https://plings.io/reset-passwordhttp://localhost:8080/reset-password
Sign-up verification (origin root):
https://plings.io/(also the Site URL, so covered by default)http://localhost:8080/
For Vercel preview deploys, add a wildcard entry matching the project’s preview domains,
e.g. https://*-<team>.vercel.app/** — Supabase supports * and ** globs in this list.
If a redirect URL is missing from this allowlist, Supabase silently falls back to the Site URL: a password reset lands on the home page signed in without a set-password step, and a localhost sign-up verification bounces to production instead of the dev server.
Status: imported skeleton – expand with diagrams & edge cases (MFA, SSO) later.