How sign-in, roles, and row-level security work in CivicLoop County OS.
/[locale]/login, enter email + password (Supabase Auth).requireStaff(locale, minRole) runs on every gated route/server action and redirects to /login (307) when there is no valid staff session./mfa).| Role | Can do |
|---|---|
agent |
Work the 311 queue, reply to residents, resolve with evidence |
supervisor |
All agent rights + agency modules (KPIs + manage records) + data feeds + assistant |
department_head |
Supervisor rights, scoped to their department |
director |
County-wide dashboard, council views, automations |
county_admin |
Admin console: staff, roles, 2FA status, locations |
requireStaff takes a minimum role; a higher role satisfies a lower requirement.
The county boss is the tenant admin (county_admin, /admin). TaTech engineers are a separate tier: rows in platform_admins with access levels owner > engineer > support, gated by requirePlatformAdmin at /[locale]/platform (the Platform Console). They are not county staff and have no tenant role.
/suspended; platform admins can still inspect via impersonation. Enforced in requireStaff.county_admin session with an amber banner and a one-tap exit. Every start/stop is written to platform_impersonations.scripts/seed-platform-admins.mjs); an owner manages others but cannot change their own row.county_id in (select county_id from staff where auth_user_id = auth.uid()).requireStaff, using the service-role admin client. A flaw in the UI cannot bypass the database boundary./public portals, Open311) use the admin client on the server and return only public-record aggregates - never PII.POST /api/automations, POST /api/cron/synthetic - header x-automation-secret: $AUTOMATION_SECRET.POST /api/feeds/ingest - header x-feeds-secret: $FEEDS_INGEST_SECRET.| Situation | Result |
|---|---|
| No staff session on a gated route | 307 redirect to /login |
| Role too low | redirect / not-authorized |
| Valid session, MFA required, not enrolled | nudge to /mfa |
| Machine endpoint, secret unset | 503 not_configured |
| Machine endpoint, wrong secret | 401 unauthorized |