The two apps
First Six is two front-end applications over one database. Understanding that split is the fastest way to reason about where your integration data lands and who can read it.
Two front ends, one backend
There are two user-facing apps:
| App | Who uses it | Talks to |
|---|---|---|
| Student app | Students | Supabase (PostgREST), scoped by RLS |
| Staff console | Wellbeing staff | Supabase (PostgREST) + privileged server routes |
Both are Next.js apps in the same monorepo. There is also a small embed app for the marketing site, but it carries no real data and you can ignore it for integration purposes.
The backend is Supabase: Postgres, exposed over PostgREST, fronted by row-level security. There is no separate API tier to integrate against. Your data flows in through the sync and SSO endpoints and is read back out through the same RLS-protected tables the apps use.
Row-level security is the perimeter
Every table that holds tenant data has RLS policies that key off the caller's identity. A student's session can only ever read that student's rows; a staff session can only read rows for cohorts within their institution and scope.
This means there is no "admin" client that quietly sees everything by default. The boundary is enforced in the database, not in application code, so a bug in a route handler cannot leak another tenant's data through it.
// A request carries the user's JWT. PostgREST runs every query as that
// user, so RLS decides what is visible — the app never widens the scope.
const supabase = createServerClient(); // attaches the caller's auth cookie
const { data } = await supabase
.from("check_ins")
.select("week, mood")
.order("week");
// `data` contains only rows RLS permits for this caller. No tenant filter
// in the query — the policy already applied it.
The service-role boundary
A few operations legitimately need to cross the RLS line: a webhook writing on behalf of a student who is not logged in, or a SIS sync creating accounts in bulk. These run server side with the service role key, which bypasses RLS.
The service-role key bypasses every policy, so any route that uses it must re-impose tenant scoping itself, explicitly, on every query. Treat these routes as the highest-risk surface in the system and keep them small.
The rule we follow: service-role routes take the tenant from a verified source (a signed webhook, an authenticated sync secret), never from caller-supplied input, and they filter every read and write by that tenant id by hand.
The actor model
Identity has two layers. An auth_user_id is who signed in. An actor (a
student or a staff record) is what they are within an institution.
A single human can map to different actors across tenants, but never more than
one actor per tenant. When you integrate, you are almost always creating or
referencing actors: SIS sync provisions student actors, SSO links an
auth_user_id to the right actor at login time.
Where to go next
For the identity detail, read The actor model. For getting your users in, SSO setup covers the OIDC flow end to end, and SIS sync covers bulk provisioning. Both build directly on the boundaries described here.
The fastest answer is usually one question away.