Clerk Authentication

Jazz can be integrated with Clerk to authenticate users. This method combines Clerk's comprehensive authentication services with Jazz's local-first capabilities.

How it works

When using Clerk authentication:

  1. Users sign up or sign in through Clerk's authentication system
  2. Jazz securely stores the user's account keys with Clerk
  3. When logging in, Jazz retrieves these keys from Clerk
  4. Once authenticated, users can work offline with full Jazz functionality

This authentication method is not fully local-first, as login and signup need to be done online, but once authenticated, users can use all of Jazz's features without needing to be online.

Key benefits

  • Rich auth options: Email/password, social logins, multi-factor authentication
  • User management: Complete user administration dashboard
  • Familiar sign-in: Standard auth flows users already know
  • OAuth providers: Google, GitHub, and other popular providers
  • Enterprise features: SSO, SAML, and other advanced options

Implementation

We offer Clerk integration through our package: jazz-expo.

After installing, use <JazzProviderWithClerk /> to wrap your app.

import { const useClerk: () => LoadedClerkuseClerk, function ClerkProvider(props: ClerkProviderProps): JSX.ElementClerkProvider, const ClerkLoaded: ({ children }: React.PropsWithChildren<unknown>) => React.ReactNodeClerkLoaded } from '@clerk/clerk-expo';
import { const secureStore: () => IStorage
Creates a store based on expo-secure-store, that handles the 2048 size limit on values. The store uses a queue to manage multiple save requests and two slots (A and B) to store the key-value pairs. The function alternates between the two slots to save the key-value pairs and splits the value into chunks to save them. The two slots are used to handle corrupted data or incomplete saves. The keys used are the following: - key-latest -> 'A'/'B' - key-{A/B}-metadata -> Metadata - key-{A/B}-chunk-{i} -> data chunk - key-{A/B}-complete -> 'true'/'false'
secureStore
} from "@clerk/clerk-expo/secure-store";
import {
const JazzProviderWithClerk: (props: {
    clerk: MinimalClerkClient;
} & JazzProviderProps) => JSX.Element | null
JazzProviderWithClerk
} from "jazz-expo/auth/clerk";
function
function JazzAndAuth({ children }: {
    children: React.ReactNode;
}): React.JSX.Element
JazzAndAuth
({ children: React.ReactNodechildren }: { children: React.ReactNodechildren: React.type React.ReactNode = string | number | boolean | React.ReactElement<any, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | React.ReactPortal | null | undefined
Represents all of the things React can render. Where {@link ReactElement } only represents JSX, `ReactNode` represents everything that can be rendered.
@see{@link https://react-typescript-cheatsheet.netlify.app/docs/react-types/reactnode/ React TypeScript Cheatsheet}@example```tsx // Typing children type Props = { children: ReactNode } const Component = ({ children }: Props) => <div>{children}</div> <Component>hello</Component> ```@example```tsx // Typing a custom element type Props = { customElement: ReactNode } const Component = ({ customElement }: Props) => <div>{customElement}</div> <Component customElement={<div>hello</div>} /> ```
ReactNode
}) {
const const clerk: LoadedClerkclerk = function useClerk(): LoadedClerkuseClerk(); return ( <
const JazzProviderWithClerk: (props: {
    clerk: MinimalClerkClient;
} & JazzProviderProps) => JSX.Element | null
JazzProviderWithClerk
clerk: MinimalClerkClientclerk={const clerk: LoadedClerkclerk} sync: SyncConfigsync={{ peer: "wss://cloud.jazz.tools/?key=you@example.com"peer: `wss://cloud.jazz.tools/?key=${const apiKey: "you@example.com"apiKey}`, }} > {children: React.ReactNodechildren} </
const JazzProviderWithClerk: (props: {
    clerk: MinimalClerkClient;
} & JazzProviderProps) => JSX.Element | null
JazzProviderWithClerk
>
); } export default function function RootLayout(): React.JSX.ElementRootLayout() { const const publishableKey: string | undefinedpublishableKey = var process: NodeJS.Processprocess.NodeJS.Process.env: NodeJS.ProcessEnv
The `process.env` property returns an object containing the user environment. See [`environ(7)`](http://man7.org/linux/man-pages/man7/environ.7.html). An example of this object looks like: ```js { TERM: 'xterm-256color', SHELL: '/usr/local/bin/bash', USER: 'maciej', PATH: '~/.bin/:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin', PWD: '/Users/maciej', EDITOR: 'vim', SHLVL: '1', HOME: '/Users/maciej', LOGNAME: 'maciej', _: '/usr/local/bin/node' } ``` It is possible to modify this object, but such modifications will not be reflected outside the Node.js process, or (unless explicitly requested) to other `Worker` threads. In other words, the following example would not work: ```bash node -e 'process.env.foo = "bar"' &#x26;&#x26; echo $foo ``` While the following will: ```js import { env } from 'node:process'; env.foo = 'bar'; console.log(env.foo); ``` Assigning a property on `process.env` will implicitly convert the value to a string. **This behavior is deprecated.** Future versions of Node.js may throw an error when the value is not a string, number, or boolean. ```js import { env } from 'node:process'; env.test = null; console.log(env.test); // => 'null' env.test = undefined; console.log(env.test); // => 'undefined' ``` Use `delete` to delete a property from `process.env`. ```js import { env } from 'node:process'; env.TEST = 1; delete env.TEST; console.log(env.TEST); // => undefined ``` On Windows operating systems, environment variables are case-insensitive. ```js import { env } from 'node:process'; env.TEST = 1; console.log(env.test); // => 1 ``` Unless explicitly specified when creating a `Worker` instance, each `Worker` thread has its own copy of `process.env`, based on its parent thread's `process.env`, or whatever was specified as the `env` option to the `Worker` constructor. Changes to `process.env` will not be visible across `Worker` threads, and only the main thread can make changes that are visible to the operating system or to native add-ons. On Windows, a copy of `process.env` on a `Worker` instance operates in a case-sensitive manner unlike the main thread.
@sincev0.1.27
env
.string | undefinedEXPO_PUBLIC_CLERK_PUBLISHABLE_KEY;
if (!const publishableKey: string | undefinedpublishableKey) { throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
(
"Missing Publishable Key. Please set EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY in your .env", ); } return ( <function ClerkProvider(props: ClerkProviderProps): JSX.ElementClerkProvider tokenCache?: TokenCache | undefined
The token cache is used to persist the active user's session token. Clerk stores this token in memory by default, however it is recommended to use a token cache for production applications.
@seehttps://clerk.com/docs/quickstarts/expo#configure-the-token-cache-with-expo
tokenCache
={
const tokenCache: {
    getToken: (key: string) => Promise<null>;
    saveToken: (key: string, token: string) => Promise<void>;
    clearToken: (key: string) => Promise<void>;
}
tokenCache
}
publishableKey: string
The Clerk publishable key for your instance
@noteThis can be found in your Clerk Dashboard on the [API Keys](https://dashboard.clerk.com/last-active?path=api-keys) page
publishableKey
={const publishableKey: stringpublishableKey}
__experimental_resourceCache?: (() => IStorage) | undefined
This cache is used to store the resources that Clerk fetches from the server when the network is offline.
@experimentalThis API is experimental and may change at any moment.
__experimental_resourceCache
={const secureStore: () => IStorage
Creates a store based on expo-secure-store, that handles the 2048 size limit on values. The store uses a queue to manage multiple save requests and two slots (A and B) to store the key-value pairs. The function alternates between the two slots to save the key-value pairs and splits the value into chunks to save them. The two slots are used to handle corrupted data or incomplete saves. The keys used are the following: - key-latest -> 'A'/'B' - key-{A/B}-metadata -> Metadata - key-{A/B}-chunk-{i} -> data chunk - key-{A/B}-complete -> 'true'/'false'
secureStore
}
> <const ClerkLoaded: ({ children }: React.PropsWithChildren<unknown>) => React.ReactNodeClerkLoaded> <
function JazzAndAuth({ children }: {
    children: React.ReactNode;
}): React.JSX.Element
JazzAndAuth
>
<function Slot(props: Omit<NavigatorProps<any>, "children">): React.JSX.Element
Renders the currently selected content. There are actually two different implementations of `<Slot/>`: - Used inside a `_layout` as the `Navigator` - Used inside a `Navigator` as the content Since a custom `Navigator` will set the `NavigatorContext.contextKey` to the current `_layout`, you can use this to determine if you are inside a custom navigator or not.
Slot
/>
</
function JazzAndAuth({ children }: {
    children: React.ReactNode;
}): React.JSX.Element
JazzAndAuth
>
</const ClerkLoaded: ({ children }: React.PropsWithChildren<unknown>) => React.ReactNodeClerkLoaded> </function ClerkProvider(props: ClerkProviderProps): JSX.ElementClerkProvider> ); }

Once set up, you can use Clerk's auth methods for login and signup:

import { const useSignIn: UseSignInuseSignIn } from "@clerk/clerk-expo";
import { 
function useAccount<A extends RegisteredAccount>(): {
    me: A;
    logOut: () => void;
} (+1 overload)
useAccount
, function useIsAuthenticated(): booleanuseIsAuthenticated } from "jazz-expo";
import { class ButtonButton, class TextText } from 'react-native'; export function function AuthButton(): React.JSX.ElementAuthButton() { const { const logOut: () => voidlogOut } =
useAccount<Account>(): {
    me: Account;
    logOut: () => void;
} (+1 overload)
useAccount
();
const { const signIn: SignInResource | undefinedsignIn, const setActive: SetActive | undefinedsetActive, const isLoaded: booleanisLoaded } = function useSignIn(): UseSignInReturnuseSignIn(); const const isAuthenticated: booleanisAuthenticated = function useIsAuthenticated(): booleanuseIsAuthenticated(); if (const isAuthenticated: booleanisAuthenticated) { return <class ButtonButton title: string
Text to display inside the button. On Android the given title will be converted to the uppercased form.
title
="Logout" onPress?: ((event: GestureResponderEvent) => void) | undefined
Called when the touch is released, but not if cancelled (e.g. by a scroll that steals the responder lock).
onPress
={() => const logOut: () => voidlogOut()} />;
} const const onSignInPress: () => Promise<void>onSignInPress = async () => { if (!const isLoaded: booleanisLoaded) return; const const signInAttempt: SignInResourcesignInAttempt = await const signIn: SignInResourcesignIn.SignInResource.create: (params: SignInCreateParams) => Promise<SignInResource>create({ identifier: stringidentifier: "you@example.com", password: stringpassword: "password", }); if (const signInAttempt: SignInResourcesignInAttempt.SignInResource.status: SignInStatus | nullstatus === "complete") { await const setActive: (params: SetActiveParams) => Promise<void>setActive({ session?: string | ActiveSessionResource | null | undefined
The session resource or session id (string version) to be set as active. If `null`, the current session is deleted.
session
: const signInAttempt: SignInResourcesignInAttempt.SignInResource.createdSessionId: string | nullcreatedSessionId });
} }; return <class ButtonButton title: string
Text to display inside the button. On Android the given title will be converted to the uppercased form.
title
="Sign In" onPress?: ((event: GestureResponderEvent) => void) | undefined
Called when the touch is released, but not if cancelled (e.g. by a scroll that steals the responder lock).
onPress
={const onSignInPress: () => Promise<void>onSignInPress} />;
}

Examples

You can explore Jazz with Clerk integration in our example projects. For more Clerk-specific demos, visit Clerk's documentation.

When to use Clerk

Clerk authentication is ideal when:

  • You need an existing user management system
  • You want to integrate with other Clerk features (roles, permissions)
  • You require email/password authentication with verification
  • You need OAuth providers (Google, GitHub, etc.)
  • You want to avoid users having to manage passphrases

Limitations and considerations

  • Online requirement: Initial signup/login requires internet connectivity
  • Third-party dependency: Relies on Clerk's services for authentication
  • Not fully local-first: Initial authentication requires a server
  • Platform support: Not available on all platforms

Additional resources