Authentication

Zero uses a JWT-based flow to authenticate connections to zero-cache.

Frontend

During login:

  1. Your API server creates a JWT and sends it to your client.
  2. Your client constructs a Zero instance with this token by passing it to the auth option.
const zero = new Zero({
  ...,
  auth: token, // your JWT
  userID, // this must match the `sub` field from `token`
});

Server

For zero-cache to be able to verify the JWT, one of the following environment variables needs to be set:

  1. ZERO_AUTH_SECRET - If your API server uses a symmetric key (secret) to create JWTs then this is that same key.
  2. ZERO_AUTH_JWK - If your API server uses a private key to create JWTs then this is the corresponding public key, in JWK format.
  3. ZERO_AUTH_JWKS_URL - Many auth providers host the public keys used to verify the JWTs they create at a public URL. If you use a provider that does this, or you publish your own keys publicly, set this to that URL.

Refresh

The auth parameter to Zero can also be a function:

const zero = new Zero({
  ...,
  auth: async () => {
    const token = await fetchNewToken();
    return token;
  },
  userID,
});

In this case, Zero will call this function to get a new JWT if verification fails.

Opaque Auth Tokens

If you use only custom mutators and synced queries, you can use an opaque token instead of a JWT. This is because in this case Zero's permission system is not used and thus Zero doesn't need to be able to decode your auth token. In this case, you do not need to set ZERO_AUTH_SECRET, ZERO_AUTH_JWK, or ZERO_AUTH_JWKS_URL on the server.

You will still set the auth parameter to Zero on the client as described above, but Zero won't try to decode or validate it. Zero will simply forward it to your server in the Authorization header.

Restricting yourself to custom mutators and synced queries also enables standard cookie-based auth.

To do this, you'll need zero-cache and your server to run on the same domain so that they can share cookies. This happens automatically for dev as long as your frontend and zero-cache are both running on localhost with different ports.

For production you'll need to do two things:

  1. Run zero-cache on a subdomain of your main site (e.g., zero.example.com if your main site is example.com). Consult your hosting provider's docs, or your favorite LLM for how to configure this.
  2. Set cookies from your main site with the Domain attribute set to your main domain (e.g., .example.com). If you use a third-party auth provider, consult their docs on how to do this. For example, for Better Auth, this is done with the crossSubDomainCookies feature.

Once you've done these two steps, tell zero-cache to forward cookies to your server by setting these two config vars:

ZERO_GET_QUERIES_FORWARD_COOKIES=true
ZERO_MUTATE_FORWARD_COOKIES=true

From there, you can authenticate the user using cookies the same way you would for any other request to your server.

Client-Side Data Storage

Zero stores client-side data in IndexedDB by default, but this is customizable with the kvStore parameter:

const zero = new Zero({
  // Store data in React Native's SQLite database
  // See https://zero.rocicorp.dev/docs/react-native
  kvStore: expoSQLiteStoreProvider(),
});

const zero = new Zero({
  // Store data in memory, it disappears on refresh
  kvStore: 'mem',
});

Because multiple users can share the same browser, Zero requires that you provide a userID parameter on construction:

const zero = new Zero({
  ...,
  userID: "user-123",
});

Zero stores each user's data in a different IndexedDB instance. This allows users to quickly switch between multiple users and accounts without resyncing.

If your application is unauthenticated, or if you don't need fast user switching, you can just set userID to a constant like anon or guest:

const zero = new Zero({
  ...,
  userID: "anon",
});

Alternately, if you have more than one set of Zero data per-user (i.e., for different apps in the same domain), you can additionally use the storageKey parameter:

const zero = new Zero({
  ...,
  userID: "user-123",
  storageKey: "my-app",
});

If specified, storageKey is concatenated along with userID and other internal Zero information to form a unique IndexedDB database name.

Zero's IndexedDB databases are prefixed with 'rep' or 'replicache' because ... well because we haven't fixed this yet.

Zero's IndexedDB databases are prefixed with 'rep' or 'replicache' because ... well because we haven't fixed this yet.

Logging Out

When a user logs out, you should consider what should happen to the synced data.

If you do nothing, the synced data will be left on the device. The next login will be a little faster because Zero doesn't have to resync that data from scratch. But also, the data will be left on the device indefinitely which could be undesirable for privacy and security.

If you instead want to clear data on logout, Zero provides the dropAllDatabases function:

import {dropAllDatabases} from '@rocicorp/zero';

// Returns an object with:
// - The names of the successfully dropped databases
// - Any errors encountered while dropping
const {dropped, errors} = await dropAllDatabases();

Permissions

Any data placed into your JWT (claims) can be used by permission rules on the backend.

const isAdminRule = (decodedJWT, {cmp}) => cmp(decodedJWT.role, '=', 'admin');

See the permissions section for more details.

Examples

See zbugs or hello-zero.