Skip to content

Auth & audiences setup

The portal supports two authentication modes: supabase (OAuth and email/password, with in-app audience management) and password (bcrypt-hashed secrets in environment variables, a developer fallback). Use supabase for all real deployments.

Set AUTH_PROVIDER in your .env (or in the Netlify environment variables):

AUTH_PROVIDER=supabase

Omit AUTH_PROVIDER or set it to password to use password-only auth. In password mode, audiences are defined entirely by the VIEWER_<NAME>_PASSWORD environment variables — they cannot be managed in the editor UI.

All client sites share one Supabase project. Every *.drawn.guide site does OAuth, invite, and password-reset flows against the same Auth instance. The three required env vars are the same across all client sites:

SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key

Each site has a default viewer audience seeded at site creation. The isDefault flag is a Supabase-only concept — it marks the audience that viewers fall into when they authenticate without being assigned to a named audience. This is set in the database at provisioning time and does not appear in password-mode sites.

The flag now also grants access: a viewer whose sign-in belongs to the default audience can reach /audiences and reveal every sign-in password there, so the client can hand a new partner access without an editor login. Treat a default-audience sign-in accordingly. See Auth architecture.

A newly provisioned site gets one credential automatically — username client on the default audience, with a generated passphrase returned in the provisioning result. Hand that over; it is not stored in readable form, so if it is lost an editor must set a new password (which then becomes revealable in-portal).

Named sign-ins replace the audience password

Section titled “Named sign-ins replace the audience password”

Viewers log in with a username and password. Each credential row points at exactly one audience and inherits its access, so agency1/agency2/agency3 can all sit on External Agencies and be revoked independently. Audiences themselves carry no password at all — the column was dropped once every portal had been redeployed (20260805154537). A new audience has no way in until you add a sign-in to it.

The credentials migration back-fills one credential per existing audience password, named after the audience slug. Existing viewers therefore keep their password and simply type e.g. external-agencies as the username — no lockout and no coordination needed.

/audiences can only show a password stored with a reversible copy alongside its bcrypt hash. Anything created before that shipped — including every back-filled credential and the provisioner’s initial sign-in — reads as “can’t be shown” until its password is next changed in Site Settings → Viewer Access. bcrypt cannot be reversed, so there is no backfill. Expect to reset each carried-over sign-in once per site if the client wants to look them up.

The following rules apply to the Supabase dashboard (the production source of truth). Do not apply them via supabase config push — the monorepo’s supabase/config.toml has site_url set to localhost and would clobber production settings.

Site URL — set to a single concrete origin:

https://acme.drawn.guide

Wildcards are rejected in the Site URL field and a wildcard entry silently breaks the fallback path for every site. Pick one concrete domain as the Site URL (typically the main client site or the first site created).

Redirect URL allow-list — restrict to platform-controlled paths:

https://*.drawn.guide/edit/login
https://*.drawn.guide/edit/login/callback

These are the only two paths any auth flow redirects to: OAuth and invite/reset flows use /edit/login/callback; the existing-user invite login link uses /edit/login. Path-restricting the allow-list means a wildcard subdomain entry cannot be abused to redirect to an arbitrary page on any drawn.guide subdomain.

Never add a bare *.netlify.app entry. netlify.app is a shared hosting domain — any Netlify tenant could register a subdomain and become a valid redirect target in your allow-list, enabling an open-redirect attack that could steal auth codes.

The portal builds the OAuth redirect_to parameter from the live request origin, not from the build-time SITE environment variable. This is required because Netlify’s URL environment variable (which Astro bakes into import.meta.env.SITE) can resolve to the *.netlify.app host instead of the custom domain — making redirect_to unmatched in the allow-list and landing the PKCE callback on a different origin from where the verifier cookie was set.

Any custom auth flow you add must derive its redirect origin from the incoming request, not from import.meta.env.SITE.

In password mode, audiences are defined by environment variables. Each audience needs a name (the <NAME> suffix), a bcrypt password hash, and optionally a display color:

VIEWER_INTERNAL_PASSWORD=\$2b\$10\$...
VIEWER_INTERNAL_COLOR=#10b981
VIEWER_EXTERNAL_PASSWORD=\$2b\$10\$...
VIEWER_EXTERNAL_COLOR=#3b82f6

See Environment variables for the bcrypt hash generation command and the \$-escaping requirement.

Audience settings are read-only in the editor UI when running in password mode. To add or remove an audience, edit the environment variables and redeploy.

Password mode also cannot reveal passwords on /audiences — the env vars hold only bcrypt hashes, so createPasswordAuth() omits the reveal capability and the page masks them.