Troubleshooting¶
A runbook for common failures. Search this page for the symptom or the error string.
"401 Unauthorized" / "Dev-auth mode: expected 'dev:'"¶
The backend is in dev-auth mode but the frontend sent a real JWT (or nothing).
- Local: the frontend login form should send
dev:<email>. CheckVITE_SUPABASE_URLis unset in the frontend.envso it shows the dev form, and the backend hasSUPABASE_URL/SUPABASE_JWT_SECRETunset. - The token is passed via
setAuthToken()inlib/api.tson every auth change — if it's missing, checkAuthContext.
"402 Payment Required" on an action that should work¶
The workspace is free. Either:
- It's meant to be free — this is the paywall doing its job.
- You're testing a write path: sign in as
owner@navase.example, go to Admin → Companies, toggle the target workspace premium (or callPOST /billing/dev-upgradeas that workspace's admin).
"403 Managers only" / "App owner only"¶
Role gate, not the paywall. require_admin needs role in (admin, super_admin);
require_super_admin needs exactly super_admin. Check users.role for that
email.
CORS errors in the browser console¶
CORS_ORIGINS (backend) doesn't include the frontend's origin. It's a
comma-separated list; settings.cors_origin_list splits it. Add the origin,
restart the API.
Dates compare wrong / "can't compare offset-naive and offset-aware datetimes"¶
SQLite returns naive datetimes. Code that compares scheduled_for /
last_activity_at re-applies tzinfo=timezone.utc first — see
services/dormancy.py and routers/followups.py. Any new date comparison must
do the same.
A lead won't leave the "dormant" list¶
Dormancy is derived live from Contact.last_activity_at vs
Company.dormancy_days. last_activity_at is bumped by: logging an
interaction, scheduling a follow-up, or a stage change (POST
/contacts/{id}/stage). If it's not updating, check the write path actually set
it (next_activity_bump / an explicit utcnow() assignment) and committed.
The hosted demo "lost all its data"¶
Expected. Render free tier has an ephemeral disk; SQLite resets on every cold
start (15 min idle) and every redeploy. seed_demo_data / seed.py repopulates
on the next sign-in. For persistence, move to Postgres.
The hosted demo is slow / 502 on first hit¶
Render free services sleep after 15 minutes idle and take ~50 seconds to wake. The first request after sleep may 502 or hang — retry after a minute. Warm it before a demo.
Changed a model, the new column isn't there¶
Base.metadata.create_all() only creates missing tables, it never alters an
existing one. On SQLite: delete crm.db and re-seed. On Postgres: apply the
ALTER TABLE by hand (or add Alembic).
Stripe checkout does nothing / "simulate payment" button is showing¶
settings.stripe_enabled is false — it needs both STRIPE_SECRET_KEY and
STRIPE_PRICE_ID. With them unset the app deliberately shows the simulate
button instead.
Webhook fired but the workspace didn't go premium¶
/webhooks/stripe needs STRIPE_WEBHOOK_SECRET to verify the signature, and it
acts on checkout.session.completed. Check the event type, the secret, and that
the company_id was passed through the checkout session metadata.
Where to look first¶
GET /health— confirms the API is up and showsauth_modeandstripe.- The browser Network tab — the failing request's status code maps directly to a section above (401 auth, 402 paywall, 403 role, 4xx validation).
backend/app/auth.py— most "why can't this user do X" questions end here.