← All posts
·5 min read
#supabase#postgres#database

Supabase Auth Schema Explained: Users, Identities, Sessions

What lives in Supabase's auth schema — users, identities, sessions, refresh_tokens — and how they relate when you introspect a live database.

The short version

Supabase stores auth in a separate auth schema — not your public schema. Every user has exactly one row in auth.users, one or more rows in auth.identities (one per login provider), and can hold multiple active auth.sessions, each backed by a refresh_tokens row. If you've seen tables you didn't create, these are the ones.

auth.users 1───* auth.identities
auth.users 1───* auth.sessions
auth.sessions 1───* auth.refresh_tokens

Why an auth schema at all

Supabase keeps auth separate from your app's tables. Your public schema is where your models live; auth is locked down and managed by GoTrue. You read from it, you don't write to it. That separation is why your migrations never touch these tables — and why introspecting a Supabase database shows a schema you didn't write.

The four tables that matter

auth.users — one row per user.

ColumnWhat it holds
idUUID PK; stable identifier
email / phoneContact + login identity
encrypted_passwordBcrypt hash (email/password only)
raw_app_meta_dataProvider claims: provider, providers, email
raw_user_meta_dataCustom metadata you set
is_sso_userTrue when sign-in came via SSO
confirmed_atWhen primary identity was confirmed

auth.identities — one row per login method. Email and GitHub = two rows. provider_id is the provider's identifier; identity_data is the raw claim payload. FK user_id → users.id.

auth.sessions — a browser/token session. One per device roughly. aal, user_agent, ip, not_after, factor_id (MFA).

auth.refresh_tokens — the long-lived token backing a session. Access tokens ~1h (JWT). Refresh tokens are long, with revoked flag and parent for token reuse detection.

How they relate (the joins you'll actually write)

select u.email, i.provider, s.id as session_id
from auth.users u
join auth.identities i on i.user_id = u.id
join auth.sessions  s on s.user_id = u.id;

The 80% case: "who is signed in, through which provider, on which sessions." Every relationship is a plain foreign key — identities.user_id, sessions.user_id, refresh_tokens.session_id — exactly what a diagram turns into readable arrows. Paste a read-only connection string into dbdiagramr to trace them visually.

What's usually NOT your business

auth.instances, auth.audit_log_entries, and auth.schema_migrations are Supabase bookkeeping. audit_log_entries records admin actions; instances is multi-tenancy plumbing. If a diagram shows them, ignore them — your reads live in the other four.

Practical tips

  • Never write to auth tables directly. Use the Supabase client / Admin API. Direct inserts create orphaned identities, unhashed passwords.
  • Foreign-key joins work across schemas. auth.users.id is the same UUID you'd use in public — join public.profiles.user_id → auth.users.id.
  • A user with no identity row is a sign of trouble. Every normal user has at least one. Orphaned identities without a users row = import bug.
  • Sessions accumulate. Many sessions per user isn't a leak, it's devices and tabs.

See it live: the Supabase auth schema diagram was generated by introspecting a live database — users, identities, sessions, and refresh_tokens with foreign keys rendered as relationships.

FAQ

Where is the Supabase auth schema?
In the auth schema, separate from publicauth.users, auth.identities, auth.sessions, auth.refresh_tokens, plus internal tables.

What is auth.users used for?
Single source of truth for who can sign in. One row per user, with email/phone, password hash, metadata, and provider flags.

Why does one user have multiple identities?
Each login method is a separate auth.identities row. Email + GitHub + Google = three rows, all pointing at the same users.id.

What's the difference between a session and a refresh token?
Session is device/token context; refresh token is the long-lived credential that renews the short-lived JWT. One session maps to one refresh token chain.

Can I join auth tables to my public tables?
Yes — use auth.users.id as the join key with your public.* tables.

Visualize your own database

Paste your PostgreSQL connection string and get an interactive ER diagram in under 10 seconds. No signup required.