Skip to Content
BuildAuth

Auth

Auth in ZXCV is built in, not bolted on. Every project ships with a full identity layer — sign-up, sign-in, sessions, the current user — and an authorization layer that decides which rows each user may touch. Both run through the same Data Gateway that guards your data, so you never hold a credential and the anon key is safe to ship in client code.

This page covers two halves:

  1. Authentication — who the user is (sign in / sign up / sessions).
  2. Authorization — what they can do (the .policy language, row-level security).

Authentication

Sign users in and out with the SDK’s auth API. Every method returns { data, error }; error is null on success.

Sign up

Create a user with an email and password:

const { data, error } = await zxcv.auth.signUp({ email: 'ada@example.com', password: 'a-strong-password', })

Sign in with a password

const { data, error } = await zxcv.auth.signInWithPassword({ email: 'ada@example.com', password: 'a-strong-password', })

Send a one-time link to the user’s inbox — no password required:

const { data, error } = await zxcv.auth.signInWithOtp({ email: 'ada@example.com', })

The user clicks the link and lands back in your app signed in.

Sign in with OAuth

Redirect to a provider and back:

const { data, error } = await zxcv.auth.signInWithOAuth({ provider: 'google', })

Sign out

await zxcv.auth.signOut()

Sessions and the current user

Once signed in, the SDK holds the session and attaches it to every request, so your queries and storage calls run as that user automatically.

Read the current user at any time:

const { data, error } = await zxcv.auth.getUser() if (data?.user) { console.log(data.user.id, data.user.email) }

React to sign-in and sign-out across your app with onAuthStateChange:

zxcv.auth.onAuthStateChange((event, session) => { // event: sign-in, sign-out, token refresh // session is null when signed out })

Use onAuthStateChange to redirect after login, clear local state on logout, or gate UI on session. Unsubscribe when your component unmounts.

Authorization

Authenticating a user tells ZXCV who they are. Authorization decides which rows they may read, write, or delete. Access is enforced at the data layer with row-level security, compiled from a small, declarative policy language — so the same rules hold across the database, realtime, functions, and storage, no matter which surface reads the data.

The 3-slot model

A rule answers three questions, in order:

  1. Who is making the request (the principal — the signed-in user)?
  2. What are they trying to do (read, write, delete)?
  3. Which rows may they touch (the predicate)?

Authorization is deny-by-default. A row is invisible unless a rule grants it — and a forbid anywhere wins globally.

The .policy language

Policies are written in a compact DSL and compiled to enforced database rules. Surfaces are declared in A-blocks; individual rules are B-statements in a normal form; forbid applies across every surface.

documents.policy
surface documents { read where doc.workspace in me.workspaces write where doc.author == me.id } forbid write where doc.locked

The compiler expands these to per-surface enforcement so a client can never reach a row a policy didn’t grant — including through realtime change feeds.

Where it runs

The same compiled policy governs every surface that can reach your data:

  • Database — row-level security on every table
  • Realtime — change feeds are filtered by the same predicate, so a subscriber never receives a row they couldn’t select
  • Functions — declared RPCs run with the caller’s principal
  • Storage — object access checks reuse the rule set

Because enforcement lives server-side in the gateway, the anon key stays safe in the browser: a user can only ever see and change the rows your policy allows.

Next steps