Symptom: you can sign in at /login with the password you set,
but the server returns 401 {"error":"invalid email or password"}.
Signup at /register may return new row violates row-level security policy for table "users".
Root cause
Row-Level Security got enabled on auth tables (users, orgs,
org_members, api_tokens, audit_log). The Vercel serverless
functions connect through Supabase's connection pooler as a role that
is subject to RLS, so the login endpoint's
SELECT password_hash FROM users WHERE email = $1 returned zero rows
even when the row existed — bcrypt.compare never ran, response is 401.
The Supabase SQL Editor uses postgres superuser (RLS-bypassing) by default, which hides the bug — running the query manually still shows your hash, fooling you into thinking the data is fine.
Fix
Run in Supabase SQL Editor:
ALTER TABLE users DISABLE ROW LEVEL SECURITY;
ALTER TABLE orgs DISABLE ROW LEVEL SECURITY;
ALTER TABLE org_members DISABLE ROW LEVEL SECURITY;
ALTER TABLE api_tokens DISABLE ROW LEVEL SECURITY;
ALTER TABLE audit_log DISABLE ROW LEVEL SECURITY;
(Codified in migration 007_fix_rls_auth_tables.sql.)
Why this is safe
The five auth tables are never exposed directly to clients. Every
read/write goes through the Next.js API routes, which control what
gets returned (password hashes never leave the server, emails are
returned only to the owning user via /api/auth/me, etc.). The app's
session-cookie / bearer-token authorization layer is the actual gate.
Tenant-data tables (memories, namespaces, doc_pages,
webhook_endpoints, webhook_deliveries) still have RLS enabled —
those need it because SDK clients hit them with their own bearer tokens
and we don't want one org reading another org's rows.
Diagnosis if it's not RLS
If RLS is already off and login still 401s:
-
Confirm your account row exists:
SELECT id, email, length(password_hash), substring(password_hash, 1, 4) FROM users WHERE email = 'you@example.com';Hash should be 60 chars, prefix
$2a$or$2b$. -
Check that the password isn't being autofilled with a stale value — open DevTools → Network → submit login → click the request → "Payload" tab → look at what
passwordis actually being sent. -
If all else fails, reset the hash via SQL using a known bcryptjs hash matching a new password.