Symptom: curl, Python, .NET, or TypeScript SDK calls return
401 Unauthorized with body {"error":"not signed in"} or
{"error":"session expired"}. The dashboard works fine in the browser.
Likely causes (in probability order):
1. The server is on an older deploy that only reads the session cookie
The hosted backend originally only checked req.cookies.mnueron_session
for the bearer token. The fix that lets Authorization: Bearer mnu_...
work landed in commit <TBD-after-merge> — if your mnueron.com is
running an older build, no header-based auth will succeed.
Verify: hit https://www.mnueron.com/api/health (no auth). Response
should be {"ok":true,"service":"mnueron",...}. The Vercel deployment
ID is in response headers — confirm it's a build after the fix landed.
Fix: redeploy the latest main from Vercel. No DB changes needed.
2. RLS is blocking api_tokens
If row-level security is enforced on api_tokens, the server's
SELECT … FROM api_tokens WHERE token_hash = $1 returns zero rows
even when the token exists.
Verify: in Supabase SQL Editor —
SELECT c.relname, c.relrowsecurity AS rls
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'public' AND c.relname = 'api_tokens';
Should show rls = false. If it's true, run:
ALTER TABLE api_tokens DISABLE ROW LEVEL SECURITY;
3. The token was revoked or expired
Verify: in SQL Editor —
SELECT id, prefix, name, expires_at, created_at
FROM api_tokens
WHERE prefix = LEFT('your_token_first_8_chars', 8);
expires_at should be NULL or in the future. If the row is missing,
the token was revoked. Issue a fresh one at /account-settings/tokens.
4. The token has invisible chars from clipboard / autofill
Some browsers / clipboard tools introduce trailing whitespace or
smart-quote substitutions. Verify the token sent by the client by
opening DevTools → Network → click the request → "Headers" → check
the literal Authorization value matches what you copied character-by-
character.