Skip to content
OTFotf
All posts

Supabase auth hardening checklist that keeps production sign-in safe

D
DaveAuthor
8 min read
Supabase auth hardening checklist that keeps production sign-in safe

Production sign-in breaks in predictable places: an open redirect, an email link that lands on the wrong URL, a table that exposes rows to the wrong user, or a privileged key that shipped inside a client bundle. Hardening auth means closing those paths before launch, then proving each one with a test.

Auth documentation describes authentication as verifying identity and authorization as verifying resource access, with JSON Web Tokens for the first and row-level rules for the second. Use that split as the hardening plan. Identity checks belong in the auth configuration. Data access belongs in the database, enforced on every request.

Lock redirect urls and confirmation behavior

Email sign-in depends on redirect configuration. The password auth guide notes that confirmation links redirect to a configured URL, that the redirect must be registered as an allowed redirect URL, and that hosted projects require email confirmation by default. Treat each of those as a production setting, not a default to accept.

Record the exact values:

auth settings reviewed

site url: https://YOUR-PRODUCTION-DOMAIN
allowed redirects:
  - https://YOUR-PRODUCTION-DOMAIN/welcome
  - https://YOUR-PRODUCTION-DOMAIN/reset-callback
email confirmation: required
phone confirmation: required where phone sign-in is offered

Check that no localhost, preview, staging, or abandoned domain remains in the allowed list. An allowed redirect that points at a domain you no longer control hands a valid auth link to someone else. Remove wildcard patterns unless the product needs them and the risk was reviewed.

Test the full loop from a clean account: sign up, receive the link, confirm, sign in, request a password reset, use the reset link once, then try to reuse it. Confirm expired links fail with a clear message and that the user can request a new one. If the app runs on web and mobile from one codebase, test both clients because the redirect target and link handling differ by platform.

Enforce row access inside the database

Client-side checks hide buttons. They do not protect rows. The row-level security guide warns that a table in an exposed schema without row-level protection stays readable and writable by any role with a grant, and that adding policies does not revoke existing grants. That warning describes the most common production auth gap: policies were added, grants were never tightened.

Apply the documented procedure to every table in an exposed schema. Enable row-level protection, set grants so each role keeps only the operations it needs, then add policies that express the access rule.

-- enable protection and restrict direct access
alter table public.todos enable row level security;

revoke all on public.todos from anon;
grant select on public.todos to anon;

revoke all on public.todos from authenticated;
grant select, insert, update, delete on public.todos to authenticated;

-- owners read and edit their own rows
create policy "Owners manage their own todos"
on public.todos
for all
to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);

The example follows the guide's model: grants decide whether a role can run an operation at all, policies decide which rows it applies to. A missing grant surfaces as a permission error before any policy runs, so when a legitimate request fails, inspect grants before rewriting the policy.

Add negative tests for every policy. An authenticated user reads another user's row and gets nothing. An unauthenticated request writes and fails. A deleted membership loses access on the next request, not after the token cache clears. Row-level security production checklist walks through that test matrix table by table, including the grant check teams skip.

One codebase. iOS, Android, and web.

The Fitness Kit ships with auth, a database, and a backend already connected — no setup. Live demo at fitness-preview.otf-kit.dev.

See the live demo

Keep privileged keys server side only

The service-role key bypasses row-level rules, so it must never reach a device or browser bundle. Store it in server configuration or protected deployment settings, rotate it if it ever appeared in logs or client output, and audit which server routes actually need it.

// server-only helper, never imported by client code
import { createClient } from "@supabase/supabase-js";

export function createAdminClient() {
  const url = process.env.SUPABASE_URL;
  const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
  if (!url || !serviceKey) {
    throw new Error("Missing server auth configuration");
  }
  return createClient(url, serviceKey, {
    auth: { persistSession: false },
  });
}
// client code uses the user session, never the service key
const { data, error } = await supabase
  .from("todos")
  .select("id,title,completed")
  .order("created_at", { ascending: false });

Search release artifacts for key material before every launch. Grep the bundled output, mobile binary strings, error reports, and analytics payloads for key prefixes and project URLs. A key that appears in a crash report or log export needs rotation even if no misuse was observed. Ship an AI MVP to production checklist covers that pre-release secrets pass alongside the wider launch gate.

Harden sessions, refresh, and sign out

A session that never expires cleanly becomes a support queue. Define token lifetime, refresh behavior, idle timeout, concurrent-session policy, and what happens when a password changes or an account is disabled.

Test these paths:

- sign in on two devices, sign out on one, confirm the other behaves as designed
- revoke a session, confirm the next data request fails closed with a sign-in prompt
- expire a token mid-task, confirm the draft is preserved and the retry works
- change a password, confirm other sessions end or re-authenticate
- disable a test user, confirm read and write both stop on the next request
- clear app storage, confirm the app returns to a signed-out state without a crash

Keep user-facing errors generic. "Check your email and password" is enough for a failed sign-in. Detailed reasons such as "this email is not registered" help enumeration. Log the detailed cause server side with request identifiers, rate-limit repeated attempts, and add a short delay or captcha path when abuse appears.

If the product offers phone or social sign-in, apply the same review per provider. Each provider adds callback URLs, test accounts, data mapping, and account-linking rules. Document which identity becomes the primary key when the same person signs in two ways, and test merging and unlinking before users do it in production.

Test auth like an attacker before launch

Build a small auth test plan with named accounts: owner, teammate, former teammate, stranger, and unauthenticated. Run every read, write, share, transfer, and delete path as each identity. Record expected allow or deny beside the actual result.

auth matrix, todos feature

owner reads own row: allowed, passed
stranger reads owner row: denied, passed
former teammate writes after removal: denied, passed
unauthenticated lists rows: denied, passed
owner deletes own row: allowed, passed
stranger replays deleted row id: denied, passed

Run the matrix against the release build and production settings, not a local database with relaxed rules. Include deep links, shared URLs, background refresh, offline replay, and duplicate submit. A shared link should enforce the same policy as the in-app screen. An offline queue should not apply a write with a stale role when it syncs.

Add monitoring for auth failures after release. A sudden rise in expired sessions points at clock, refresh, or deployment issues. A rise in denied writes after a policy change points at a grant mistake. Sentry error tracking for React Native in production shows how to attach release identity and safe context so an auth regression maps to the exact build that introduced it.

Give the coding agent an auth task with evidence

An AI coding agent can add sign-in screens quickly and widen access just as quickly. Give it the schema, the allowed files, the data restrictions, and the proof required before merge.

Harden todo access for the current release.

- Read the auth and data-access rules first.
- Touch only the todos table migration, policy tests, and the todo data helper.
- Keep the service key out of client code and tests.
- Add allow and deny tests for owner, stranger, and unauthenticated roles.
- Run the focused database and type checks.
- Report passed, failed, and not-run checks separately.
- List every changed grant and policy in plain language.

Review the generated migration line by line. Confirm protection was enabled, grants were tightened, policies use the authenticated identity function, and no test bypassed the database path. Require a human decision for provider changes, session lifetime, redirect URLs, and key rotation. Those are production trust decisions, not code style.

Connect auth to the product foundation

The reusable starting point here is concrete: the same component name, props, and look on web, mobile web, and native from one codebase, backed by one theme and a copy-paste or package install. That shared UI surface makes sign-in, confirmation, reset, and error states easier to keep consistent, but it does not configure redirects, grants, or session policy. Those remain per-project production settings.

Paid full-stack kits bundle auth, billing, database access, and payments in one owned codebase, which gives the hardening checklist a real surface to run against. Start from the verified templates page, wire the production URLs and keys for the actual release, run the matrix above, and keep the auth settings under version review like any other security boundary.

Auth hardening is done when every table enforces access inside the database, redirects point only at domains you control, privileged keys never leave the server, sessions expire and revoke cleanly, and the allow-deny matrix passes on the release build. Lock those settings, test them as each role, monitor failures by release, and treat every auth change as a security release.

Sources

supabasebackendcross-platform
OTF Fitness Kit

Stop wiring. Start shipping.

  • Login, database, and backend already connected — nothing to set up
  • iOS + Android + web from one codebase
  • AI configs pre-tuned + 40+ tested prompts included