Skip to content

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>. Check VITE_SUPABASE_URL is unset in the frontend .env so it shows the dev form, and the backend has SUPABASE_URL / SUPABASE_JWT_SECRET unset.
  • The token is passed via setAuthToken() in lib/api.ts on every auth change — if it's missing, check AuthContext.

"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 call POST /billing/dev-upgrade as 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

  1. GET /health — confirms the API is up and shows auth_mode and stripe.
  2. The browser Network tab — the failing request's status code maps directly to a section above (401 auth, 402 paywall, 403 role, 4xx validation).
  3. backend/app/auth.py — most "why can't this user do X" questions end here.