Troubleshooting
Common failures when setting up or running SAPOT locally, grouped by symptom. See getting-started/quickstart.md for the setup path these assume.
Server won't start or crashes on import
Symptom: RuntimeError raised immediately when running uvicorn app.main:app.
Cause: One of DATABASE_URL, JWT_SECRET_KEY, or CORS_ALLOWED_ORIGINS is unset. All three are required and fail fast at import time — see the repo-root SECURITY.md for why.
Fix: Set all three in server/.env (or your shell environment) before starting. See environment-config.md for the full list and quickstart.md for a working example.
Server starts but MariaDB connection fails
Symptom: sqlalchemy.exc.OperationalError / Can't connect to MySQL server on startup, after the required-env-var check passes.
Cause: DATABASE_URL points to a MariaDB instance that isn't running, isn't reachable from this host, or has wrong credentials.
Fix:
mysql -u <user> -p -h <host> -e "SELECT 1;" # confirm the DB is reachable with these exact credentials
If this fails, check MariaDB is running (sudo systemctl status mariadb) and that the user/password/host/port in DATABASE_URL match.
Mobile app can't reach the server
Symptom: Registration/login hangs or shows a network error; nothing appears in the server's logs for the request.
Cause (most common): the phone and the server are not on the same Wi-Fi network, or EXPO_PUBLIC_DEV_HOST doesn't match the server's actual LAN IP.
Fix:
- Confirm the server's LAN IP:
ip addr(Linux) orifconfig(macOS) on the server machine — look for the Wi-Fi/Ethernet interface IP, not127.0.0.1. - Confirm the phone is on the same Wi-Fi network as that machine (not a guest network, not cellular data).
- Confirm
EXPO_PUBLIC_DEV_HOSTinmobile-app/sapot-mobile-app/.env.localmatches that IP exactly, and that the app's in-app server settings (getting-started screen → Server Mode → settings icon) match too — both must agree. - Confirm the server is reachable from other devices, not just
localhost— the Docker setup's Nginx publishes on every host interface by default; if running bare-metal instead (uvicorn app.main:app --host 0.0.0.0 --port 8000, see server-setup.md), binding to127.0.0.1makes it unreachable from any other device.
CORS error in the admin frontend or a browser-based client
Symptom: Browser console shows a CORS error; the request never completes.
Cause: The calling origin isn't in CORS_ALLOWED_ORIGINS.
Fix: Add the exact origin (scheme + host + port) to the comma-separated CORS_ALLOWED_ORIGINS env var and restart the server. See environment-config.md.
/testing/* endpoints return 404 in development or staging
Symptom: A test helper endpoint like /testing/test-make-admin returns 404 even locally.
Cause: The testing router is gated behind ENVIRONMENT=development or staging (see SECURITY.md) — it's unreachable unless that env var is set to one of those values exactly.
Fix: Set ENVIRONMENT=development for local development or ENVIRONMENT=staging for a QA deployment. Never set either in a production deployment.
LAN peer discovery finds nothing (mDNS)
Symptom: Two devices on the same Wi-Fi network don't see each other in the app.
Cause: Some routers/networks isolate clients from each other ("AP/client isolation"), which blocks mDNS broadcast between devices even though both can reach the internet/server fine. Corporate and public Wi-Fi networks commonly enable this; a MikroTik router configured for a SAPOT deployment should not.
Fix: Confirm client isolation is disabled on the router/AP. As a workaround for local dev on an isolated network, use a phone hotspot or a dedicated router instead.
Server rejects GSM inbound callbacks
Symptom: Inbound SMS forwarding fails and the server logs show a rejected X-GSM-Secret header.
Cause: GSM_SECRET differs between the two components' env files.
Fix: Set the exact same value for GSM_SECRET in both server/.env and the GSM module's env file. See gsm-module-setup.md.
Port collision between the server and GSM module
Symptom: GSM module fails to bind, or one of the two services silently doesn't respond, when both run on the same host.
Cause: The GSM module's config.py documents a PORT default of 8000, matching the server's default — but PORT is not actually read: GSM-fastapi/main.py hardcodes uvicorn.run(..., port=8001, ...) regardless of the variable. In practice the two services don't collide because the GSM module always binds 8001. See environment-config.md.
Fix: No action needed for the port itself — the GSM module always listens on 8001. If you still see a collision, confirm nothing else on the host is bound to 8001, and ensure the server's _gsm_http_client base URL points at 8001.
Still stuck?
Check the repo-root SECURITY.md for known required env vars, environment-config.md for the full variable reference across every component, and architecture/system-overview.md for how the components are expected to talk to each other.