# Jazz
## Getting started
### Overview
# Learn some Jazz
**Jazz is a new kind of database** that's **distributed** across your frontend, containers, serverless functions and its own storage cloud.
It syncs structured data, files and LLM streams instantly, and looks like local reactive JSON state.
It also provides auth, orgs & teams, real-time multiplayer, edit histories, permissions, E2E encryption and offline-support out of the box.
---
## Quickstart
### Show me
[Check out our tiny To Do list example](/docs#a-minimal-jazz-app) to see what Jazz can do in a nutshell.
### Help me understand
Follow our [quickstart guide](/docs/quickstart) for a more detailed guide on building a simple app with Jazz.
### Just want to get started?
You can use [create-jazz-app](/docs/tooling-and-resources/create-jazz-app) to create a new Jazz project from one of our starter templates or example apps:
```sh
npx create-jazz-app@latest --api-key you@example.com
```
\--- Section applies only to react ---
**Using an LLM?** [Add our llms.txt](/react/llms-full.txt) to your context window!
\--- End of react specific section ---
\--- Section applies only to svelte ---
**Using an LLM?** [Add our llms.txt](/svelte/llms-full.txt) to your context window!
\--- End of svelte specific section ---
\--- Section applies only to react-native ---
**Using an LLM?** [Add our llms.txt](/react-native/llms-full.txt) to your context window!
\--- End of react-native specific section ---
\--- Section applies only to react-native-expo ---
**Using an LLM?** [Add our llms.txt](/react-native-expo/llms-full.txt) to your context window!
\--- End of react-native-expo specific section ---
\--- Section applies only to vanilla ---
**Using an LLM?** [Add our llms.txt](/vanilla/llms-full.txt) to your context window!
\--- End of vanilla specific section ---
**Info:**
Requires at least Node.js v20\. See our [Troubleshooting Guide](/docs/troubleshooting) for quick fixes.
## How it works
1. **Define your data** with CoValues schemas
2. **Connect to storage infrastructure** (Jazz Cloud or self-hosted)
3. **Create and edit CoValues** locally
4. **Get automatic sync and persistence** across all devices and users
Your UI updates instantly on every change, everywhere. It's like having reactive local state that happens to be shared with the world.
## A Minimal Jazz App
Here, we'll scratch the surface of what you can do with Jazz. We'll build a quick and easy To Do list app — easy to use, easy to build, and easy to make comparisons with!
This is the end result: we're showing it here running in two iframes, updating in real-time through the Jazz Cloud.
Try adding items on the left and watch them appear instantly on the right!
**Info: Using Jazz Cloud**
These two iframes are syncing through the Jazz Cloud. You can use the toggle in the top right to switch between 'online' and 'offline' on each client, and see how with Jazz, you can keep working even when you're offline.
### Imports
Start by importing Jazz into your app.
```ts
import { co, z } from 'jazz-tools';
import { JazzBrowserContextManager } from 'jazz-tools/browser';
```
### Schema
Then, define what your data looks like using [Collaborative Values](/docs/core-concepts/covalues/overview) — the building blocks that make Jazz apps work.
```ts
const ToDo = co.map({ title: z.string(), completed: z.boolean() });
const ToDoList = co.list(ToDo);
```
### Context
Next, [give your app some context](/docs/project-setup#give-your-app-context) and tell Jazz your sync strategy — use the Jazz Cloud to get started quickly. We'll also create our to do list and get its ID here to use later.
```ts
await new JazzBrowserContextManager().createContext({
sync: {
peer: 'wss://cloud.jazz.tools?key=minimal-vanilla-example',
when: 'always',
},
});
const newList = ToDoList.create([{ title: 'Learn Jazz', completed: false }]);
const listId = newList.$jazz.id;
```
### Build your UI
Now, build a basic UI skeleton for your app.
```ts
const app = document.querySelector('#app')!;
const id = Object.assign(document.createElement('small'), {
innerText: `List ID: ${listId}`,
});
const listContainer = document.createElement('div');
app.append(listContainer, id);
```
### Display Items
Display your items and add logic to mark them as done...
```ts
function toDoItemElement(todo: co.loaded) {
const label = document.createElement('label');
const checkbox = Object.assign(document.createElement('input'), {
type: 'checkbox',
checked: todo.completed,
onclick: () => todo.$jazz.set('completed', checkbox.checked),
});
label.append(checkbox, todo.title);
return label;
}
```
### Add New Items
...and add new items to the list using an input and a button.
```ts
function newToDoFormElement(list: co.loaded) {
const form = Object.assign(document.createElement('form'), {
onsubmit: (e: Event) => {
e.preventDefault();
list.$jazz.push({ title: input.value, completed: false });
}
});
const input = Object.assign(document.createElement('input'), {
placeholder: 'New task',
});
const btn = Object.assign(document.createElement('button'), {
innerText: 'Add',
});
form.append(input, btn);
return form;
}
```
### Subscribe to Changes
Now for the magic: listen to changes coming from [**anyone, anywhere**](/docs/permissions-and-sharing/overview), and update your UI in real time.
```ts
const unsubscribe = ToDoList.subscribe(
listId,
{ resolve: { $each: true } },
(toDoList) => {
const addForm = newToDoFormElement(toDoList);
listContainer.replaceChildren(
...toDoList.map((todo) => {
return toDoItemElement(todo);
}),
addForm
);
}
);
```
### Simple Routing
Lastly, we'll add a tiny bit of routing logic to be able to share the list by URL: if there's an `id` search parameter, that'll be the list we'll subscribe to later. If we don't have an `id`, we'll [create a new ToDo list](/docs/core-concepts/covalues/colists#creating-colists). We'll replace the section where we created the `ToDoList` above.
```ts
//[!code --:2]
const newList = ToDoList.create([{ title: 'Learn Jazz', completed: false }]);
const listId = newList.$jazz.id;
// [!code ++:8]
const listId = new URLSearchParams(window.location.search).get('id');
if (!listId) {
const newList = ToDoList.create([{ title: 'Learn Jazz', completed: false }]);
await newList.$jazz.waitForSync();
window.location.search = `?id=${newList.$jazz.id}`;
throw new Error('Redirecting...');
}
```
### All Together
Put it all together for a simple Jazz app in less than 100 lines of code.
```ts
import { co, z } from 'jazz-tools';
import { JazzBrowserContextManager } from 'jazz-tools/browser';
const ToDo = co.map({ title: z.string(), completed: z.boolean() });
const ToDoList = co.list(ToDo);
await new JazzBrowserContextManager().createContext({
sync: {
peer: 'wss://cloud.jazz.tools?key=minimal-vanilla-example',
when: 'always',
},
});
const listId = new URLSearchParams(window.location.search).get('id');
if (!listId) {
const newList = ToDoList.create([{ title: 'Learn Jazz', completed: false }]);
await newList.$jazz.waitForSync();
window.location.search = `?id=${newList.$jazz.id}`;
throw new Error('Redirecting...');
}
const app = document.querySelector('#app')!;
const id = Object.assign(document.createElement('small'), {
innerText: `List ID: ${listId}`,
});
const listContainer = document.createElement('div');
app.append(listContainer, id);
function toDoItemElement(todo: co.loaded) {
const label = document.createElement('label');
const checkbox = Object.assign(document.createElement('input'), {
type: 'checkbox',
checked: todo.completed,
onclick: () => todo.$jazz.set('completed', checkbox.checked),
});
label.append(checkbox, todo.title);
return label;
}
function newToDoFormElement(list: co.loaded) {
const form = Object.assign(document.createElement('form'), {
onsubmit: (e: Event) => {
e.preventDefault();
list.$jazz.push({ title: input.value, completed: false });
}
});
const input = Object.assign(document.createElement('input'), {
placeholder: 'New task',
});
const btn = Object.assign(document.createElement('button'), {
innerText: 'Add',
});
form.append(input, btn);
return form;
}
const unsubscribe = ToDoList.subscribe(
listId,
{ resolve: { $each: true } },
(toDoList) => {
const addForm = newToDoFormElement(toDoList);
listContainer.replaceChildren(
...toDoList.map((todo) => {
return toDoItemElement(todo);
}),
addForm
);
}
);
```
## Want to see more?
Have a look at our [example apps](/examples) for inspiration and to see what's possible with Jazz. From real-time chat and collaborative editors to file sharing and social features — these are just the beginning of what you can build.
If you have any questions or need assistance, please don't hesitate to reach out to us on [Discord](https://discord.gg/utDMjHYg42). We'd love to help you get started.
### Quickstart
# Get started with Jazz in 10 minutes
This quickstart guide will take you from an empty project to a working app with a simple data model and components to create and display your data.
## Create your App
\--- Section applies only to react ---
We'll be using Next.js for this guide per the [React team's recommendation](https://react.dev/learn/creating-a-react-app), but Jazz works great with vanilla React and other full-stack frameworks too.
You can accept the defaults for all the questions, or customise the project as you like.
##### npm:
```sh
npx create-next-app@latest --typescript jazzfest
cd jazzfest
```
##### pnpm:
```sh
pnpx create-next-app@latest --typescript jazzfest
cd jazzfest
```
\--- End of react specific section ---
\--- Section applies only to svelte ---
We'll be using SvelteKit for this guide, per the [Svelte team's recommendation](https://svelte.dev/docs/svelte/getting-started), but Jazz works great with vanilla Svelte too.
You can accept the defaults for all the questions, or customise the project as you like.
##### npm:
```sh
npx sv create --types ts --template minimal jazzfest
cd jazzfest
```
##### pnpm:
```sh
pnpx sv create --types ts --template minimal jazzfest
cd jazzfest
```
\--- End of svelte specific section ---
**Note: Requires Node.js 20+**
## Install Jazz
The `jazz-tools` package includes everything you're going to need to build your first Jazz app.
##### npm:
```sh
npm install jazz-tools
```
##### pnpm:
```sh
pnpm add jazz-tools
```
## Get your free API key
Sign up for a free API key at [dashboard.jazz.tools](https://dashboard.jazz.tools) for higher limits or production use, or use your email address as a temporary key to get started quickly.
\--- Section applies only to react ---
**File name: .env**
\--- End of react specific section ---
\--- Section applies only to svelte ---
**File name: .env**
\--- End of svelte specific section ---
##### React:
```bash
NEXT_PUBLIC_JAZZ_API_KEY="you@example.com" # or your API key
```
##### Svelte:
```bash
PUBLIC_JAZZ_API_KEY="you@example.com" # or your API key
```
## Define your schema
Jazz uses Zod for more simple data types (like strings, numbers, booleans), and its own schemas to create collaborative data structures known as CoValues. CoValues are automatically persisted across your devices and the cloud and synced in real-time. Here we're defining a schema made up of both Zod types and CoValues.
Adding a `root` to the user's account gives us a container that can be used to keep a track of all the data a user might need to use the app. The migration runs when the user logs in, and ensures the account is properly set up before we try to use it.
\--- Section applies only to react ---
**File name: app/schema.ts**
\--- End of react specific section ---
\--- Section applies only to svelte ---
**File name: src/lib/schema.ts**
\--- End of svelte specific section ---
```ts
import { co, z } from "jazz-tools";
export const Band = co.map({
name: z.string(), // Zod primitive type
});
export const Festival = co.list(Band);
export const JazzFestAccountRoot = co.map({
myFestival: Festival,
});
export const JazzFestAccount = co
.account({
root: JazzFestAccountRoot,
profile: co.profile(),
})
.withMigration((account) => {
if (!account.$jazz.has("root")) {
account.$jazz.set("root", {
myFestival: [],
});
}
});
```
## Add the Jazz Provider
Wrap your app with a provider so components can use Jazz.
\--- Section applies only to react ---
**File name: app/components/JazzWrapper.tsx**
```tsx
"use client"; // tells Next.js that this component can't be server-side rendered. If you're not using Next.js, you can remove it.
import { JazzReactProvider } from "jazz-tools/react";
import { JazzFestAccount } from "@/app/schema";
const apiKey = process.env.NEXT_PUBLIC_JAZZ_API_KEY;
export function JazzWrapper({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
\--- End of react specific section ---
\--- Section applies only to react ---
**File name: app/layout.tsx**
\--- End of react specific section ---
\--- Section applies only to svelte ---
**File name: src/routes/+layout.svelte**
\--- End of svelte specific section ---
##### React:
```tsx
import { JazzWrapper } from "@/app/components/JazzWrapper";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
{children}
);
}
```
##### Svelte:
```svelte
{@render children?.()}
```
## Start your app
Moment of truth — time to start your app and see if it works.
##### npm:
```bash
npm run dev
```
##### pnpm:
```bash
pnpm run dev
```
\--- Section applies only to react ---
If everything's going according to plan, you should see the default Next.js welcome page!
\--- End of react specific section ---
\--- Section applies only to svelte ---
If everything's going according to plan, you should see the default SvelteKit welcome page!
\--- End of svelte specific section ---
### Not loading?
If you're not seeing the welcome page:
\--- Section applies only to react ---
* Check you wrapped your app with the Jazz Provider in `app/layout.tsx`
* Check your schema is properly defined in `app/schema.ts`
\--- End of react specific section ---
\--- Section applies only to svelte ---
* Check you wrapped your app with the Jazz Provider in `src/routes/+layout.svelte`
* Check your schema is properly defined in `src/lib/schema.ts`
\--- End of svelte specific section ---
**Info: Still stuck?** Ask for help on [Discord](https://discord.gg/utDMjHYg42)!
## Create data
Let's create a simple form to add a new band to the festival. We'll use the `useAccount` hook to get the current account and tell Jazz to load the `myFestival` CoValue by passing a `resolve` query.
\--- Section applies only to react ---
**File name: app/components/NewBand.tsx**
\--- End of react specific section ---
\--- Section applies only to svelte ---
**File name: src/lib/components/NewBand.svelte**
\--- End of svelte specific section ---
##### React:
```tsx
"use client";
import { useAccount } from "jazz-tools/react";
import { JazzFestAccount } from "@/app/schema";
import { useState } from "react";
export function NewBand() {
const me = useAccount(JazzFestAccount, {
resolve: { root: { myFestival: true } },
});
const [name, setName] = useState("");
const handleSave = () => {
if (!me.$isLoaded) return;
me.root.myFestival.$jazz.push({ name });
setName("");
};
return (
setName(e.target.value)}
/>
);
}
```
##### Svelte:
```svelte
```
## Display your data
Now we've got a way to create data, so let's add a component to display it.
\--- Section applies only to react ---
**File name: app/components/Festival.tsx**
\--- End of react specific section ---
\--- Section applies only to svelte ---
**File name: src/lib/components/Festival.svelte**
\--- End of svelte specific section ---
##### React:
```tsx
"use client";
import { useAccount } from "jazz-tools/react";
import { JazzFestAccount } from "@/app/schema";
export function Festival() {
const me = useAccount(JazzFestAccount, {
resolve: {
root: {
myFestival: {
$each: true
}
}
},
});
if (!me.$isLoaded) return null;
return (
{me.root.myFestival.map(
(band) => band &&
{band.name}
,
)}
);
}
```
##### Svelte:
```svelte
{#if me.current.$isLoaded}
{#each me.current.root.myFestival as band}
{band.name}
{/each}
{/if}
```
## Put it all together
You've built all your components, time to put them together.
\--- Section applies only to react ---
**File name: app/page.tsx**
\--- End of react specific section ---
\--- Section applies only to svelte ---
**File name: src/routes/+page.svelte**
\--- End of svelte specific section ---
##### React:
```tsx
import { Festival } from "@/app/components/Festival";
import { NewBand } from "@/app/components/NewBand";
export default function Home() {
return (
🎪 My Festival
);
}
```
##### Svelte:
```svelte
🎪 My Festival
```
You should now be able to add a band to your festival, and see it appear in the list!
**Congratulations! 🎉** You've built your first Jazz app!
You've begun to scratch the surface of what's possible with Jazz. Behind the scenes, your local-first JazzFest app is **already** securely syncing your data to the cloud in real-time, ready for you to build more and more powerful features.
\--- Section applies only to react ---
Psst! Got a few more minutes and want to add Server Side Rendering to your app? [We've got you covered!](/docs/server-side/ssr)
\--- End of react specific section ---
## Next steps
* [Add authentication](/docs/key-features/authentication/quickstart) to your app so that you can log in and view your data wherever you are!
* Dive deeper into the collaborative data structures we call [CoValues](/docs/core-concepts/covalues/overview)
* Learn how to share and [collaborate on data](/docs/permissions-and-sharing/overview) using groups and permissions
* Complete the [server-side quickstart](/docs/server-side/quickstart) to learn more about Jazz on the server
### Installation
# Providers
\--- Section applies only to react ---
`` is the core component that connects your React application to Jazz. It handles:
\--- End of react specific section ---
\--- Section applies only to svelte ---
`` is the core component that connects your Svelte application to Jazz. It handles:
\--- End of svelte specific section ---
\--- Section applies only to react-native ---
`` is the core component that connects your React Native application to Jazz. It handles:
\--- End of react-native specific section ---
\--- Section applies only to expo ---
`` is the core component that connects your Expo application to Jazz. It handles:
\--- End of expo specific section ---
* **Data Synchronization**: Manages connections to peers and the Jazz cloud
* **Local Storage**: Persists data locally between app sessions
* **Schema Types**: Provides APIs for the [AccountSchema](/docs/core-concepts/schemas/accounts-and-migrations)
* **Authentication**: Connects your authentication system to Jazz
\--- Section applies only to react ---
Our [Chat example app](https://jazz.tools/examples#chat) provides a complete implementation of JazzReactProvider with authentication and real-time data sync.
\--- End of react specific section ---
\--- Section applies only to svelte ---
Our [File Share example app](https://github.com/garden-co/jazz/blob/main/examples/file-share-svelte/src/routes/%2Blayout.svelte) provides an implementation of JazzSvelteProvider with authentication and real-time data sync.
\--- End of svelte specific section ---
## Setting up the Provider
The provider accepts several configuration options:
##### React:
```tsx
import { JazzReactProvider } from "jazz-tools/react";
import { MyAppAccount } from "./schema";
export function MyApp({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
##### Svelte:
```svelte
{@render children()}
```
##### React Native:
```tsx
import { JazzReactNativeProvider } from "jazz-tools/react-native";
import { MyAppAccount } from "./schema";
export function MyJazzProvider({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
##### Expo:
```tsx
import { JazzExpoProvider } from "jazz-tools/expo";
import { MyAppAccount } from "./schema";
export function MyJazzProvider({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
**Info: Tip**
Sign up for a free API key at [dashboard.jazz.tools](https://dashboard.jazz.tools) for higher limits or production use, or use your email address as a temporary key to get started quickly.
\--- Section applies only to react ---
**File name: .env**
\--- End of react specific section ---
\--- Section applies only to svelte ---
**File name: .env**
\--- End of svelte specific section ---
##### React:
```bash
NEXT_PUBLIC_JAZZ_API_KEY="you@example.com" # or your API key
```
##### Svelte:
```bash
PUBLIC_JAZZ_API_KEY="you@example.com" # or your API key
```
## Provider Options
### Sync Options
The `sync` property configures how your application connects to the Jazz network:
```ts
import { type SyncConfig } from "jazz-tools";
export const syncConfig: SyncConfig = {
// Connection to Jazz Cloud or your own sync server
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
// When to sync: "always" (default), "never", or "signedUp"
when: "always",
};
```
\--- Section applies only to react-native-expo ---
**Warning: iOS Credential Persistence**
When using `sync: 'never'` or `sync: 'signedUp'`, like all other data, the user's account exists only on their device, and is deleted if the user uninstalls your app. On iOS though, login credentials are saved to the Keychain, and are not deleted when the app is uninstalled.
If a user reinstalls your app, Jazz will try to re-use these credentials to sign in to an account that no longer exists, which will cause errors.
To avoid this, consider using `sync: 'always'` for your iOS users, or let them know they'll need to remove their credentials from Keychain before reinstalling.
\--- End of react-native-expo specific section ---
See [Authentication States](/docs/key-features/authentication/authentication-states#controlling-sync-for-different-authentication-states) for more details on how the `when` property affects synchronization based on authentication state.
### Account Schema
The `AccountSchema` property defines your application's account structure:
##### React:
```tsx
import { JazzReactProvider } from "jazz-tools/react";
import { MyAppAccount } from "./schema";
export function MyApp({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
##### Svelte:
```svelte
{@render children()}
```
##### React Native:
```tsx
import { JazzReactNativeProvider } from "jazz-tools/react-native";
import { MyAppAccount } from "./schema";
export function MyJazzProvider({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
##### Expo:
```tsx
import { JazzExpoProvider } from "jazz-tools/expo";
import { MyAppAccount } from "./schema";
export function MyJazzProvider({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
### Additional Options
The provider accepts these additional options:
\--- Section applies only to react-native ---
* `kvStore`
* `MMKVStoreAdapter` (default)
* `AccountSchema`
* `Account` (default)
* `CryptoProvider`
* `PureJSCrypto` (default) - Pure JavaScript crypto provider
* `RNQuickCrypto` \- C++ accelerated crypto provider
* `RNCrypto` \- Native crypto provider
\--- End of react-native specific section ---
\--- Section applies only to react-native-expo ---
* `kvStore`
* `ExpoSecureStoreAdapter` (default)
* `AccountSchema`
* `Account` (default)
* `CryptoProvider`
* `PureJSCrypto` (default) - Pure JavaScript crypto provider
* `RNQuickCrypto` \- C++ accelerated crypto provider
* `RNCrypto` \- Native crypto provider
\--- End of react-native-expo specific section ---
\--- Section applies only to react,svelte ---
##### React:
```tsx
export function MyApp({ children }: { children: React.ReactNode }) {
return (
{
console.log("User logged out");
}}
// Handle anonymous account data when user logs in to existing account
onAnonymousAccountDiscarded={(account) => {
console.log("Anonymous account discarded", account.$jazz.id);
// Migrate data here
return Promise.resolve();
}}
>
{children}
);
}
```
##### Svelte:
```svelte
{@render children()}
```
See [Authentication States](/docs/key-features/authentication/authentication-states) for more information on authentication states, guest mode, and handling anonymous accounts.
\--- End of react,svelte specific section ---
## Authentication
\--- Section applies only to react,svelte ---
The Jazz Provider works with various authentication methods to enable users to access their data across multiple devices. For a complete guide to authentication, see our [Authentication Overview](/docs/key-features/authentication/overview).
\--- End of react,svelte specific section ---
\--- Section applies only to react-native ---
The Provider works with various authentication methods, with PassphraseAuth being the easiest way to get started for development and testing. For authentication details, refer to our [Authentication Overview](/docs/key-features/authentication/overview) guide.
The authentication hooks must always be used inside the Provider component.
Implementing PassphraseAuth is straightforward:
1. Import the [wordlist](https://github.com/bitcoinjs/bip39/tree/a7ecbfe2e60d0214ce17163d610cad9f7b23140c/src/wordlists) for generating recovery phrases
2. Use the `usePassphraseAuth` hook to handle authentication
3. Create simple registration and sign-in screens
##### React Native:
```tsx
import {
JazzReactNativeProvider,
usePassphraseAuth,
} from "jazz-tools/react-native";
import { englishWordlist } from "./wordlist";
function JazzAuthentication({ children }: { children: ReactNode }) {
const auth = usePassphraseAuth({
wordlist: englishWordlist,
});
// If the user is already signed in, render the App
if (auth.state === "signedIn") {
return children;
}
// Otherwise, show a sign-in screen
return ;
}
function AuthenticatedProvider({ children }: { children: ReactNode }) {
return (
{children}
);
}
```
##### Expo:
```tsx
import { JazzExpoProvider, usePassphraseAuth } from "jazz-tools/expo";
import { englishWordlist } from "./wordlist";
function JazzAuthentication({ children }: { children: ReactNode }) {
const auth = usePassphraseAuth({
wordlist: englishWordlist,
});
// If the user is already signed in, render the App
if (auth.state === "signedIn") {
return children;
}
// Otherwise, show a sign-in screen
return ;
}
function AuthenticatedProvider({ children }: { children: ReactNode }) {
return (
{children}
);
}
```
## Local Persistence \[!framework=react-native,expo\]
\--- End of react-native specific section ---
\--- Section applies only to react-native ---
Jazz for React Native includes built-in local persistence using SQLite. This implementation uses:
* **Database Storage**: `@op-engineering/op-sqlite` \- A high-performance SQLite implementation
* **Key-Value Storage**: `react-native-mmkv` \- A fast key-value storage system
\--- End of react-native specific section ---
\--- Section applies only to react-native-expo ---
Jazz for Expo includes built-in local persistence using SQLite. Following Expo's best practices, the Expo implementation uses:
* **Database Storage**: `expo-sqlite` \- Expo's official SQLite module
* **Key-Value Storage**: `expo-secure-store` \- Expo's secure storage system
\--- End of react-native-expo specific section ---
\--- Section applies only to react-native,react-native-expo ---
Local persistence is enabled by default with no additional configuration required. Your data will automatically persist across app restarts.
\--- End of react-native,react-native-expo specific section ---
\--- Section applies only to react-native,react-native-expo ---
## RNCrypto
For accelerated crypto operations, you can use the `RNCrypto` crypto provider. It is the most performant crypto provider available for React Native and Expo.
To use it, install the following package:
```bash
pnpm add cojson-core-rn
```
**Pay Attention:** The version of `cojson-core-rn` must be the same as the version of `jazz-tools`.
```json
"dependencies": {
"cojson-core-rn": "x.x.x", # same version as jazz-tools
"jazz-tools": "x.x.x" # same version as cojson-core-rn
}
```
Then add the following to your provider:
##### React Native:
```tsx
import { JazzReactNativeProvider } from "jazz-tools/react-native";
import { RNCrypto } from "jazz-tools/react-native-core/crypto/RNCrypto";
function MyJazzProvider({ children }: { children: ReactNode }) {
return (
{children}
);
}
```
##### Expo:
```tsx
import { JazzExpoProvider } from "jazz-tools/expo";
import { RNCrypto } from "jazz-tools/react-native-core/crypto/RNCrypto";
function MyJazzProvider({ children }: { children: ReactNode }) {
return (
{children}
);
}
```
\--- End of react-native,react-native-expo specific section ---
\--- Section applies only to react-native,react-native-expo ---
## Quick Crypto
For accelerated crypto operations, you can use the `RNQuickCrypto` crypto provider.
To use it, install the following Packages:
```bash
pnpm add react-native-quick-crypto@1.0.0-beta.18 react-native-nitro-modules react-native-fast-encoder
```
Then add the following to your provider:
##### React Native:
```tsx
import { JazzReactNativeProvider } from "jazz-tools/react-native";
import { RNQuickCrypto } from "jazz-tools/react-native/crypto";
function MyJazzProvider({ children }: { children: ReactNode }) {
return (
{children}
);
}
```
##### Expo:
```tsx
import { JazzExpoProvider } from "jazz-tools/expo";
import { RNQuickCrypto } from "jazz-tools/expo/crypto";
function MyJazzProvider({ children }: { children: ReactNode }) {
return (
{children}
);
}
```
\--- End of react-native,react-native-expo specific section ---
\--- Section applies only to react-native ---
For configuration, add the following settings:
```ruby
# ios/Podfile
ENV['SODIUM_ENABLED'] = '1'
```
and
```groovy
// android/gradle.properties
sodiumEnabled=true
```
\--- End of react-native specific section ---
\--- Section applies only to react-native-expo ---
For configuration, use the RNQC Expo config plugin:
```json
{
"expo": {
"plugins": [
[
"react-native-quick-crypto",
{
"sodiumEnabled": true
}
]
]
}
}
```
\--- End of react-native-expo specific section ---
## Need Help?
If you have questions about configuring the Jazz Provider for your specific use case, [join our Discord community](https://discord.gg/utDMjHYg42) for help.
### Troubleshooting
# Setup troubleshooting
A few reported setup hiccups and how to fix them.
---
## Node.js version requirements
Jazz requires **Node.js v20 or later** due to native module dependencies.
Check your version:
```sh
node -v
```
If you’re on Node 18 or earlier, upgrade via nvm:
```sh
nvm install 20
nvm use 20
```
---
### Required TypeScript Configuration
In order to build successfully with TypeScript, you must ensure that you have the following options configured (either in your `tsconfig.json` or using the command line):
* `skipLibCheck` must be `true`
* `exactOptionalPropertyTypes` must be `false`
---
## npx jazz-run: command not found
If, when running:
```sh
npx jazz-run sync
```
you encounter:
```sh
sh: jazz-run: command not found
```
This is often due to an npx cache quirk. (For most apps using Jazz)
1. Clear your npx cache:
```sh
npx clear-npx-cache
```
1. Rerun the command:
```sh
npx jazz-run sync
```
---
### Node 18 workaround (rebuilding the native module)
If you can’t upgrade to Node 20+, you can rebuild the native `better-sqlite3` module for your architecture.
1. Install `jazz-run` locally in your project:
```sh
pnpm add -D jazz-run
```
1. Find the installed version of better-sqlite3 inside node\_modules. It should look like this:
```sh
./node_modules/.pnpm/better-sqlite3{version}/node_modules/better-sqlite3
```
Replace `{version}` with your installed version and run:
```sh
# Navigate to the installed module and rebuild
pushd ./node_modules/.pnpm/better-sqlite3{version}/node_modules/better-sqlite3
&& pnpm install
&& popd
```
If you get ModuleNotFoundError: No module named 'distutils': Linux:
```sh
pip install --upgrade setuptools
```
macOS:
```sh
brew install python-setuptools
```
_Workaround originally shared by @aheissenberger on Jun 24, 2025._
---
### Still having trouble?
If none of the above fixes work:
Make sure dependencies installed without errors (`pnpm install`).
Double-check your `node -v` output matches the required version.
Open an issue on GitHub with:
* Your OS and version
* Node.js version
* Steps you ran and full error output
We're always happy to help! If you're stuck, reachout via [Discord](https://discord.gg/utDMjHYg42)
## Upgrade guides
### 0.19.0 - Explicit loading states
### 0.18.0 - New `$jazz` field in CoValues
### 0.17.0 - New image APIs
## Core Concepts
### Overview
# Defining schemas: CoValues
**CoValues ("Collaborative Values") are the core abstraction of Jazz.** They're your bread-and-butter datastructures that you use to represent everything in your app.
As their name suggests, CoValues are inherently collaborative, meaning **multiple users and devices can edit them at the same time.**
**Think of CoValues as "super-fast Git for lots of tiny data."**
* CoValues keep their full edit histories, from which they derive their "current state".
* The fact that this happens in an eventually-consistent way makes them [CRDTs](https://en.wikipedia.org/wiki/Conflict-free%5Freplicated%5Fdata%5Ftype).
* Having the full history also means that you often don't need explicit timestamps and author info - you get this for free as part of a CoValue's [edit metadata](/docs/key-features/history).
CoValues model JSON with CoMaps and CoLists, but also offer CoFeeds for simple per-user value feeds, and let you represent binary data with FileStreams.
## Start your app with a schema
Fundamentally, CoValues are as dynamic and flexible as JSON, but in Jazz you use them by defining fixed schemas to describe the shape of data in your app.
This helps correctness and development speed, but is particularly important...
* when you evolve your app and need migrations
* when different clients and server workers collaborate on CoValues and need to make compatible changes
Thinking about the shape of your data is also a great first step to model your app.
Even before you know the details of how your app will work, you'll probably know which kinds of objects it will deal with, and how they relate to each other.
In Jazz, you define schemas using `co` for CoValues and `z` (from [Zod](https://zod.dev/)) for their primitive fields.
**File name: schema.ts**
```ts
import { co, z } from "jazz-tools";
export const TodoProject = co.map({
title: z.string(),
tasks: ListOfTasks,
});
```
This gives us schema info that is available for type inference _and_ at runtime.
Check out the inferred type of `project` in the example below, as well as the input `.create()` expects.
```ts
import { Group } from "jazz-tools";
import { TodoProject, ListOfTasks } from "./schema";
const project = TodoProject.create(
{
title: "New Project",
tasks: ListOfTasks.create([], Group.create()),
},
Group.create(),
);
```
When creating CoValues that contain other CoValues, you can pass in a plain JSON object. Jazz will automatically create the CoValues for you.
```ts
const group = Group.create().makePublic();
const publicProject = TodoProject.create(
{
title: "New Project",
tasks: [], // Permissions are inherited, so the tasks list will also be public
},
group,
);
```
**Info:**
To learn more about how permissions work when creating nested CoValues with plain JSON objects, refer to [Ownership on implicit CoValue creation](/docs/permissions-and-sharing/cascading-permissions#ownership-on-implicit-covalue-creation).
## Types of CoValues
### `CoMap` (declaration)
CoMaps are the most commonly used type of CoValue. They are the equivalent of JSON objects (Collaborative editing follows a last-write-wins strategy per-key).
You can either declare struct-like CoMaps:
```ts
export const Task = co.map({
title: z.string(),
completed: z.boolean(),
});
```
Or record-like CoMaps (key-value pairs, where keys are always `string`):
```ts
export const ColourToHex = co.record(z.string(), z.string());
export const ColorToFruit = co.record(z.string(), Fruit);
```
See the corresponding sections for [creating](/docs/core-concepts/covalues/comaps#creating-comaps),[subscribing/loading](/docs/core-concepts/subscription-and-loading),[reading from](/docs/core-concepts/covalues/comaps#reading-from-comaps) and[updating](/docs/core-concepts/covalues/comaps#updating-comaps) CoMaps.
### `CoList` (declaration)
CoLists are ordered lists and are the equivalent of JSON arrays. (They support concurrent insertions and deletions, maintaining a consistent order.)
You define them by specifying the type of the items they contain:
```ts
export const ListOfColors = co.list(z.string());
export const ListOfTasks = co.list(Task);
```
See the corresponding sections for [creating](/docs/core-concepts/covalues/colists#creating-colists),[subscribing/loading](/docs/core-concepts/subscription-and-loading),[reading from](/docs/core-concepts/covalues/colists#reading-from-colists) and[updating](/docs/core-concepts/covalues/colists#updating-colists) CoLists.
### `CoFeed` (declaration)
CoFeeds are a special CoValue type that represent a feed of values for a set of users/sessions (Each session of a user gets its own append-only feed).
They allow easy access of the latest or all items belonging to a user or their sessions. This makes them particularly useful for user presence, reactions, notifications, etc.
You define them by specifying the type of feed item:
```ts
export const FeedOfTasks = co.feed(Task);
```
See the corresponding sections for [creating](/docs/core-concepts/covalues/overview#creating-cofeeds),[subscribing/loading](/docs/core-concepts/subscription-and-loading),[reading from](/docs/core-concepts/covalues/cofeeds#reading-from-cofeeds) and[writing to](/docs/core-concepts/covalues/cofeeds#writing-to-cofeeds) CoFeeds.
### `FileStream` (declaration)
FileStreams are a special type of CoValue that represent binary data. (They are created by a single user and offer no internal collaboration.)
They allow you to upload and reference files.
You typically don't need to declare or extend them yourself, you simply refer to the built-in `co.fileStream()` from another CoValue:
```ts
export const Document = co.map({
title: z.string(),
file: co.fileStream(),
});
```
See the corresponding sections for [creating](/docs/core-concepts/covalues/filestreams#creating-filestreams),[subscribing/loading](/docs/core-concepts/subscription-and-loading),[reading from](/docs/core-concepts/covalues/filestreams#reading-from-filestreams) and[writing to](/docs/core-concepts/covalues/filestreams#writing-to-filestreams) FileStreams.
**Note: For images, we have a special, higher-level `co.image()` helper, see [ImageDefinition](/docs/core-concepts/covalues/imagedef).**
### Unions of CoMaps (declaration)
You can declare unions of CoMaps that have discriminating fields, using `co.discriminatedUnion()`.
```ts
export const ButtonWidget = co.map({
type: z.literal("button"),
label: z.string(),
});
export const SliderWidget = co.map({
type: z.literal("slider"),
min: z.number(),
max: z.number(),
});
export const WidgetUnion = co.discriminatedUnion("type", [
ButtonWidget,
SliderWidget,
]);
```
See the corresponding sections for [creating](/docs/core-concepts/schemas/schemaunions#creating-schema-unions),[subscribing/loading](/docs/core-concepts/subscription-and-loading) and[narrowing](/docs/core-concepts/schemas/schemaunions#narrowing-unions) schema unions.
## CoValue field/item types
Now that we've seen the different types of CoValues, let's see more precisely how we declare the fields or items they contain.
### Primitive fields
You can declare primitive field types using `z` (re-exported in `jazz-tools` from [Zod](https://zod.dev/)).
Here's a quick overview of the primitive types you can use:
```ts
z.string(); // For simple strings
z.number(); // For numbers
z.boolean(); // For booleans
z.date(); // For dates
z.literal(["waiting", "ready"]); // For enums
```
Finally, for more complex JSON data, that you _don't want to be collaborative internally_ (but only ever update as a whole), you can use more complex Zod types.
For example, you can use `z.object()` to represent an internally immutable position:
```ts
const Sprite = co.map({
// assigned as a whole
position: z.object({ x: z.number(), y: z.number() }),
});
```
Or you could use a `z.tuple()`:
```ts
const SpriteWithTuple = co.map({
// assigned as a whole
position: z.tuple([z.number(), z.number()]),
});
```
### References to other CoValues
To represent complex structured data with Jazz, you form trees or graphs of CoValues that reference each other.
Internally, this is represented by storing the IDs of the referenced CoValues in the corresponding fields, but Jazz abstracts this away, making it look like nested CoValues you can get or assign/insert.
The important caveat here is that **a referenced CoValue might or might not be loaded yet,** but we'll see what exactly that means in [Subscribing and Deep Loading](/docs/core-concepts/subscription-and-loading).
In Schemas, you declare references by just using the schema of the referenced CoValue:
```ts
const Person = co.map({
name: z.string(),
});
const ListOfPeople = co.list(Person);
const Company = co.map({
members: ListOfPeople,
});
```
#### Optional References
You can make schema fields optional using either `z.optional()` or `co.optional()`, depending on the type of value:
* Use `z.optional()` for primitive Zod values like `z.string()`, `z.number()`, or `z.boolean()`
* Use `co.optional()` for CoValues like `co.map()`, `co.list()`, or `co.record()`
You can make references optional with `co.optional()`:
```ts
const PersonWithOptionalProperties = co.map({
age: z.optional(z.number()), // primitive
pet: co.optional(Pet), // CoValue
});
```
#### Recursive References
You can wrap references in getters. This allows you to defer evaluation until the property is accessed. This technique is particularly useful for defining circular references, including recursive (self-referencing) schemas, or mutually recursive schemas.
```ts
const SelfReferencingPerson = co.map({
name: z.string(),
get bestFriend() {
return Person;
},
});
```
You can use the same technique for mutually recursive references:
```ts
const MutuallyRecursivePerson = co.map({
name: z.string(),
get friends() {
return ListOfFriends;
},
});
const ListOfFriends = co.list(Person);
```
If you try to reference `ListOfPeople` in `Person` without using a getter, you'll run into a `ReferenceError` because of the [temporal dead zone](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let#temporal%5Fdead%5Fzone%5Ftdz).
### Helper methods
If you find yourself repeating the same logic to access computed CoValues properties, you can define helper functions to encapsulate it for better reusability:
```ts
const Person = co.map({
firstName: z.string(),
lastName: z.string(),
dateOfBirth: z.date(),
});
type Person = co.loaded;
export function getPersonFullName(person: Person) {
return `${person.firstName} ${person.lastName}`;
}
function differenceInYears(date1: Date, date2: Date) {
const diffTime = Math.abs(date1.getTime() - date2.getTime());
return Math.ceil(diffTime / (1000 * 60 * 60 * 24 * 365.25));
}
export function getPersonAgeAsOf(person: Person, date: Date) {
return differenceInYears(date, person.dateOfBirth);
}
const person = Person.create({
firstName: "John",
lastName: "Doe",
dateOfBirth: new Date("1990-01-01"),
});
const fullName = getPersonFullName(person);
const age = getPersonAgeAsOf(person, new Date());
```
Similarly, you can encapsulate logic needed to update CoValues:
```ts
export function updatePersonName(person: Person, fullName: string) {
const [firstName, lastName] = fullName.split(" ");
person.$jazz.set("firstName", firstName);
person.$jazz.set("lastName", lastName);
}
console.log(person.firstName, person.lastName); // John Doe
updatePersonName(person, "Jane Doe");
console.log(person.firstName, person.lastName); // Jane Doe
```
### CoMaps
# CoMaps
CoMaps are key-value objects that work like JavaScript objects. You can access properties with dot notation and define typed fields that provide TypeScript safety. They're ideal for structured data that needs type validation.
## Creating CoMaps
CoMaps are typically defined with `co.map()` and specifying primitive fields using `z` (see [Defining schemas: CoValues](/docs/core-concepts/covalues/overview) for more details on primitive fields):
```ts
import { co, z } from "jazz-tools";
const Project = co.map({
name: z.string(),
startDate: z.date(),
status: z.literal(["planning", "active", "completed"]),
coordinator: co.optional(Member),
});
export type Project = co.loaded;
export type ProjectInitShape = co.input; // type accepted by `Project.create`
```
You can create either struct-like CoMaps with fixed fields (as above) or record-like CoMaps for key-value pairs:
```ts
const Inventory = co.record(z.string(), z.number());
```
To instantiate a CoMap:
```ts
const project = Project.create({
name: "Spring Planting",
startDate: new Date("2025-03-15"),
status: "planning",
});
const inventory = Inventory.create({
tomatoes: 48,
basil: 12,
});
```
### Ownership
When creating CoMaps, you can specify ownership to control access:
```ts
// Create with default owner (current user)
const privateProject = Project.create({
name: "My Herb Garden",
startDate: new Date("2025-04-01"),
status: "planning",
});
// Create with shared ownership
const gardenGroup = Group.create();
gardenGroup.addMember(memberAccount, "writer");
const communityProject = Project.create(
{
name: "Community Vegetable Plot",
startDate: new Date("2025-03-20"),
status: "planning",
},
{ owner: gardenGroup },
);
```
See [Groups as permission scopes](/docs/permissions-and-sharing/overview) for more information on how to use groups to control access to CoMaps.
## Reading from CoMaps
CoMaps can be accessed using familiar JavaScript object notation:
```ts
console.log(project.name); // "Spring Planting"
console.log(project.status); // "planning"
```
### Handling Optional Fields
Optional fields require checks before access:
```ts
if (project.coordinator) {
console.log(project.coordinator.name); // Safe access
}
```
### Recursive references
You can wrap references in getters. This allows you to defer evaluation until the property is accessed. This technique is particularly useful for defining circular references, including recursive (self-referencing) schemas, or mutually recursive schemas.
```ts
import { co, z } from "jazz-tools";
const Project = co.map({
name: z.string(),
startDate: z.date(),
status: z.literal(["planning", "active", "completed"]),
coordinator: co.optional(Member),
get subProject() {
return Project.optional();
},
});
export type Project = co.loaded;
```
When the recursive references involve more complex types, it is sometimes required to specify the getter return type:
```ts
const ProjectWithTypedGetter = co.map({
name: z.string(),
startDate: z.date(),
status: z.literal(["planning", "active", "completed"]),
coordinator: co.optional(Member),
// [!code ++:3]
get subProjects(): co.Optional> {
return co.optional(co.list(Project));
},
});
export type Project = co.loaded;
```
### Partial
For convenience Jazz provies a dedicated API for making all the properties of a CoMap optional:
```ts
const Project = co.map({
name: z.string(),
startDate: z.date(),
status: z.literal(["planning", "active", "completed"]),
});
const ProjectDraft = Project.partial();
// The fields are all optional now
const project = ProjectDraft.create({});
```
### Pick
You can also pick specific fields from a CoMap:
```ts
const Project = co.map({
name: z.string(),
startDate: z.date(),
status: z.literal(["planning", "active", "completed"]),
});
const ProjectStep1 = Project.pick({
name: true,
startDate: true,
});
// We don't provide the status field
const project = ProjectStep1.create({
name: "My project",
startDate: new Date("2025-04-01"),
});
```
### Working with Record CoMaps
For record-type CoMaps, you can access values using bracket notation:
```ts
const inventory = Inventory.create({
tomatoes: 48,
peppers: 24,
basil: 12,
});
console.log(inventory["tomatoes"]); // 48
```
## Updating CoMaps
To update a CoMap's properties, use the `$jazz.set` method:
```ts
project.$jazz.set("name", "Spring Vegetable Garden"); // Update name
project.$jazz.set("startDate", new Date("2025-03-20")); // Update date
```
**Info:**
The `$jazz` namespace is available on all CoValues, and provides access to methods to modify and load CoValues, as well as access common properties like `id` and `owner`.
When updating references to other CoValues, you can provide both the new CoValue or a JSON object from which the new CoValue will be created.
```ts
const Dog = co.map({
name: co.plainText(),
});
const Person = co.map({
name: co.plainText(),
dog: Dog,
});
const person = Person.create({
name: "John",
dog: { name: "Rex" },
});
// Update the dog field using a CoValue
person.$jazz.set("dog", Dog.create({ name: co.plainText().create("Fido") }));
// Or use a plain JSON object
person.$jazz.set("dog", { name: "Fido" });
```
When providing a JSON object, Jazz will automatically create the CoValues for you. To learn more about how permissions work in this case, refer to[Ownership on implicit CoValue creation](/docs/permissions-and-sharing/cascading-permissions#ownership-on-implicit-covalue-creation).
### Type Safety
CoMaps are fully typed in TypeScript, giving you autocomplete and error checking:
```ts
project.$jazz.set("name", "Spring Vegetable Planting"); // ✓ Valid string
// [!code --]
project.$jazz.set("startDate", "2025-03-15"); // ✗ Type error: expected Date
// [!code --]
// Argument of type 'string' is not assignable to parameter of type 'Date'
```
### Soft Deletion
Implementing a soft deletion pattern by using a `deleted` flag allows you to maintain data for potential recovery and auditing.
```ts
const Project = co.map({
name: z.string(),
// [!code ++]
deleted: z.optional(z.boolean()),
});
```
When an object needs to be "deleted", instead of removing it from the system, the deleted flag is set to true. This gives us a property to omit it in the future.
### Deleting Properties
You can delete properties from CoMaps:
```ts
inventory.$jazz.delete("basil"); // Remove a key-value pair
// For optional fields in struct-like CoMaps
project.$jazz.set("coordinator", undefined); // Remove the reference
```
## Running migrations on CoMaps
Migrations are functions that run when a CoMap is loaded, allowing you to update existing data to match new schema versions. Use them when you need to modify the structure of CoMaps that already exist in your app. Unlike [Account migrations](/docs/core-concepts/schemas/accounts-and-migrations#when-migrations-run), CoMap migrations are not run when a CoMap is created.
**Note:** Migrations are run synchronously and cannot be run asynchronously.
Here's an example of a migration that adds the `priority` field to the `Task` CoMap:
```ts
const Task = co
.map({
done: z.boolean(),
text: co.plainText(),
version: z.literal([1, 2]),
priority: z.enum(["low", "medium", "high"]), // new field
})
.withMigration((task) => {
if (task.version === 1) {
task.$jazz.set("priority", "medium");
// Upgrade the version so the migration won't run again
task.$jazz.set("version", 2);
}
});
```
### Migration best practices
Design your schema changes to be compatible with existing data:
* **Add, don't change:** Only add new fields; avoid renaming or changing types of existing fields
* **Make new fields optional:** This prevents errors when loading older data
* **Use version fields:** Track schema versions to run migrations only when needed
### Migration & reader permissions
Migrations need write access to modify CoMaps. If some users only have read permissions, they can't run migrations on those CoMaps.
**Forward-compatible schemas** (where new fields are optional) handle this gracefully - users can still use the app even if migrations haven't run.
**Non-compatible changes** require handling both schema versions in your app code using discriminated unions.
When you can't guarantee all users can run migrations, handle multiple schema versions explicitly:
```ts
const TaskV1 = co.map({
version: z.literal(1),
done: z.boolean(),
text: z.string(),
});
const TaskV2 = co
.map({
// We need to be more strict about the version to make the
// discriminated union work
version: z.literal(2),
done: z.boolean(),
text: z.string(),
priority: z.enum(["low", "medium", "high"]),
})
.withMigration((task) => {
if (task.version === 1) {
task.$jazz.set("version", 2);
task.$jazz.set("priority", "medium");
}
});
// Export the discriminated union; because some users might
// not be able to run the migration
export const Task = co.discriminatedUnion("version", [TaskV1, TaskV2]);
export type Task = co.loaded;
```
## Best Practices
### Structuring Data
* Use struct-like CoMaps for entities with fixed, known properties
* Use record-like CoMaps for dynamic key-value collections
* Group related properties into nested CoMaps for better organization
### Common Patterns
#### Helper methods
You should define helper methods of CoValue schemas separately, in standalone functions:
```ts
import { co, z } from "jazz-tools";
const Project = co.map({
name: z.string(),
startDate: z.date(),
endDate: z.optional(z.date()),
});
type Project = co.loaded;
export function isProjectActive(project: Project) {
const now = new Date();
return (
now >= project.startDate && (!project.endDate || now <= project.endDate)
);
}
export function formatProjectDuration(
project: Project,
format: "short" | "full",
) {
const start = project.startDate.toLocaleDateString();
if (!project.endDate) {
return format === "full" ? `Started on ${start}, ongoing` : `From ${start}`;
}
const end = project.endDate.toLocaleDateString();
return format === "full"
? `From ${start} to ${end}`
: `${(project.endDate.getTime() - project.startDate.getTime()) / 86400000} days`;
}
const project = Project.create({
name: "My project",
startDate: new Date("2025-04-01"),
endDate: new Date("2025-04-04"),
});
console.log(isProjectActive(project)); // false
console.log(formatProjectDuration(project, "short")); // "3 days"
```
#### Uniqueness
CoMaps are typically created with a CoValue ID that acts as an opaque UUID, by which you can then load them. However, there are situations where it is preferable to load CoMaps using a custom identifier:
* The CoMaps have user-generated identifiers, such as a slug
* The CoMaps have identifiers referring to equivalent data in an external system
* The CoMaps have human-readable & application-specific identifiers
* If an application has CoValues used by every user, referring to it by a unique _well-known_ name (eg, `"my-global-comap"`) can be more convenient than using a CoValue ID
Consider a scenario where one wants to identify a CoMap using some unique identifier that isn't the Jazz CoValue ID:
```ts
// This will not work as `learning-jazz` is not a CoValue ID
const myTask = await Task.load("learning-jazz");
```
To make it possible to use human-readable identifiers Jazz lets you to define a `unique` property on CoMaps.
Then the CoValue ID is deterministically derived from the `unique` property and the owner of the CoMap.
```ts
// Given the project owner, myTask will have always the same id
Task.create(
{
text: "Let's learn some Jazz!",
},
{
unique: "learning-jazz",
owner: project.$jazz.owner, // Different owner, different id
},
);
```
Now you can use `CoMap.loadUnique` to easily load the CoMap using the human-readable identifier:
```ts
const learnJazzTask = await Task.loadUnique(
"learning-jazz",
project.$jazz.owner.$jazz.id,
);
```
It's also possible to combine the create+load operation using `CoMap.upsertUnique`:
```ts
await Task.upsertUnique({
value: {
text: "Let's learn some Jazz!",
},
unique: "learning-jazz",
owner: project.$jazz.owner,
});
```
**Caveats:**
* The `unique` parameter acts as an _immutable_ identifier - i.e. the same `unique` parameter in the same `Group` will always refer to the same CoValue.
* To make dynamic renaming possible, you can create an indirection where a stable CoMap identified by a specific value of `unique` is simply a pointer to another CoMap with a normal, dynamic CoValue ID. This pointer can then be updated as desired by users with the corresponding permissions.
* This way of introducing identifiers allows for very fast lookup of individual CoMaps by identifier, but it doesn't let you enumerate all the CoMaps identified this way within a `Group`. If you also need enumeration, consider using a global `co.record()` that maps from identifier to a CoMap, which you then do lookups in (this requires at least a shallow load of the entire `co.record()`, but this should be fast for up to 10s of 1000s of entries)
#### Creating Set-like Collections
You can use CoRecords as a way to create set-like collections, by keying the CoRecord on the item's CoValue ID. You can then use static `Object` methods to iterate over the CoRecord, effectively allowing you to treat it as a set.
```ts
const Chat = co.map({
messages: co.list(Message),
participants: co.record(z.string(), MyAppUser),
});
const chat = await Chat.load(chatId, {
resolve: {
participants: true,
},
});
let participantList: string[];
// Note that I don't need to load the map deeply to read and set keys
if (chat.$isLoaded) {
chat.participants.$jazz.set(me.$jazz.id, me);
participantList = Object.keys(chat.participants);
}
```
You can choose a loading strategy for the CoRecord. Use $each when you need all item properties to be immediately available. In general, it is enough to shallowly load a CoRecord to access its keys, and then load the values of those keys as needed (for example, by passing the keys as strings to a child component).
```ts
const { participants } = await chat.$jazz.ensureLoaded({
resolve: {
participants: {
$each: {
profile: {
avatar: true,
},
},
},
},
});
const avatarList = Object.values(participants).map(
(user) => user.profile.avatar,
);
```
### CoLists
# CoLists
CoLists are ordered collections that work like JavaScript arrays. They provide indexed access, iteration methods, and length properties, making them perfect for managing sequences of items.
## Creating CoLists
CoLists are defined by specifying the type of items they contain:
```ts
import { co, z } from "jazz-tools";
const ListOfResources = co.list(z.string());
export type ListOfResources = co.loaded;
const ListOfTasks = co.list(Task);
export type ListOfTasks = co.loaded;
export type ListOfTasksInitShape = co.input; // type accepted by `ListOfTasks.create`
```
To create a `CoList`:
```ts
// Create an empty list
const resources = co.list(z.string()).create([]);
// Create a list with initial items
const tasks = co.list(Task).create([
{ title: "Prepare soil beds", status: "in-progress" },
{ title: "Order compost", status: "todo" },
]);
```
### Ownership
Like other CoValues, you can specify ownership when creating CoLists.
```ts
// Create with shared ownership
const teamGroup = Group.create();
teamGroup.addMember(colleagueAccount, "writer");
const teamList = co.list(Task).create([], { owner: teamGroup });
```
See [Groups as permission scopes](/docs/permissions-and-sharing/overview) for more information on how to use groups to control access to CoLists.
## Reading from CoLists
CoLists support standard array access patterns:
```ts
// Access by index
const firstTask = tasks[0];
console.log(firstTask.title); // "Prepare soil beds"
// Get list length
console.log(tasks.length); // 2
// Iteration
tasks.forEach((task) => {
console.log(task.title);
// "Prepare soil beds"
// "Order compost"
});
// Array methods
const todoTasks = tasks.filter((task) => task.status === "todo");
console.log(todoTasks.length); // 1
```
## Updating CoLists
Methods to update a CoList's items are grouped inside the `$jazz` namespace:
```ts
// Add items
resources.$jazz.push("Tomatoes"); // Add to end
resources.$jazz.unshift("Lettuce"); // Add to beginning
tasks.$jazz.push({
// Add complex items
title: "Install irrigation", // (Jazz will create
status: "todo", // the CoValue for you!)
});
// Replace items
resources.$jazz.set(0, "Cucumber"); // Replace by index
// Modify nested items
tasks[0].$jazz.set("status", "complete"); // Update properties of references
```
### Soft Deletion
You can do a soft deletion by using a deleted flag, then creating a helper method that explicitly filters out items where the deleted property is true.
```ts
const Task = co.map({
title: z.string(),
status: z.literal(["todo", "in-progress", "complete"]),
deleted: z.optional(z.boolean()), // [!code ++]
});
type Task = typeof Task;
const ListOfTasks = co.list(Task);
type ListOfTasks = typeof ListOfTasks;
export function getCurrentTasks(list: co.loaded) {
return list.filter((task): task is co.loaded => !task.deleted);
}
async function main() {
const myTaskList = ListOfTasks.create([]);
myTaskList.$jazz.push({
title: "Tomatoes",
status: "todo",
deleted: false,
});
myTaskList.$jazz.push({
title: "Cucumbers",
status: "todo",
deleted: true,
});
myTaskList.$jazz.push({
title: "Carrots",
status: "todo",
});
const activeTasks = getCurrentTasks(myTaskList);
console.log(activeTasks.map((task) => task.title));
// Output: ["Tomatoes", "Carrots"]
}
```
There are several benefits to soft deletions:
* **recoverablity** \- Nothing is truly deleted, so recovery is possible in the future
* **data integrity** \- Relationships can be maintained between current and deleted values
* **auditable** \- The data can still be accessed, good for audit trails and checking compliance
### Deleting Items
Jazz provides two methods to retain or remove items from a CoList:
```ts
// Remove items
resources.$jazz.remove(2); // By index
console.log(resources); // ["Cucumber", "Peppers"]
resources.$jazz.remove((item) => item === "Cucumber"); // Or by predicate
console.log(resources); // ["Tomatoes", "Peppers"]
// Keep only items matching the predicate
resources.$jazz.retain((item) => item !== "Cucumber");
console.log(resources); // ["Tomatoes", "Peppers"]
```
You can also remove specific items by index with `splice`, or remove the first or last item with `pop` or `shift`:
```ts
// Remove 2 items starting at index 1
resources.$jazz.splice(1, 2);
console.log(resources); // ["Tomatoes"]
// Remove a single item at index 0
resources.$jazz.splice(0, 1);
console.log(resources); // ["Cucumber", "Peppers"]
// Remove items
const lastItem = resources.$jazz.pop(); // Remove and return last item
resources.$jazz.shift(); // Remove first item
```
### Array Methods
`CoList`s support the standard JavaScript array methods you already know. Methods that mutate the array are grouped inside the `$jazz` namespace.
```ts
// Add multiple items at once
resources.$jazz.push("Tomatoes", "Basil", "Peppers");
// Find items
const basil = resources.find((r) => r === "Basil");
// Filter (returns regular array, not a CoList)
const tItems = resources.filter((r) => r.startsWith("T"));
console.log(tItems); // ["Tomatoes"]
```
### Type Safety
CoLists maintain type safety for their items:
```ts
// TypeScript catches type errors
resources.$jazz.push("Carrots"); // ✓ Valid string
// [!code --]
resources.$jazz.push(42); // ✗ Type error: expected string
// [!code --]
// Argument of type 'number' is not assignable to parameter of type 'string'
// For lists of references
tasks.forEach((task) => {
console.log(task.title); // TypeScript knows task has title
});
```
## Best Practices
### Common Patterns
#### List Rendering
CoLists work well with UI rendering libraries:
```ts
import { co, z } from "jazz-tools";
const ListOfTasks = co.list(Task);
// React example
function TaskList({ tasks }: { tasks: co.loaded }) {
return (
{tasks.map((task) =>
task.$isLoaded ? (
{task.title} - {task.status}
) : null,
)}
);
}
```
#### Managing Relations
CoLists can be used to create one-to-many relationships:
```ts
import { co, z } from "jazz-tools";
const Task = co.map({
title: z.string(),
status: z.literal(["todo", "in-progress", "complete"]),
get project(): co.Optional {
return co.optional(Project);
},
});
const ListOfTasks = co.list(Task);
const Project = co.map({
name: z.string(),
get tasks(): co.List {
return ListOfTasks;
},
});
const project = Project.create({
name: "Garden Project",
tasks: ListOfTasks.create([]),
});
const task = Task.create({
title: "Plant seedlings",
status: "todo",
project: project, // Add a reference to the project
});
// Add a task to a garden project
project.tasks.$jazz.push(task);
// Access the project from the task
console.log(task.project); // { name: "Garden Project", tasks: [task] }
```
#### Set-like Collections
CoLists, like JavaScript arrays, allow you to insert the same item multiple times. In some cases, you might want to have a collection of unique items (similar to a set). To achieve this, you can use a CoRecord with entries keyed on a unique identifier (for example, the CoValue ID).
You can read [more about this pattern here](/docs/core-concepts/covalues/comaps#creating-set-like-collections).
### CoFeeds
# CoFeeds
CoFeeds are append-only data structures that track entries from different user sessions and accounts. Unlike other CoValues where everyone edits the same data, CoFeeds maintain separate streams for each session.
Each account can have multiple sessions (different browser tabs, devices, or app instances), making CoFeeds ideal for building features like activity logs, presence indicators, and notification systems.
The following examples demonstrate a practical use of CoFeeds:
* [Multi-cursors](https://github.com/garden-co/jazz/tree/main/examples/multi-cursors) \- track user presence on a canvas with multiple cursors and out of bounds indicators
* [Reactions](https://github.com/garden-co/jazz/tree/main/examples/reactions) \- store per-user emoji reaction using a CoFeed
## Creating CoFeeds
CoFeeds are defined by specifying the type of items they'll contain, similar to how you define CoLists:
```ts
// Define a schema for feed items
const Activity = co.map({
timestamp: z.date(),
action: z.literal(["watering", "planting", "harvesting", "maintenance"]),
notes: z.optional(z.string()),
});
export type Activity = co.loaded;
// Define a feed of garden activities
const ActivityFeed = co.feed(Activity);
// Create a feed instance
const activityFeed = ActivityFeed.create([]);
```
### Ownership
Like other CoValues, you can specify ownership when creating CoFeeds.
```ts
const teamGroup = Group.create();
teamGroup.addMember(colleagueAccount, "writer");
const teamFeed = ActivityFeed.create([], { owner: teamGroup });
```
See [Groups as permission scopes](/docs/permissions-and-sharing/overview) for more information on how to use groups to control access to CoFeeds.
## Reading from CoFeeds
Since CoFeeds are made of entries from users over multiple sessions, you can access entries in different ways - from a specific user's session or from their account as a whole.
### Per-Session Access
To retrieve entries from a session:
```ts
// Get the feed for a specific session
const sessionFeed = activityFeed.perSession[sessionId];
// Latest entry from a session
if (sessionFeed?.value.$isLoaded) {
console.log(sessionFeed.value.action); // "watering"
}
```
For convenience, you can also access the latest entry from the current session with `inCurrentSession`:
```ts
// Get the feed for the current session
const currentSessionFeed = activityFeed.inCurrentSession;
// Latest entry from the current session
if (currentSessionFeed?.value.$isLoaded) {
console.log(currentSessionFeed.value.action); // "harvesting"
}
```
### Per-Account Access
To retrieve entries from a specific account (with entries from all sessions combined) use `perAccount`:
```ts
// Get the feed for a specific session
const accountFeed = activityFeed.perAccount[accountId];
// Latest entry from an account
if (accountFeed?.value.$isLoaded) {
console.log(accountFeed.value.action); // "watering"
}
```
For convenience, you can also access the latest entry from the current account with `byMe`:
```ts
// Get the feed for the current account
const myLatestEntry = activityFeed.byMe;
// Latest entry from the current account
if (myLatestEntry?.value.$isLoaded) {
console.log(myLatestEntry.value.action); // "harvesting"
}
```
### Feed Entries
#### All Entries
To retrieve all entries from a CoFeed:
```ts
// Get the feeds for a specific account and session
const accountFeed = activityFeed.perAccount[accountId];
const sessionFeed = activityFeed.perSession[sessionId];
// Iterate over all entries from the account
for (const entry of accountFeed.all) {
if (entry.value.$isLoaded) {
console.log(entry.value);
}
}
// Iterate over all entries from the session
for (const entry of sessionFeed.all) {
if (entry.value.$isLoaded) {
console.log(entry.value);
}
}
```
#### Latest Entry
To retrieve the latest entry from a CoFeed, ie. the last update:
```ts
// Get the latest entry from the current account
const latestEntry = activityFeed.byMe;
if (latestEntry?.value.$isLoaded) {
console.log(`My last action was ${latestEntry?.value?.action}`);
// "My last action was harvesting"
}
// Get the latest entry from each account
const latestEntriesByAccount = Object.values(activityFeed.perAccount).map(
(entry) => ({
accountName: entry.by?.profile.$isLoaded ? entry.by.profile.name : "Unknown",
value: entry.value,
}),
);
```
## Writing to CoFeeds
CoFeeds are append-only; you can add new items, but not modify existing ones. This creates a chronological record of events or activities.
### Adding Items
```ts
// Log a new activity
activityFeed.$jazz.push(
Activity.create({
timestamp: new Date(),
action: "watering",
notes: "Extra water for new seedlings",
}),
);
```
Each item is automatically associated with the current user's session. You don't need to specify which session the item belongs to - Jazz handles this automatically.
### Understanding Session Context
Each entry is automatically added to the current session's feed. When a user has multiple open sessions (like both a mobile app and web browser), each session creates its own separate entries:
```ts
// On mobile device:
fromMobileFeed.$jazz.push(
Activity.create({
timestamp: new Date(),
action: "harvesting",
notes: "Vegetable patch",
}),
);
// On web browser (same user):
fromBrowserFeed.$jazz.push(
Activity.create({
timestamp: new Date(),
action: "planting",
notes: "Flower bed",
}),
);
// These are separate entries in the same feed, from the same account
```
## Metadata
CoFeeds support metadata, which is useful for tracking information about the feed itself.
### By
The `by` property is the account that made the entry.
```ts
Me
// Get the feed for the current account
const myLatestEntry = activityFeed.byMe;
// Latest entry from the current account
if (myLatestEntry?.value.$isLoaded) {
console.log(myLatestEntry.value.action); // "harvesting"
}
```
### MadeAt
The `madeAt` property is a timestamp of when the entry was added to the feed.
```ts
const accountFeed = activityFeed.perAccount[accountId];
// Get the timestamp of the last update
console.log(accountFeed?.madeAt);
// Get the timestamp of each entry
for (const entry of accountFeed.all) {
console.log(entry.madeAt);
}
```
## Best Practices
### When to Use CoFeeds
* **Use CoFeeds when**:
* You need to track per-user/per-session data
* Time-based information matters (activity logs, presence)
* **Consider alternatives when**:
* Data needs to be collaboratively edited (use CoMaps or CoLists)
* You need structured relationships (use CoMaps/CoLists with references)
### CoTexts
# CoTexts
Jazz provides two CoValue types for collaborative text editing, collectively referred to as "CoText" values:
* **`co.plainText()`** for simple text editing without formatting
* **`co.richText()`** for rich text with HTML-based formatting (extends `co.plainText()`)
Both types enable real-time collaborative editing of text content while maintaining consistency across multiple users.
**Note:** If you're looking for a quick way to add rich text editing to your app, check out [our prosemirror plugin](#using-rich-text-with-prosemirror).
```ts
const note = co.plainText().create("Meeting notes");
// Update the text
note.$jazz.applyDiff("Meeting notes for Tuesday");
console.log(note.toString()); // "Meeting notes for Tuesday"
```
For a full example of CoTexts in action, see [our Richtext example app](https://github.com/garden-co/jazz/tree/main/examples/richtext-prosemirror), which shows plain text and rich text editing.
## `co.plainText()` vs `z.string()`
While `z.string()` is perfect for simple text fields, `co.plainText()` is the right choice when you need:
* Frequent text edits that aren't just replacing the whole field
* Fine-grained control over text edits (inserting, deleting at specific positions)
* Multiple users editing the same text simultaneously
* Character-by-character collaboration
* Efficient merging of concurrent changes
Both support real-time updates, but `co.plainText()` provides specialized tools for collaborative editing scenarios.
## Creating CoText Values
CoText values are typically used as fields in your schemas:
```ts
const Profile = co.profile({
name: z.string(),
bio: co.plainText(), // Plain text field
description: co.richText(), // Rich text with formatting
});
```
Create a CoText value with a simple string:
```ts
// Create plaintext with default ownership (current user)
const meetingNotes = co.plainText().create("Meeting notes");
// Create rich text with HTML content
const document = co
.richText()
.create("
Project overview
");
```
### Ownership
Like other CoValues, you can specify ownership when creating CoTexts.
```ts
// Create with shared ownership
const teamGroup = Group.create();
teamGroup.addMember(colleagueAccount, "writer");
const teamNote = co.plainText().create("Team updates", { owner: teamGroup });
```
See [Groups as permission scopes](/docs/permissions-and-sharing/overview) for more information on how to use groups to control access to CoText values.
## Reading Text
CoText values work similarly to JavaScript strings:
```ts
// Get the text content
console.log(note.toString()); // "Meeting notes"
console.log(`${note}`); // "Meeting notes"
// Check the text length
console.log(note.length); // 14
```
\--- Section applies only to react ---
When using CoTexts in JSX, you can read the text directly:
```tsx
<>
{note.toString()}
{note}
>;
```
\--- End of react specific section ---
## Making Edits
Insert and delete text with intuitive methods:
```ts
// Insert text at a specific position
note.insertBefore(8, "weekly "); // "Meeting weekly notes"
// Insert after a position
note.insertAfter(21, " for Monday"); // "Meeting weekly notes for Monday"
// Delete a range of text
note.deleteRange({ from: 8, to: 15 }); // "Meeting notes for Monday"
// Apply a diff to update the entire text
note.$jazz.applyDiff("Team meeting notes for Tuesday");
```
### Applying Diffs
Use `applyDiff` to efficiently update text with minimal changes:
```ts
// Original text: "Team status update"
const minutes = co.plainText().create("Team status update");
// Replace the entire text with a new version
minutes.$jazz.applyDiff("Weekly team status update for Project X");
// Make partial changes
let text = minutes.toString();
text = text.replace("Weekly", "Monday");
minutes.$jazz.applyDiff(text); // Efficiently updates only what changed
```
Perfect for handling user input in form controls:
\--- Section applies only to react ---
```ts
function TextEditor({ textId }: { textId: string }) {
const note = useCoState(co.plainText(), textId);
return (
note.$isLoaded && (
}>
;
)
}
```
##### Svelte:
```ts
```
##### React Native:
```tsx
function EditProjectComponent({
projectId,
currentBranchName,
}: {
projectId: ID;
currentBranchName: string;
}) {
const project = useCoState(Project, projectId, {
resolve: {
// When we use a 'resolve' query with a branch, all of the 'resolved' CoValues are also part of the new branch
tasks: { $each: true },
},
unstable_branch: {
name: currentBranchName,
},
});
return (
{project.$isLoaded &&
Project project.$jazz.set("title", v)} />
{project.tasks.map(task => {
return (
{ task.$jazz.set('title', v) }} />
)
})}
}
);
}
```
##### React Native (Expo):
```tsx
function EditProjectComponent({
projectId,
currentBranchName,
}: {
projectId: ID;
currentBranchName: string;
}) {
const project = useCoState(Project, projectId, {
resolve: {
// When we use a 'resolve' query with a branch, all of the 'resolved' CoValues are also part of the new branch
tasks: { $each: true },
},
unstable_branch: {
name: currentBranchName,
},
});
return (
{project.$isLoaded &&
Project project.$jazz.set("title", v)} />
{project.tasks.map(task => {
return (
{ task.$jazz.set('title', v) }} />
)
})}
}
);
}
```
### Account & Group
Branching does not bring isolation on Account and Group CoValues.
This means that, adding a member on a branched Group will also add the member to the main Group.
```ts
const featureBranch = await Project.load(projectId, {
unstable_branch: { name: "feature-branch" },
});
featureBranch.$isLoaded &&
featureBranch.$jazz.owner.addMember(member, "writer"); // Will also add the member to the main Group
```
If you are modifying an account, be aware that replacing the root or profile will also modify the main account (although updating the properties will happen on the branch).
##### Vanilla:
```ts
const myAcct = MyAccount.getMe();
const me = await myAcct.$jazz.ensureLoaded({
resolve: { root: true },
unstable_branch: { name: "feature-branch" },
});
me.$isLoaded && me.$jazz.set("root", { value: "Feature Branch" }); // Will also modify the main account
me.$isLoaded && me.root.$jazz.set("value", "Feature Branch"); // This only modifies the branch
```
##### React:
```tsx
const me = useAccount(MyAccount, {
resolve: { root: true },
unstable_branch: { name: "feature-branch" },
});
me.$isLoaded && me.$jazz.set("root", { value: "Feature Branch" }); // Will also modify the main account
me.$isLoaded && me.root.$jazz.set("value", "Feature Branch"); // This only modifies the branch
```
##### Svelte:
```svelte
```
##### React Native:
```tsx
const me = useAccount(MyAccount, {
resolve: { root: true },
unstable_branch: { name: "feature-branch" },
});
me.$isLoaded && me.$jazz.set("root", { value: "Feature Branch" }); // Will also modify the main account
me.$isLoaded && me.root.$jazz.set("value", "Feature Branch"); // This only modifies the branch
```
##### React Native (Expo):
```tsx
const me = useAccount(MyAccount, {
resolve: { root: true },
unstable_branch: { name: "feature-branch" },
});
me.$isLoaded && me.$jazz.set("root", { value: "Feature Branch" }); // Will also modify the main account
me.$isLoaded && me.root.$jazz.set("value", "Feature Branch"); // This only modifies the branch
```
### Merging Branches
There are two ways to merge a branch in Jazz, each with different characteristics:
#### 1\. Merge loaded values
This method merges all the values that are currently loaded inside the branch. It happens synchronously and there is no possibility of errors because the values are already loaded.
```ts
async function handleSave() {
// Merge all currently loaded values in the branch
branch.$isLoaded && branch.$jazz.unstable_merge();
}
```
This approach is recommended when you can co-locate the merge operation with the branch load, keeping at a glance what the merge operation will affect.
**Info:**
**Important:** The merge operation will only affect values loaded in the current subscription scope. Values loaded via `ensureLoaded` or `subscribe` will not be affected.
#### 2\. Merge with resolve query
This is a shortcut for loading a value and calling `branch.$jazz.unstable_merge()` on it and will fail if the load isn't possible due to permission errors or network issues.
```ts
async function handleSaveWithResolve() {
// Merge the branch changes back to main
await Project.unstable_merge(projectId, {
resolve: {
tasks: { $each: true },
},
branch: { name: "feature-branch" },
});
}
```
This approach is recommended for more complex merge operations where it's not possible to co-locate the merge with the branch load.
#### Best Practices
When using version control with Jazz, always be exhaustive when defining the resolve query to keep the depth of the branch under control and ensure that the merge covers all the branched values.
The mechanism that Jazz uses to automatically load accessed values should be avoided with branching, as it might lead to cases where merge won't reach all the branch changes.
All the changes made to the branch will be merged into the main CoValue, preserving both author and timestamp.
The merge is idempotent, so you can merge the same branch multiple times, the result will always depend on the branch changes and loading state.
The merge operation cascades down to the CoValue's children, but not to its parents. So if you call `unstable_merge()` on a task, only the changes to the task and their children will be merged:
```tsx
async function handleTaskSave(index: number) {
const task = project.tasks[index];
// Only the changes to the task will be merged
task.$jazz.unstable_merge();
}
```
## Conflict Resolution
When conflicts occur (the same field is modified in both the branch and main), Jazz uses a "last writer wins" strategy:
```ts
// Branch modifies priority to "high"
branch.$isLoaded && branch.$jazz.applyDiff({ priority: "high" });
// Meanwhile, main modifies priority to "urgent"
originalProject.$isLoaded &&
originalProject.$jazz.applyDiff({ priority: "urgent" });
// Merge the branch
branch.$isLoaded && branch.$jazz.unstable_merge();
// Main's value ("urgent") wins because it was written later
console.log(originalProject.priority); // "urgent"
```
## Private branches
When the owner is not specified, the branch has the same permissions as the main values.
You can also create a private branch by providing a group owner.
```ts
// Create a private group for the branch
const privateGroup = Group.create();
const privateBranch = Project.load(projectId, {
unstable_branch: {
name: "private-edit",
owner: privateGroup,
},
});
// Only members of privateGroup can see the branch content
// The sync server cannot read the branch content
```
You can use private branches both to make the changes to the branches "private" until merged, or to give controlled write access to a group of users.
Only users with both write access to the main branch and read access to the private branch have the rights to merge the branch.
**Info:**
**Important:** Branch names are scoped to their owner. The same branch name with different owners creates completely separate branches. For example, a branch named "feature-branch" owned by User A is completely different from a branch named "feature-branch" owned by User B.
## Branch Identification
You can get the current branch information from the `$jazz` field.
```ts
const myBranch = await Project.load(projectId, {
unstable_branch: { name: "feature-branch" },
});
console.log(myBranch.$jazz.id); // Branch ID is the same as source
console.log(myBranch.$isLoaded && myBranch.$jazz.branchName); // "feature-branch"
console.log(myBranch.$isLoaded && myBranch.$jazz.isBranched); // true
```
### History
# History
Jazz tracks every change to your data automatically. See who changed what, when they did it, and even look at your data from any point in the past.
See the [version history example](https://github.com/garden-co/jazz/tree/main/examples/version-history) for reference.
Let's use the following schema to see how we can use the edit history.
```ts
export const Task = co.map({
title: z.string(),
status: z.literal(["todo", "in-progress", "completed"]),
});
export type Task = co.loaded;
```
## The $jazz.getEdits() method
Every CoValue has a `$jazz.getEdits()` method that contains the complete history for each field. Here's how to get the edit history for `task.status`:
```ts
task.$jazz.getEdits().status;
// Returns the latest edit
task.$jazz.getEdits().status?.all;
// Returns array of all edits in chronological order
// Check if edits exist
const statusEdits = task.$jazz.getEdits().status;
if (statusEdits && statusEdits.by?.profile.$isLoaded) {
const name = statusEdits.by.profile.name;
console.log(`Last changed by ${name}`);
}
```
## Edit Structure
Each edit contains:
```ts
const edit = task.$jazz.getEdits().status;
// The edit object contains:
edit?.value; // The new value: "in-progress"
edit?.by; // Account that made the change
edit?.madeAt; // Date when the change occurred
```
## Accessing History
### Latest Edit
Get the most recent change to a field:
```ts
// Direct access to latest edit
const latest = task.$jazz.getEdits().title;
if (latest) {
console.log(`Title is now "${latest.value}"`);
}
```
### All Edits
Get the complete history for a field:
```ts
// Get all edits (chronologically)
const allStatusEdits = task.$jazz.getEdits().status?.all || [];
allStatusEdits.forEach((edit, index) => {
console.log(`Edit ${index}: ${edit.value} at ${edit.madeAt.toISOString()}`);
});
// Edit 0: todo at 2025-05-22T13:00:00.000Z
// Edit 1: in-progress at 2025-05-22T14:00:00.000Z
// Edit 2: completed at 2025-05-22T15:30:00.000Z
```
### Initial Values
The first edit contains the initial value:
```ts
const allEdits = task.$jazz.getEdits().status?.all || [];
const initialValue = allEdits[0]?.value;
console.log(`Started as: ${initialValue}`);
// Started as: todo
```
### Created Date and Last Updated Date
To show created date and last updated date, use the `$jazz.createdAt` and `$jazz.lastUpdatedAt` getters.
```tsx
console.log(new Date(task.$jazz.createdAt));
console.log(new Date(task.$jazz.lastUpdatedAt));
```
## Requirements
* CoValues must be loaded to access history (see [Subscription & Loading](/docs/core-concepts/subscription-and-loading))
* History is only available for fields defined in your schema
* Edit arrays are ordered chronologically (oldest to newest)
## Common Patterns
For practical implementations using history, see [History Patterns](/docs/reference/design-patterns/history-patterns):
* Building audit logs
* Creating activity feeds
* Implementing undo/redo
* Showing change indicators
* Querying historical data
## Server-Side Development
### Quickstart
# Get started with Server Workers in 10 minutes
This quickstart guide will take you from an empty project to a server worker which can interact with your Jazz application.
* You'll get the most out of this guide if you complete [the frontend quickstart guide](/docs/quickstart) first.
* If you've already completed the frontend quickstart, you can skip straight to [extending your schema](#define-your-schema).
\--- Section applies only to react ---
## Create your Next.js App
We'll be using Next.js for simplicity, but you can use any framework you like.
You can accept the defaults for all the questions, or customise the project as you like.
##### npm:
```sh
npx create-next-app@latest --typescript jazzfest
cd jazzfest
```
##### pnpm:
```sh
pnpx create-next-app@latest --typescript jazzfest
cd jazzfest
```
\--- End of react specific section ---
\--- Section applies only to svelte ---
## Create your SvelteKit App
We'll be using SvelteKit for simplicity, but you can use any framework you like.
You can accept the defaults for all the questions, or customise the project as you like.
##### npm:
```sh
npx sv create --types ts --template minimal jazzfest
cd jazzfest
```
##### pnpm:
```sh
pnpx sv create --types ts --template minimal jazzfest
cd jazzfest
```
\--- End of svelte specific section ---
**Info:**
Requires Node.js 20+
## Install Jazz
The `jazz-tools` package includes everything you're going to need to build your first Jazz server worker.
##### npm:
```sh
npm install jazz-tools
```
##### pnpm:
```sh
pnpm add jazz-tools
```
## Set your API key
Sign up for a free API key at [dashboard.jazz.tools](https://dashboard.jazz.tools) for higher limits or production use, or use your email address as a temporary key to get started quickly.
\--- Section applies only to react ---
**File name: .env**
\--- End of react specific section ---
\--- Section applies only to svelte ---
**File name: .env**
\--- End of svelte specific section ---
##### React:
```bash
NEXT_PUBLIC_JAZZ_API_KEY="you@example.com" # or your API key
```
##### Svelte:
```bash
PUBLIC_JAZZ_API_KEY="you@example.com" # or your API key
```
## Define your schema
We're going to define a simple schema for our server worker. We'll use the `root` on the worker to store a list of bands. We're also going to add a migration to initialise the `root` if it doesn't exist.
\--- Section applies only to react ---
**File name: app/schema.ts**
\--- End of react specific section ---
\--- Section applies only to svelte ---
**File name: src/lib/schema.ts**
\--- End of svelte specific section ---
```ts
import { co, z } from "jazz-tools";
export const Band = co.map({
name: z.string(),
});
export const BandList = co.list(Band);
export const JazzFestWorkerAccount = co
.account({
root: co.map({
bandList: BandList,
}),
profile: co.profile(),
})
.withMigration(async (account) => {
if (!account.$jazz.has("root")) {
account.$jazz.set("root", {
bandList: [],
});
if (account.root.$isLoaded) {
account.root.$jazz.owner.makePublic();
}
}
});
```
**Info:**
If you're continuing from the [front-end Quickstart](/docs/quickstart), you can extend your existing schema.
## Create a Server Worker
Jazz provides a CLI to create server workers. You can create a server worker using the following command:
##### npm:
```sh
npx jazz-run account create --name "JazzFest Server Worker"
```
##### pnpm:
```sh
pnpx jazz-run account create --name "JazzFest Server Worker"
```
You can copy the output of this command and paste it directly into your `.env` file:
**File name: .env**
##### Next.js:
```bash
NEXT_PUBLIC_JAZZ_API_KEY=you@example.com # or your API key
#[!code ++:2]
NEXT_PUBLIC_JAZZ_WORKER_ACCOUNT=co_z...
JAZZ_WORKER_SECRET=sealerSecret_z.../signerSecret_z...
```
##### SvelteKit:
```bash
PUBLIC_JAZZ_API_KEY=you@example.com # or your API key
#[!code ++:2]
PUBLIC_JAZZ_WORKER_ACCOUNT=co_z...
JAZZ_WORKER_SECRET=sealerSecret_z.../signerSecret_z...
```
**Warning:**
Your `JAZZ_WORKER_SECRET` should **never** be exposed to the client.
## Defining your HTTP request schema
Next, we're going to set up an HTTP request schema to define our request and response. Here, we tell Jazz that we will send a `Band` under the key `band` and expect a `bandList` in response, which is a list of `Band`s.
We also need to tell Jazz which keys should be treated as loaded in the request and response using the `resolve` query.
\--- Section applies only to react ---
**File name: app/announceBandSchema.ts**
\--- End of react specific section ---
\--- Section applies only to svelte ---
**File name: src/lib/announceBandSchema.ts**
\--- End of svelte specific section ---
##### Next.js:
```ts
import { experimental_defineRequest } from "jazz-tools";
import { Band, BandList } from "./schema";
const workerId = process.env.NEXT_PUBLIC_JAZZ_WORKER_ACCOUNT;
if (!workerId) throw new Error("NEXT_PUBLIC_JAZZ_WORKER_ACCOUNT is not set");
export const announceBand = experimental_defineRequest({
url: "/api/announce-band",
workerId: workerId,
request: { schema: { band: Band }, resolve: { band: true } },
response: {
schema: { bandList: BandList },
resolve: { bandList: { $each: true } },
},
});
```
##### SvelteKit:
```ts
import { experimental_defineRequest } from "jazz-tools";
import { Band, BandList } from "./schema";
import { PUBLIC_JAZZ_WORKER_ACCOUNT } from "$env/static/public";
const workerId = PUBLIC_JAZZ_WORKER_ACCOUNT;
if (!workerId) throw new Error("PUBLIC_JAZZ_WORKER_ACCOUNT is not set");
export const announceBand = experimental_defineRequest({
url: "/api/announce-band",
workerId: workerId,
request: { schema: { band: Band }, resolve: { band: true } },
response: {
schema: { bandList: BandList },
resolve: { bandList: { $each: true } },
},
});
```
## Configure your Server Worker
We're going to use the `startWorker` function to start our server worker, and register a `POST` handler, which will listen for the requests being sent to our server worker.
We'll also use a `resolve` query here to make sure that the `bandList` is loaded on the worker's root.
\--- Section applies only to react ---
**File name: app/api/announce-band/route.ts**
\--- End of react specific section ---
\--- Section applies only to svelte ---
**File name: src/routes/api/announce-band/+server.ts**
\--- End of svelte specific section ---
##### Next.js:
```ts
import { startWorker } from "jazz-tools/worker";
import { announceBand } from "@/app/announceBandSchema";
import { JazzFestWorkerAccount } from "./schema";
const { worker } = await startWorker({
syncServer: `wss://cloud.jazz.tools/?key=${process.env.NEXT_PUBLIC_JAZZ_API_KEY}`,
accountID: process.env.NEXT_PUBLIC_JAZZ_WORKER_ACCOUNT,
accountSecret: process.env.JAZZ_WORKER_SECRET,
AccountSchema: JazzFestWorkerAccount,
});
export async function POST(request: Request) {
return announceBand.handle(request, worker, async ({ band }) => {
if (!band) {
throw new Error("Band is required");
}
const {
root: { bandList },
} = await worker.$jazz.ensureLoaded({
resolve: {
root: {
bandList: true,
},
},
});
bandList.$jazz.push(band);
return { bandList };
});
}
```
##### SvelteKit:
```ts
import { startWorker } from "jazz-tools/worker";
import { announceBand } from "$lib/announceBandSchema";
import { JazzFestWorkerAccount } from "./schema";
import {
PUBLIC_JAZZ_API_KEY,
PUBLIC_JAZZ_WORKER_ACCOUNT,
} from "$env/static/public";
import { JAZZ_WORKER_SECRET } from "$env/static/private";
import type { RequestHandler } from "./$types";
const { worker } = await startWorker({
syncServer: `wss://cloud.jazz.tools/?key=${PUBLIC_JAZZ_API_KEY}`,
accountID: PUBLIC_JAZZ_WORKER_ACCOUNT,
accountSecret: JAZZ_WORKER_SECRET,
AccountSchema: JazzFestWorkerAccount,
});
export const POST: RequestHandler = async ({ request }) => {
return announceBand.handle(request, worker, async ({ band }) => {
if (!band) {
throw new Error("Band is required");
}
const {
root: { bandList },
} = await worker.$jazz.ensureLoaded({
resolve: {
root: {
bandList: true,
},
},
});
bandList.$jazz.push(band);
return {
bandList,
};
});
};
```
## Start your server worker
We can now start our development server to make sure everything is working.
##### npm:
```bash
npm run dev
```
##### pnpm:
```bash
pnpm run dev
```
\--- Section applies only to react ---
If you open your browser, you should see the default Next.js welcome page.
\--- End of react specific section ---
\--- Section applies only to svelte ---
If you open your browser, you should see the default SvelteKit welcome page.
\--- End of svelte specific section ---
### Not working?
\--- Section applies only to react ---
* Check you set up your `.env` file correctly with `NEXT_PUBLIC_` where necessary
\--- End of react specific section ---
\--- Section applies only to svelte ---
* Check you set up your `.env` file correctly with `PUBLIC_` where necessary
\--- End of svelte specific section ---
* Check you're importing `startWorker` from `jazz-tools/worker`
**Info: Still stuck?** Ask for help on [Discord](https://discord.gg/utDMjHYg42)!
## Send requests to your server worker
### Creating a Jazz Client
_If you already have a working provider from the frontend quickstart, you can skip this step._
\--- Section applies only to react ---
We're going to wrap our Next.js app in a `JazzReactProvider` so that we can use Jazz on our client.
\--- End of react specific section ---
\--- Section applies only to svelte ---
We're going to wrap our SvelteKit app in a `JazzSvelteProvider` so that we can use Jazz on our client.
\--- End of svelte specific section ---
\--- Section applies only to react ---
**File name: app/layout.tsx**
\--- End of react specific section ---
\--- Section applies only to svelte ---
**File name: src/routes/+layout.svelte**
\--- End of svelte specific section ---
##### Next.js:
```tsx
import { JazzReactProvider } from "jazz-tools/react";
const apiKey = process.env.NEXT_PUBLIC_JAZZ_API_KEY;
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
{children}
);
}
```
##### Svelte:
```svelte
{@render children?.()}
```
### Creating your page component
We're going to send a request to our server worker to announce a new band. Our worker will respond with a list of bands that we can display on our page.
\--- Section applies only to react ---
**File name: app/page.tsx**
\--- End of react specific section ---
\--- Section applies only to svelte ---
**File name: src/routes/+page.svelte**
\--- End of svelte specific section ---
##### Next.js:
```tsx
"use client";
import type { co } from "jazz-tools";
import { useState } from "react";
import { announceBand } from "@/app/announceBandSchema";
import type { BandList } from "./schema";
export default function Home() {
const [bandName, setBandName] = useState("");
const [bandList, setBandList] =
useState>();
const handleAnnounceBand = async () => {
const bandListResponse = await announceBand.send({
band: { name: bandName },
});
setBandName("");
if (bandListResponse.bandList.$isLoaded) {
setBandList(bandListResponse.bandList);
}
};
return (
setBandName(e.target.value)}
/>
{bandList?.$isLoaded &&
bandList.map(
(band) => band &&
{band.name}
,
)}
);
}
```
##### Svelte:
```svelte
{#if bandList?.$isLoaded}
{#each bandList as band (band?.$jazz.id)}
{band.name}
{/each}
{/if}
```
## Try it out!
Your browser should now be showing you a page with an input field and a button. If you enter a band name and click the button, your server worker will receive the request and add the band to the list.
**Congratulations! 🎉** You've just built your first Jazz server worker!
This simple pattern is the foundation for building powerful, real-time applications.
Here are some ideas about what you could use your server worker for:
* integrating with payment providers
* sending emails/SMSes
* gathering data from external APIs
* managing authoritative state
Looking forward to seeing what you build!
## Next steps
* Complete the [front-end quickstart](/docs/quickstart) to learn more about how to build real-time UIs using Jazz
* Find out how to [handle errors](/docs/server-side/communicating-with-workers/http-requests#error-handling) gracefully in your server worker
* Learn how to share and [collaborate on data](/docs/permissions-and-sharing/overview) in groups with complex permissions
### Setup
# Running Jazz on the server
Jazz is a distributed database that can be used on both clients or servers without any distinction.
You can use servers to:
* perform operations that can't be done on the client (e.g. sending emails, making HTTP requests, etc.)
* validate actions that require a central authority (e.g. a payment gateway, booking a hotel, etc.)
We call the code that runs on the server a "Server Worker".
The main difference to keep in mind when working with Jazz compared to traditional systems is that server code doesn't have any special or privileged access to the user data. You need to be explicit about what you want to share with the server.
This means that your server workers will have their own accounts, and they need to be explicitly given access to the CoValues they need to work on.
## Generating credentials
Server Workers typically have static credentials, consisting of a public Account ID and a private Account Secret.
To generate new credentials for a Server Worker, you can run:
```sh
npx jazz-run account create --name "My Server Worker"
```
The name will be put in the public profile of the Server Worker's `Account`, which can be helpful when inspecting metadata of CoValue edits that the Server Worker has done.
**Info: Note**
By default the account will be stored in Jazz Cloud. You can use the `--peer` flag to store the account on a different sync server.
## Running a server worker
You can use `startWorker` to run a Server Worker. Similarly to setting up a client-side Jazz context, it:
* takes a custom `AccountSchema` if you have one (for example, because the worker needs to store information in its private account root)
* takes a URL for a sync & storage server
The migration defined in the `AccountSchema` will be executed every time the worker starts, the same way as it would be for a client-side Jazz context.
```ts
import { startWorker } from "jazz-tools/worker";
const { worker } = await startWorker({
AccountSchema: MyWorkerAccount,
syncServer: `wss://cloud.jazz.tools/?key=${apiKey}`,
accountID: process.env.JAZZ_WORKER_ACCOUNT,
accountSecret: process.env.JAZZ_WORKER_SECRET,
});
```
`worker` is an instance of the `Account` schema provided, and acts like `me` (as returned by `useAccount` on the client).
It will implicitly become the current account, and you can avoid mentioning it in most cases.
For this reason we also recommend running a single worker instance per server, because it makes your code much more predictable.
In case you want to avoid setting the current account, you can pass `asActiveAccount: false` to `startWorker`.
## Storing & providing credentials
Server Worker credentials are typically stored and provided as environment variables.
**Take extra care with the Account Secret — handle it like any other secret environment variable such as a DB password.**
## Wasm on Edge runtimes
To maximize compatibility, Jazz falls back to a slower, JavaScript crypto implementation if the faster WASM implementation is not available.
On some edge platforms, such as Cloudflare Workers or Vercel Edge Functions, environment security restrictions may trigger this fallback unnecessarily.
You can ensure that Jazz uses the faster WASM implementation by importing the WASM loader before using Jazz. For example:
```ts
import "jazz-tools/load-edge-wasm";
// Other Jazz Imports
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
// Jazz application logic
return new Response("Hello from Jazz on Cloudflare!");
},
};
```
Currently, the Jazz Loader is tested on the following edge environments:
* Cloudflare Workers
* Vercel Functions
### Requirements
* Edge runtime environment that supports WebAssembly
* `jazz-tools/load-edge-wasm` must be imported before any Jazz import
## Node-API
Jazz uses a WASM-based crypto implementation that provides near-native performance while ensuring full compatibility across a wide variety of environments.
For even higher performance on Node.js or Deno, you can enable the native crypto (Node-API) implementation. Node-API is Node.js's native API for building modules in Native Code (Rust/C++) that interact directly with the underlying system, allowing for true native execution speed.
You can use it as follows:
```ts
import { startWorker } from "jazz-tools/worker";
import { NapiCrypto } from "jazz-tools/napi";
const { worker } = await startWorker({
syncServer: `wss://cloud.jazz.tools/?key=${apiKey}`,
accountID: process.env.JAZZ_WORKER_ACCOUNT,
accountSecret: process.env.JAZZ_WORKER_SECRET,
crypto: await NapiCrypto.create(),
});
```
**Info: Note**
The Node-API implementation is not available on all platforms. It is only available on Node.js 20.x and higher. The supported platforms are:
* macOS (x64, ARM64)
* Linux (x64, ARM64, ARM, musl)
It does not work in edge runtimes.
### On Next.js
In order to use Node-API with Next.js, you need to tell Next.js to bundle the native modules in your build.
You can do this by adding the required packages to the [serverExternalPackages](https://nextjs.org/docs/app/api-reference/config/next-config-js/serverExternalPackages) array in your `next.config.js`.
**Note**: if you're deploying to Vercel, be sure to use the `nodejs` runtime!
**File name: next.config.js**
```ts
module.exports = {
serverExternalPackages: [
"cojson-core-napi",
"cojson-core-napi-linux-x64-gnu",
"cojson-core-napi-linux-x64-musl",
"cojson-core-napi-linux-arm64-gnu",
"cojson-core-napi-linux-arm64-musl",
"cojson-core-napi-darwin-x64",
"cojson-core-napi-darwin-arm64",
"cojson-core-napi-linux-arm-gnueabihf",
],
};
```
### Overview
# Communicating with Server Workers
Server Workers in Jazz can receive data from clients through two different APIs, each with their own characteristics and use cases. This guide covers the key properties of each approach to help you choose the right one for your application.
## Overview
Jazz provides three ways to communicate with Server Workers:
1. **JazzRPC** \- A simple, yet powerful RPC system that allows you to call functions on Server Workers from the client side.
2. **HTTP Requests** \- The easiest to work with and deploy, ideal for simple communication with workers.
3. **Inbox** \- Fully built using the Jazz data model with offline support
## JazzRPC (Recommended)
JazzRPC is the most straightforward way to communicate with Server Workers. It works well with any framework or runtime that supports standard Request and Response objects, can be scaled horizontally, and put clients and workers in direct communication.
### When to use JazzRPC
Use JazzRPC when you need immediate responses, are deploying to serverless environments, need horizontal scaling, or are working with standard web frameworks.
It's also a good solution when using full-stack frameworks like Next.js, where you can use the API routes to handle the server-side logic.
[Learn more about JazzRPC →](/docs/server-side/jazz-rpc)
## HTTP Requests
If all you need is basic authentication when communicating with a worker, you can use Regular HTTP requests. They are the easiest to work with and deploy, ideal for simple communication with workers.
HTTP requests are the easiest way to communicate with Server Workers. They don't come with any of the benefits of JazzRPC, but are a good solution for simple communication with workers.
### When to use HTTP Requests
Use HTTP requests when you don't need the advanced features of JazzRPC, but you need to communicate with a worker from a serverless environment or a standard web framework and need basic authentication.
[Learn more about HTTP Requests →](/docs/server-side/communicating-with-workers/http-requests)
## Inbox
The Inbox API is fully built using the Jazz data model and provides offline support. Requests and responses are synced as soon as the device becomes online, but require the Worker to always be online to work properly.
### When to use Inbox
Use Inbox when you need offline support, want to leverage the Jazz data model, can ensure the worker stays online, need persistent message storage, or want to review message history.
It works great when you don't want to expose your server with a public address, because it uses Jazz's sync to make the communication happen.
Since Jazz handles all the network communication, the entire class of network errors that usually come with traditional HTTP requests are not a problem when using the Inbox API.
[Learn more about Inbox →](/docs/server-side/communicating-with-workers/inbox)
### JazzRPC
# JazzRPC
JazzRPC is the most straightforward and complete way to securely communicate with Server Workers. It works well with any framework or runtime that supports standard Request and Response objects, can be scaled horizontally, and puts clients and workers in direct communication.
## Setting up JazzRPC
### Defining request schemas
Use `experimental_defineRequest` to define your API schema:
```ts
import { experimental_defineRequest, z } from "jazz-tools";
import { Event, Ticket } from "@/lib/schema";
const workerId = process.env.NEXT_PUBLIC_JAZZ_WORKER_ACCOUNT!;
export const bookEventTicket = experimental_defineRequest({
url: "/api/book-event-ticket",
// The id of the worker Account or Group
workerId,
// The schema definition of the data we send to the server
request: {
schema: {
event: Event,
},
// The data that will be considered as "loaded" in the server input
resolve: {
event: { reservations: true },
},
},
// The schema definition of the data we expect to receive from the server
response: {
schema: { ticket: Ticket },
// The data that will be considered as "loaded" in the client response
// It defines the content that the server directly sends to the client, without involving the sync server
resolve: { ticket: true },
},
});
```
### Setting up the Server Worker
We need to start a Server Worker instance that will be able to sync data with the sync server, and handle the requests.
```ts
import { startWorker } from "jazz-tools/worker";
export const jazzServer = await startWorker({
syncServer: "wss://cloud.jazz.tools/?key=your-api-key",
accountID: process.env.JAZZ_WORKER_ACCOUNT,
accountSecret: process.env.JAZZ_WORKER_SECRET,
});
```
## Handling JazzRPC requests on the server
### Creating API routes
Create API routes to handle the defined RPC requests. Here's an example using Next.js API routes:
```ts
import { jazzServer } from "@/jazzServer";
import { Ticket } from "@/lib/schema";
import { bookEventTicket } from "@/bookEventTicket";
import { Group, JazzRequestError } from "jazz-tools";
export async function POST(request: Request) {
return bookEventTicket.handle(
request,
jazzServer.worker,
async ({ event }, madeBy) => {
const ticketGroup = Group.create(jazzServer.worker);
const ticket = Ticket.create({
account: madeBy,
event,
});
// Give access to the ticket to the client
ticketGroup.addMember(madeBy, "reader");
event.reservations.$jazz.push(ticket);
return {
ticket,
};
},
);
}
```
## Making requests from the client
### Using the defined API
Make requests from the client using the defined API:
```ts
import { bookEventTicket } from "@/bookEventTicket";
import { Event } from "@/lib/schema";
import { co, isJazzRequestError } from "jazz-tools";
export async function sendEventBookingRequest(event: co.loaded) {
const { ticket } = await bookEventTicket.send({ event });
return ticket;
}
export async function sendEventBookingRequest(event: co.loaded) {
try {
const { ticket } = await bookEventTicket.send({ event });
return ticket;
} catch (error) {
// This works as a type guard, so you can easily get the error message and details
if (isJazzRequestError(error)) {
alert(error.message);
return;
}
}
}
```
## Error handling
### Server-side error handling
Use `JazzRequestError` to return proper HTTP error responses:
```ts
export async function POST(request: Request) {
return bookEventTicket.handle(
request,
jazzServer.worker,
async ({ event }, madeBy) => {
// Check if the event is full
if (event.reservations.length >= event.capacity) {
// The JazzRequestError is propagated to the client, use it for any validation errors
throw new JazzRequestError("Event is full", 400);
}
const ticketGroup = Group.create(jazzServer.worker);
const ticket = Ticket.create({
account: madeBy,
event,
});
// Give access to the ticket to the client
ticketGroup.addMember(madeBy, "reader");
event.reservations.$jazz.push(ticket);
return {
ticket,
};
},
);
}
```
**Info: Note**
To ensure that the limit is correctly enforced, the handler should be deployed in a single worker instance (e.g. a single Cloudflare DurableObject).
Details on how to deploy a single instance Worker are available in the [Deployments & Transactionality](#deployments--transactionality) section.
### Client-side error handling
Handle errors on the client side:
```ts
export async function sendEventBookingRequest(event: co.loaded) {
try {
const { ticket } = await bookEventTicket.send({ event });
return ticket;
} catch (error) {
// This works as a type guard, so you can easily get the error message and details
if (isJazzRequestError(error)) {
alert(error.message);
return;
}
}
}
```
**Info: Note**
The `experimental_defineRequest` API is still experimental and may change in future versions. For production applications, consider the stability implications.
## Security safeguards provided by JazzRPC
JazzRPC includes several built-in security measures to protect against common attacks:
### Cryptographic Authentication
* **Digital Signatures**: Each RPC is cryptographically signed using the sender's private key
* **Signature Verification**: The server verifies the signature using the sender's public key to ensure message authenticity and to identify the sender account
* **Tamper Protection**: Any modification to the request payload will invalidate the signature
### Replay Attack Prevention
* **Unique Message IDs**: Each RPC has a unique identifier (`co_z${string}`)
* **Duplicate Detection**: incoming messages ids are tracked to prevent replay attacks
* **Message Expiration**: RPCs expire after 60 seconds to provide additional protection
These safeguards ensure that JazzRPC requests are secure, authenticated, and protected against common attack vectors while maintaining the simplicity of standard HTTP communication.
## Deployments & Transactionality
### Single Instance Requirements
Some operations need to happen one at a time and in the same place, otherwise the data can get out of sync.
For example, if you are checking capacity for an event and creating tickets, you must ensure only one server is doing it. If multiple servers check at the same time, they might all think there is space and allow too many tickets.
Jazz uses eventual consistency (data takes a moment to sync between regions), so this problem is worse if you run multiple server copies in different locations.
Until Jazz supports transactions across regions, the solution is to deploy a single server instance for these sensitive operations.
Examples of when you must deploy on a single instance are:
1. Distribute a limited number of tickets
* Limiting ticket sales so that only 100 tickets are sold for an event.
* The check (“is there space left?”) and ticket creation must happen together, or you risk overselling.
2. Inventory stock deduction
* Managing a product stock count (e.g., 5 items left in store).
* Multiple instances could let multiple buyers purchase the last item at the same time.
3. Sequential ID or token generation
* Generating unique incremental order numbers (e.g., #1001, #1002).
* Multiple instances could produce duplicates if not coordinated.
Single servers are necessary to enforce invariants or provide a consistent view of the data.
As a rule of thumb, when the output of the request depends on the state of the database, you should probably deploy on a single instance.
### Multi-Region Deployment
If your code doesn’t need strict rules to keep data in sync (no counters, no limits, no “check‑then‑update” logic), you can run your workers in many regions at the same time.
This way:
* Users connect to the closest server (faster).
* If one region goes down, others keep running (more reliable).
Examples of when it's acceptable to deploy across multiple regions are:
1. Sending confirmation emails
* After an action is complete, sending an email to the user does not depend on current database state.
2. Pushing notifications
* Broadcasting “event booked” notifications to multiple users can be done from any region.
3. Logging or analytics events
* Recording “user clicked this button” or “page viewed” events, since these are additive and don’t require strict ordering.
4. Calling external APIs (e.g., LLMs, payment confirmations)
* If the response does not modify shared counters or limits, it can be done from any region.
5. Pre-computing cached data or summaries
* Generating read-only previews or cached summaries where stale data is acceptable and does not affect core logic.
Generally speaking, if the output of the request does not depend on the state of the database, you can deploy across multiple regions.
### HTTP requests
# HTTP Requests with Server Workers
HTTP requests are the simplest way to communicate with Server Workers. While they don't provide all the features of [JazzRPC](/docs/server-side/jazz-rpc), they are a good solution when all you need is basic authentication.
They work by generating a short-lived token with `generateAuthToken` and attaching it to the request headers as `Authorization: Jazz `. The server can then verify the token with `authenticateRequest` and get the account that the request was made by.
**Info: Note**
While the token is cryptographically secure, using non secure connections still makes you vulnerable to MITM attacks as - unlike JazzRPC - the request is not signed.
Replay attacks are mitigated by token expiration (default to 1 minute), but it's up to you to ensure that the token is not reused.
It is recommended to use HTTPS whenever possible.
## Creating a Request
You can use any method to create a request; the most common is the `fetch` API.
By default, the token is expected to be in the `Authorization` header in the form of `Jazz `.
```ts
import { generateAuthToken } from "jazz-tools";
const response = await fetch("https://example.com", {
headers: {
Authorization: `Jazz ${generateAuthToken()}`,
},
});
```
## Authenticating requests
You can use the `authenticateRequest` function to authenticate requests.
Attempting to authenticate a request without a token doesn't fail; it returns `account` as `undefined`. For endpoints that **require** authentication, ensure `account` is defined in addition to any permission checks you may need.
```ts
import { authenticateRequest } from "jazz-tools";
import { startWorker } from "jazz-tools/worker";
export async function GET(request: Request) {
const worker = await startWorker({
syncServer: "wss://cloud.jazz.tools/?key=your-api-key",
accountID: process.env.JAZZ_WORKER_ACCOUNT,
accountSecret: process.env.JAZZ_WORKER_SECRET,
asActiveAccount: true,
});
const { account, error } = await authenticateRequest(request);
// There was an error validating the token (e.g., invalid or expired)
if (error) {
return new Response(JSON.stringify(error), { status: 401 });
}
if (!account) {
return new Response("Unauthorized", { status: 401 });
}
return new Response(
JSON.stringify({
message: `The request was made by ${account.$jazz.id}`,
}),
);
}
```
## Multi-account environments
If you are using multiple accounts in your environment - for instance if your server starts multiple workers - or in general if you need to send and authenticate requests as a specific account, you can specify which one to use when generating the token or when authenticating the request.
### Making a request as a specific account
`generateAuthToken` accepts an optional account parameter, so you can generate a token for a specific account.
```ts
const response = await fetch("https://example.com", {
headers: {
Authorization: `Jazz ${generateAuthToken(account)}`,
},
});
```
### Authenticating a request as a specific account
Similarly, specify the account used to verify the token via the `loadAs` option:
```ts
import { authenticateRequest } from "jazz-tools";
import { startWorker } from "jazz-tools/worker";
export async function GET(request: Request) {
const { worker } = await startWorker({
syncServer: "wss://cloud.jazz.tools/?key=your-api-key",
accountID: process.env.JAZZ_WORKER_ACCOUNT,
accountSecret: process.env.JAZZ_WORKER_SECRET,
});
const { account, error } = await authenticateRequest(request, {
loadAs: worker,
});
}
```
## Custom token expiration
You can specify the expiration time of the token using the `expiration` option. The default expiration time is 1 minute.
```ts
import { authenticateRequest } from "jazz-tools";
export async function GET(request: Request) {
const { account, error } = await authenticateRequest(request, {
expiration: 1000 * 60 * 60 * 24, // 24 hours
});
}
```
## Custom token location
While using the `Authorization` header using the `Jazz ` format is the most common way to send the token, you can provide the token in any other way you want.
For example, you can send the token in the `x-jazz-auth-token` header:
```ts
import { generateAuthToken } from "jazz-tools";
const response = await fetch("https://example.com", {
headers: {
"x-jazz-auth-token": generateAuthToken(),
},
});
```
Then you can specify the location of the token using the `getToken` option:
```ts
import { authenticateRequest } from "jazz-tools";
export async function GET(request: Request) {
const { account, error } = await authenticateRequest(request, {
getToken: (request) => request.headers.get("x-jazz-auth-token"),
});
}
```
## Manual token parsing
If you need to manually parse a token from a string, you can use the `parseAuthToken` function.
```ts
import { parseAuthToken, generateAuthToken } from "jazz-tools";
const myToken = generateAuthToken();
const { account, error } = await parseAuthToken(myToken);
```
### Inbox API
# Inbox API with Server Workers
The Inbox API provides a message-based communication system for Server Workers in Jazz.
It works on top of the Jazz APIs and uses sync to transfer messages between the client and the server.
## Setting up the Inbox API
### Define the inbox message schema
Define the inbox message schema in your schema file:
```ts
export const BookTicketMessage = co.map({
type: z.literal("bookTicket"),
event: Event,
});
```
Any kind of CoMap is valid as an inbox message.
### Setting up the Server Worker
Run a server worker and subscribe to the `inbox`:
```ts
import { Account, co, Group } from "jazz-tools";
import { startWorker } from "jazz-tools/worker";
import { BookTicketMessage, Ticket } from "@/lib/schema";
const {
worker,
experimental: { inbox },
} = await startWorker({
accountID: process.env.JAZZ_WORKER_ACCOUNT,
accountSecret: process.env.JAZZ_WORKER_SECRET,
syncServer: "wss://cloud.jazz.tools/?key=your-api-key",
});
inbox.subscribe(BookTicketMessage, async (message, senderID) => {
const madeBy = await co.account().load(senderID, { loadAs: worker });
const { event } = await message.$jazz.ensureLoaded({
resolve: {
event: {
reservations: true,
},
},
});
const ticketGroup = Group.create(worker);
const ticket = Ticket.create({
account: madeBy,
event,
});
if (madeBy.$isLoaded) {
// Give access to the ticket to the client
ticketGroup.addMember(madeBy, "reader");
event.reservations.$jazz.push(ticket);
}
return ticket;
});
```
### Handling multiple message types
`inbox.subscribe` should be called once per worker instance.
If you need to handle multiple message types, you can use the `co.discriminatedUnion` function to create a union of the message types.
```ts
const CancelReservationMessage = co.map({
type: z.literal("cancelReservation"),
event: Event,
ticket: Ticket,
});
export const InboxMessage = co.discriminatedUnion("type", [
BookTicketMessage,
CancelReservationMessage,
]);
```
And check the message type in the handler:
```ts
import { InboxMessage } from "@/lib/schema";
inbox.subscribe(InboxMessage, async (message, senderID) => {
switch (message.type) {
case "bookTicket":
return await handleBookTicket(message, senderID);
case "cancelReservation":
return await handleCancelReservation(message, senderID);
}
});
```
## Sending messages from the client
### Using the Inbox Sender hook
Use `experimental_useInboxSender` to send messages from React components:
```ts
import { co } from "jazz-tools";
import { experimental_useInboxSender } from "jazz-tools/react";
import { BookTicketMessage, Event } from "@/lib/schema";
function EventComponent({ event }: { event: co.loaded }) {
const sendInboxMessage = experimental_useInboxSender(process.env.WORKER_ID);
const [isLoading, setIsLoading] = useState(false);
const onBookTicketClick = async () => {
setIsLoading(true);
const ticketId = await sendInboxMessage(
BookTicketMessage.create({
type: "bookTicket",
event: event,
}),
);
alert(`Ticket booked: ${ticketId}`);
setIsLoading(false);
};
return (
);
}
```
The `sendInboxMessage` API returns a Promise that waits for the message to be handled by a Worker. A message is considered to be handled when the Promise returned by `inbox.subscribe` resolves. The value returned will be the id of the CoValue returned in the `inbox.subscribe` resolved promise.
## Deployment considerations
Multi-region deployments are not supported when using the Inbox API.
If you need to split the workload across multiple regions, you can use the [HTTP API](/docs/server-side/communicating-with-workers/http-requests) instead.
### Server-side rendering
# Add Server-Side Rendering to your App
This guide will take your simple client-side app to the next level by showing you how to create a server-rendered page to publish your data to the world.
**Info:**
If you haven't gone through the [front-end Quickstart](/docs/quickstart), you might find this guide a bit confusing. If you're looking for a quick reference, you might find [this page](/docs/project-setup#ssr-integration) more helpful!
## Creating an agent
For Jazz to access data on the server, we need to create an SSR agent, which is effectively a read-only user which can access public data stored in Jazz.
We can create this user using the `createSSRJazzAgent` function. In this example, we'll create a new file and export the agent, which allows us to import and use the same agent in multiple pages.
\--- Section applies only to react ---
**File name: app/jazzSSR.ts**
\--- End of react specific section ---
\--- Section applies only to svelte ---
**File name: src/lib/jazzSSR.ts**
\--- End of svelte specific section ---
```ts
import { createSSRJazzAgent } from "jazz-tools/ssr";
export const jazzSSR = createSSRJazzAgent({
peer: "wss://cloud.jazz.tools/",
});
```
## Telling Jazz to use the SSR agent
Normally, Jazz expects a logged in user (or an anonymous user) to be accessing data. We can use the `enableSSR` setting to tell Jazz that this may not be the case, and the data on the page may be being accessed by an agent.
\--- Section applies only to react ---
**File name: app/components/JazzWrapper.tsx**
\--- End of react specific section ---
\--- Section applies only to svelte ---
**File name: src/routes/+layout.svelte**
\--- End of svelte specific section ---
##### React:
```tsx
"use client";
import { JazzReactProvider } from "jazz-tools/react";
import { JazzFestAccount } from "./schema";
const apiKey = process.env.NEXT_PUBLIC_JAZZ_API_KEY;
export function JazzWrapper({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
##### Svelte:
```svelte
{@render children?.()}
```
## Making your data public
By default, when you create data in Jazz, it's private and only accessible to the account that created it.
However, the SSR agent is credential-less and unauthenticated, so it can only read data which has been made public. Although Jazz allows you to define [complex, role-based permissions](/docs/permissions-and-sharing/overview), here, we'll focus on making the CoValues public.
**File name: app/schema.ts**
```ts
import { co, z } from "jazz-tools";
export const Band = co
.map({
name: z.string(), // Zod primitive type
})
// [!code ++:3]
.withMigration((band) => {
band.$jazz.owner.makePublic();
});
export const Festival = co.list(Band);
export const JazzFestAccountRoot = co.map({
myFestival: Festival,
});
export const JazzFestAccount = co
.account({
root: JazzFestAccountRoot,
profile: co.profile(),
})
.withMigration(async (account) => {
if (!account.$jazz.has("root")) {
account.$jazz.set("root", {
myFestival: [],
});
// [!code ++:8]
if (account.root.$isLoaded) {
const { myFestival } = await account.root.$jazz.ensureLoaded({
resolve: {
myFestival: true,
},
});
myFestival.$jazz.owner.makePublic();
}
}
});
```
## Creating a server-rendered page
Now let's set up a page which will be read by the agent we created earlier, and rendered fully on the server.
\--- Section applies only to react ---
**File name: app/festival/\[festivalId\]/page.tsx**
\--- End of react specific section ---
\--- Section applies only to svelte ---
**File name: src/routes/festival/\[festivalId\]/+page.svelte**
\--- End of svelte specific section ---
##### React:
```tsx
import { jazzSSR } from "@/app/jazzSSR";
import { Festival } from "@/app/schema";
export default async function ServerSidePage(props: {
params: { festivalId: string };
}) {
const { festivalId } = await props.params;
const festival = await Festival.load(festivalId, {
loadAs: jazzSSR,
resolve: {
$each: {
$onError: "catch",
},
},
});
return (
🎪 Server-rendered Festival {festivalId}
{festival.$isLoaded &&
festival.map((band) => {
if (!band.$isLoaded) return null;
return
🎶 {band.name}
;
})}
);
}
```
##### Svelte:
```svelte
🎪 Server-rendered Festival {festivalId}
{#await festival then festival}
{#each festival.$isLoaded ? festival : [] as band (band.$jazz.id)}
{#if band.$isLoaded}
🎶 {band.name}
{/if}
{/each}
{/await}
```
\--- Section applies only to react ---
**Info:**
TypeScript might not recognise that `params` is a promise. This is a new feature in Next.js 15, which you can [read more about here](https://nextjs.org/docs/messages/sync-dynamic-apis).
\--- End of react specific section ---
## Linking to your server-rendered page
The last step is to link to your server-rendered page from your `Festival` component so that you can find it easily!
\--- Section applies only to react ---
**File name: app/components/Festival.tsx**
\--- End of react specific section ---
\--- Section applies only to svelte ---
**File name: lib/components/Festival.svelte**
\--- End of svelte specific section ---
##### React:
```tsx
"use client";
import { useAccount } from "jazz-tools/react";
// [!code ++:1]
import Link from "next/link";
import { JazzFestAccount } from "@/app/schema";
export function Festival() {
const me = useAccount(JazzFestAccount, {
resolve: { root: { myFestival: { $each: { $onError: "catch" } } } },
});
if (!me.$isLoaded) return null;
return (
<>
{me.root.myFestival.map((band) => {
if (!band.$isLoaded) return null;
return
{band.name}
;
})}
{/* [!code ++:3] */}
Go to my Server-Rendered Festival Page!
>
);
}
```
##### Svelte:
```svelte
{#each me.current.$isLoaded ? me.current.root.myFestival : [] as band}
{#if band.$isLoaded}
{band.name}
{/if}
{/each}
{#if me.current.$isLoaded}
Go to my Server-Rendered Festival Page!
{/if}
```
## Start your app
Let's fire up your app and see if it works!
##### npm:
```bash
npm run dev
```
##### pnpm:
```bash
pnpm run dev
```
If everything's going according to plan, your app will load with the home page. You can click the link to your server-rendered page to see your data - fully rendered on the server!
**Congratulations! 🎉** You've now set up server-side rendering in your React app. You can use this same pattern to render any page on the server.
### Not working?
* Did you add `enableSSR` to the provider?
* Did you add `loadAs: jazzSSR` to `Festival.load`?
* Did you add the migrations to make the data public?
**Info: Still stuck?** Ask for help on [Discord](https://discord.gg/utDMjHYg42)!
\--- Section applies only to react ---
## Bonus: making the server-rendered page dynamic
Just like client-side pages, Jazz can update server-rendered pages in real-time.
For that we can use `export` to serialize values from Jazz and pass them to a client component:
**File name: app/festival/\[festivalId\]/page.tsx**
```tsx
import { jazzSSR } from "@/app/jazzSSR";
import { Festival } from "@/app/schema";
import { FestivalComponent } from "./FestivalComponent";
export default async function ServerSidePage(props: {
params: { festivalId: string };
}) {
const { festivalId } = await props.params;
const festival = await Festival.load(festivalId, {
loadAs: jazzSSR,
resolve: {
$each: {
$onError: "catch",
},
},
});
if (!festival.$isLoaded) return
Festival not found
;
return (
// [!code ++:1]
);
}
```
Then we can pass the exported value to the preloaded option of the `useCoState` hook.
This way Jazz can synchronously hydrate the CoValue data directly from the component props, avoiding the need to load the data:
**File name: app/festival/\[festivalId\]/FestivalComponent.tsx**
```tsx
"use client";
import { Festival } from "@/app/schema";
// [!code ++:2]
import { ExportedCoValue, co } from "jazz-tools";
import { useCoState } from "jazz-tools/react";
// [!code ++:1]
type ExportedFestival = ExportedCoValue>;
export function FestivalComponent(props: { preloaded: ExportedFestival, festivalId: string }) {
const festival = useCoState(Festival, props.festivalId, {
// [!code ++:1]
preloaded: props.preloaded,
resolve: {
$each: {
$onError: "catch",
},
},
});
return (
🎪 Server-rendered Festival {props.festivalId}
{festival.$isLoaded &&
festival.map((band) => {
if (!band.$isLoaded) return null;
return
🎶 {band.name}
;
})}
);
}
```
Now your festival page will update in real-time, without needing to reload the page.
\--- End of react specific section ---
## Next steps
* Learn more about how to [manage complex permissions](/docs/permissions-and-sharing/overview) using groups and roles
* Dive deeper into the collaborative data structures we call [CoValues](/docs/core-concepts/covalues/overview)
* Learn more about migrations in the [accounts and migrations docs](/docs/core-concepts/schemas/accounts-and-migrations)
## Project setup
### Providers
## Tooling & Resources
### create-jazz-app
# create-jazz-app
Jazz comes with a CLI tool that helps you quickly scaffold new Jazz applications. There are two main ways to get started:
1. **Starter templates** \- Pre-configured setups to start you off with your preferred framework
2. **Example apps** \- Extend one of our [example applications](https://jazz.tools/examples) to build your project
## Quick Start with Starter Templates
Create a new Jazz app from a starter template in seconds:
```bash
npx create-jazz-app@latest --api-key YOUR_API_KEY
```
**Info: Tip**
Sign up for a free API key at [dashboard.jazz.tools](https://dashboard.jazz.tools) for higher limits or production use, or use your email address as a temporary key to get started quickly.
\--- Section applies only to react ---
**File name: .env**
\--- End of react specific section ---
\--- Section applies only to svelte ---
**File name: .env**
\--- End of svelte specific section ---
##### React:
```bash
NEXT_PUBLIC_JAZZ_API_KEY="you@example.com" # or your API key
```
##### Svelte:
```bash
PUBLIC_JAZZ_API_KEY="you@example.com" # or your API key
```
This launches an interactive CLI that guides you through selecting:
* Pre-configured frameworks and authentication methods (See [Available Starters](#available-starters))
* Package manager
* Project name
* Jazz Cloud API key (optional) - Provides seamless sync and storage for your app
## Command Line Options
If you know what you want, you can specify options directly from the command line:
```bash
# Basic usage with project name
npx create-jazz-app@latest my-app --framework react --api-key YOUR_API_KEY
# Specify a starter template
npx create-jazz-app@latest my-app --starter react-passkey-auth --api-key YOUR_API_KEY
# Specify example app
npx create-jazz-app@latest my-app --example chat --api-key YOUR_API_KEY
```
### Available Options
* `directory` \- Directory to create the project in (defaults to project name)
* `-f, --framework` \- Framework to use (React, React Native, Svelte)
* `-s, --starter` \- Starter template to use
* `-e, --example` \- Example project to use
* `-p, --package-manager` \- Package manager to use (npm, yarn, pnpm, bun, deno)
* `-k, --api-key` \- Jazz Cloud API key (during our [free public alpha](/docs/core-concepts/sync-and-storage#free-public-alpha), you can use your email as the API key)
* `-h, --help` \- Display help information
## Start From an Example App
Want to start from one of [our example apps](https://jazz.tools/examples)? Our example apps include specific examples of features and use cases. They demonstrate real-world patterns for building with Jazz. Use one as your starting point:
```bash
npx create-jazz-app@latest --example chat
```
## Available Starters
Starter templates are minimal setups that include the basic configuration needed to get started with Jazz. They're perfect when you want a clean slate to build on.
Choose from these ready-to-use starter templates:
* `react-passkey-auth` \- React with Passkey authentication (easiest to start with)
* `react-clerk-auth` \- React with Clerk authentication
* `svelte-passkey-auth` \- Svelte with Passkey authentication
* `rn-clerk-auth` \- React Native with Clerk authentication
Run `npx create-jazz-app --help` to see the latest list of available starters.
## What Happens Behind the Scenes
When you run `create-jazz-app`, we'll:
1. Ask for your preferences (or use your command line arguments)
2. Clone the appropriate starter template
3. Update dependencies to their latest versions
4. Install all required packages
5. Set up your project and show next steps
## Requirements
* Node.js 20.0.0 or later
* Your preferred package manager (npm, yarn, pnpm, bun, or deno)
### Inspector
# Jazz Inspector
[Jazz Inspector](https://inspector.jazz.tools) is a tool to visually inspect a Jazz account or other CoValues.
To pass your account credentials, go to your Jazz app, copy the full JSON from the `jazz-logged-in-secret` local storage key, and paste it into the Inspector's Account ID field.
Alternatively, you can pass the Account ID and Account Secret separately.
\--- Section applies only to react,svelte,vue,vanilla ---
## Exporting current account to Inspector from your app
In development mode, you can launch the Inspector from your Jazz app to inspect your account by pressing `Cmd+J`.
## Embedding the Inspector widget into your app \[!framework=react,svelte,vue,vanilla\]
You can also embed the Inspector directly into your app, so you don't need to open a separate window.
\--- Section applies only to react ---
```tsx
import { JazzInspector } from "jazz-tools/inspector";
import { JazzReactProvider } from "jazz-tools/react";
function App() {
return (
{/* [!code ++] */}
);
}
```
\--- End of react specific section ---
\--- Section applies only to svelte,vue,vanilla ---
Install the custom element and render it.
```ts
import "jazz-tools/inspector/register-custom-element";
document.body.appendChild(document.createElement("jazz-inspector"));
```
Or
```svelte
```
\--- End of svelte,vue,vanilla specific section ---
This will show the Inspector launch button on the right of your page.
\--- End of react,svelte,vue,vanilla specific section ---
\--- Section applies only to react ---
### Positioning the Inspector button \[!framework=react\]
You can also customize the button position with the following options:
* right (default)
* left
* bottom right
* bottom left
* top right
* top left
For example:
```tsx
```
Your app
\--- End of react specific section ---
\--- Section applies only to react ---
Check out the [music player app](https://github.com/garden-co/jazz/blob/main/examples/music-player/src/2%5Fmain.tsx) for a full example.
\--- End of react specific section ---
\--- Section applies only to svelte ---
Check out the [file share app](https://github.com/garden-co/jazz/blob/main/examples/file-share-svelte/src/routes/%2Blayout.svelte) for a full example.
\--- End of svelte specific section ---
### AI tools (llms.txt)
# Using AI to build Jazz apps
AI tools, particularly large language models (LLMs), can accelerate your development with Jazz. Searching docs, responding to questions and even helping you write code are all things that LLMs are starting to get good at.
However, Jazz is a rapidly evolving framework, so sometimes AI might get things a little wrong.
To help the LLMs, we provide the Jazz documentation in a txt file that is optimized for use with AI tools, like Cursor.
\--- Section applies only to react ---
[llms-full.txt](/react/llms-full.txt)
\--- End of react specific section ---
\--- Section applies only to svelte ---
[llms-full.txt](/svelte/llms-full.txt)
\--- End of svelte specific section ---
\--- Section applies only to react-native ---
[llms-full.txt](/react-native/llms-full.txt)
\--- End of react-native specific section ---
\--- Section applies only to react-native-expo ---
[llms-full.txt](/react-native-expo/llms-full.txt)
\--- End of react-native-expo specific section ---
\--- Section applies only to vanilla ---
[llms-full.txt](/vanilla/llms-full.txt)
\--- End of vanilla specific section ---
## Setting up AI tools
Every tool is different, but generally, you'll need to either paste the contents of the [llms-full.txt](https://jazz.tools/llms-full.txt) file directly in your prompt, or attach the file to the tool.
### ChatGPT and v0
Upload the txt file in your prompt.

### Cursor
1. Go to Settings > Cursor Settings > Features > Docs
2. Click "Add new doc"
3. Enter the following URL:
```
https://jazz.tools/llms-full.txt
```
## llms.txt convention
We follow the llms.txt [proposed standard](https://llmstxt.org/) for providing documentation to AI tools at inference time that helps them understand the context of the code you're writing.
## Limitations and considerations
AI is amazing, but it's not perfect. What works well this week could break next week (or be twice as good).
We're keen to keep up with changes in tooling to help support you building the best apps, but if you need help from humans (or you have issues getting set up), please let us know on [Discord](https://discord.gg/utDMjHYg42).
### FAQs
# Frequently Asked Questions
## How established is Jazz?
Jazz is backed by fantastic angel and institutional investors with experience and know-how in devtools and has been in development since 2020.
## Will Jazz be around long-term?
We're committed to Jazz being around for a long time! We understand that when you choose Jazz for your projects, you're investing time and making a significant architectural choice, and we take that responsibility seriously. That's why we've designed Jazz with longevity in mind from the start:
* The open source nature of our sync server means you'll always be able to run your own infrastructure
* Your data remains accessible even if our cloud services change
* We're designing the protocol as an open specification
This approach creates a foundation that can continue regardless of any single company's involvement. The local-first architecture means your apps will always work, even offline, and your data remains yours.
## How secure is my data?
Jazz encrypts all your data by default using modern cryptographic standards. Every transaction is cryptographically signed, and data is encrypted using industry-standard algorithms including BLAKE3 hashing, Ed25519 signatures, and XSalsa20 stream ciphers.
Key features of Jazz's security:
* **Privacy by default**: Your data is encrypted even on Jazz Cloud servers
* **Automatic key rotation**: When members are removed from Groups, encryption keys rotate automatically
* **Verifiable authenticity**: Every change is cryptographically signed
* **Zero-trust architecture**: Only people you explicitly grant access can read your data
For technical details, see our [encryption documentation](/docs/reference/encryption).
## Does Jazz use Non-standard cryptography?
Jazz uses BLAKE3, XSalsa20, and Ed25519, which are all widely published and publicly reviewed standard cryptographic algorithms.
Although we're not lawyers, and so can't give legal advice, we believe that Jazz does not use 'Non-standard cryptography' as defined in the [BIS requirements](https://www.ecfr.gov/current/title-15/subtitle-B/chapter-VII/subchapter-C/part-772#p-772.1%28Non-standard%20cryptography%29) and therefore the requirements for publishing Jazz apps in the Apple App Store.
### Encryption
# Encryption
Jazz uses proven cryptographic primitives in a novel, but simple protocol to implement auditable permissions while allowing real-time collaboration and offline editing.
## How encryption works
Jazz uses proven cryptographic primitives in a novel, but simple protocol to implement auditable permissions while allowing real-time collaboration and offline editing.
### Write permissions: Signing with your keys
When you create or modify CoValues, Jazz cryptographically signs every transaction:
* All transactions are signed with your account's signing keypair
* This proves the transaction came from you
* Whether transactions are valid depends on your permissions in the Group that owns the CoValue
* Groups have internal logic ensuring only admins can change roles or create invites
* You can add yourself to a Group only with a specific role via invites
### Read permissions: Symmetric encryption
Groups use a shared "read key" for encrypting data:
* Admins reveal this symmetric encryption key to accounts with "reader" role or higher
* All transactions in CoValues owned by that Group are encrypted with the current read key
* When someone is removed from a Group, the read key rotates and gets revealed to all remaining members
* CoValues start using the new read key for future transactions
This means removed members can't read new data, but existing data they already had access to remains readable to them.
## Key rotation and security
Jazz automatically handles key management:
* **Member removal triggers rotation**: When you remove someone from a Group, Jazz generates a new read key
* **Seamless transition**: New transactions use the new key immediately
* **No data loss**: Existing members get the new key automatically
## Streaming encryption
Jazz encrypts data efficiently for real-time collaboration:
* **Incremental hashing**: CoValue sessions use [BLAKE3](https://github.com/BLAKE3-team/BLAKE3) for append-only hashing
* **Session signatures**: Each session is signed with [Ed25519](https://ed25519.cr.yp.to/) after each transaction
* **Stream ciphers**: Data is encrypted using [XSalsa20](https://cr.yp.to/salsa20.html) stream cipher
* **Integrity protection**: Hashing and signing ensure data hasn't been tampered with
Although we're not lawyers, and so can't give legal advice, the encryption algorithms used in Jazz are widely published. As a result, we believe that Jazz does not use 'Non-standard cryptography' per the [BIS requirements](https://www.ecfr.gov/current/title-15/subtitle-B/chapter-VII/subchapter-C/part-772#p-772.1%28Non-standard%20cryptography%29) (and therefore the requirements for publishing Jazz apps in the Apple App Store).
## Content addressing
CoValue IDs are the [BLAKE3](https://github.com/BLAKE3-team/BLAKE3) hash of their immutable "header" (containing CoValue type and owning group). This allows CoValues to be "content addressed" while remaining dynamic and changeable.
## What this means for you
**Privacy by default**: Your data is always encrypted, even on Jazz Cloud servers. Only people you explicitly give access to can read your data.
**Flexible permissions**: Use Groups to control exactly who can read, write, or admin your CoValues.
**Automatic security**: Key rotation and encryption happen behind the scenes - you don't need to think about it.
**Verifiable authenticity**: Every change is cryptographically signed, so you always know who made what changes.
## Further reading
* [BLAKE3](https://github.com/BLAKE3-team/BLAKE3) \- append-only hashing
* [Ed25519](https://ed25519.cr.yp.to/) \- signature scheme
* [XSalsa20](https://cr.yp.to/salsa20.html) \- stream cipher for data encryption
### Implementation details
The cryptographic primitives are implemented in the [cojson/src/crypto](https://github.com/garden-co/jazz/tree/main/packages/cojson/src/crypto) package.
Key files to explore:
* [permissions.ts](https://github.com/garden-co/jazz/blob/main/packages/cojson/src/permissions.ts) \- Permission logic
* [permissions.test.ts](https://github.com/garden-co/jazz/blob/main/packages/cojson/src/tests/permissions.test.ts) \- Permission tests
* [verifiedState.ts](https://github.com/garden-co/jazz/blob/main/packages/cojson/src/coValueCore/verifiedState.ts) \- State verification
* [coValueCore.test.ts](https://github.com/garden-co/jazz/blob/main/packages/cojson/src/tests/coValueCore.test.ts) \- Core functionality tests
### Testing
# Testing Jazz Apps
As you develop your Jazz app, you might find yourself needing to test functionality relating to sync, identities, and offline behaviour. The `jazz-tools/testing` utilities provide helpers to enable you to do so.
## Core test helpers
Jazz provides some key helpers that you can use to simplify writing complex tests for your app's functionality.
### `setupJazzTestSync`
This should normally be the first thing you call in your test setup, for example in a `beforeEach` or `beforeAll` block. This function sets up an in-memory sync node for the test session, which is needed in case you want to test data synchronisation functionality. Test data is not persisted, and no clean-up is needed between test runs.
```ts
import { co, z } from "jazz-tools";
import { beforeEach, describe, expect, test } from "vitest";
import {
createJazzTestAccount,
runWithoutActiveAccount,
setActiveAccount,
setupJazzTestSync,
} from "jazz-tools/testing";
const MyAccountSchema = co.account({
profile: co.profile(),
root: co.map({}),
});
describe("My app's tests", () => {
beforeEach(async () => {
await setupJazzTestSync();
});
test("I can create a test account", async () => {
// See below for details on createJazzTestAccount()
const account1 = await createJazzTestAccount({
AccountSchema: MyAccountSchema,
isCurrentActiveAccount: true,
});
expect(account1).not.toBeUndefined();
// ...
});
});
```
### `createJazzTestAccount`
After you've created the initial account using `setupJazzTestSync`, you'll typically want to create user accounts for running your tests.
You can use `createJazzTestAccount()` to create an account and link it to the sync node. By default, this account will become the currently active account (effectively the 'logged in' account).
You can use it like this:
```ts
const account = await createJazzTestAccount({
AccountSchema: MyAccountSchema,
isCurrentActiveAccount: true,
creationProps: {},
});
```
#### `AccountSchema`
This option allows you to provide a custom account schema to the utility to be used when creating the account. The account will be created based on the schema, and all attached migrations will run.
#### `isCurrentActiveAccount`
This option (disabled by default) allows you to quickly switch to the newly created account when it is created.
```ts
const account1 = await createJazzTestAccount({
isCurrentActiveAccount: true,
});
const group1 = co.group().create(); // Group is owned by account1;
const account2 = await createJazzTestAccount();
const group2 = co.group().create(); // Group is still owned by account1;
```
#### `creationProps`
This option allows you to specify `creationProps` for the account which are used during the account creation (and passed to the migration function on creation).
## Managing active Accounts
During your tests, you may need to manage the currently active account after account creation, or you may want to simulate behaviour where there is no currently active account.
### `setActiveAccount`
Use `setActiveAccount()` to switch between active accounts during a test run.
You can use this to test your app with multiple accounts.
```ts
const account1 = await createJazzTestAccount({
isCurrentActiveAccount: true,
});
const account2 = await createJazzTestAccount();
const group1 = co.group().create(); // Group is owned by account1;
group1.addMember(account2, "reader");
const myMap = MyMap.create(
{
text: "Created by account1",
},
{ owner: group1 },
);
const myMapId = myMap.$jazz.id;
setActiveAccount(account2);
// myMap is still loaded as account1, so we need to load again as account2
const myMapFromAccount2 = await MyMap.load(myMapId);
if (myMapFromAccount2.$isLoaded) {
expect(myMapFromAccount2.text).toBe("Created by account1");
expect(() =>
myMapFromAccount2.$jazz.set("text", "Updated by account2"),
).toThrow();
}
```
### `runWithoutActiveAccount`
If you need to test how a particular piece of code behaves when run without an active account.
```ts
const account1 = await createJazzTestAccount({
isCurrentActiveAccount: true,
});
runWithoutActiveAccount(() => {
expect(() => co.group().create()).toThrow(); // can't create new group
});
```
## Managing Context
To test UI components, you may need to create a mock Jazz context.
\--- Section applies only to react ---
In most cases, you'd use this for initialising a provider. You can see how we [initialise a test provider for React tests here](https://github.com/garden-co/jazz/blob/main/packages/jazz-tools/src/react-core/testing.tsx), or see how you could [integrate with @testing-library/react here](https://github.com/garden-co/jazz/blob/main/packages/jazz-tools/src/react-core/tests/testUtils.tsx).
\--- End of react specific section ---
\--- Section applies only to svelte ---
You can render your components for testing by passing a mocked Jazz context to the `@testing-library/svelte` `render` helper.
[You can see an example of how we do that here](https://github.com/garden-co/jazz/blob/main/packages/jazz-tools/src/svelte/tests/testUtils.ts).
\--- End of svelte specific section ---
\--- Section applies only to vanilla ---
The `TestJazzContextManager` mocks the `JazzContextManager` to allow you to instantiate a Jazz context as a user or a guest, allowing you to run tests which depend on an authenticated or a guest session.
You'll normally use either:
* `TestJazzContextManager.fromAccount(account, props?)` to simulate a logged-in context. You can pass `isAuthenticated: false` as an option to simulate an [anonymous user](docs/key-features/authentication/authentication-states#anonymous-authentication).
* `TestJazzContextManager.fromGuest({ guest }, props?)` to simulate a [guest context](/docs/key-features/authentication/authentication-states#guest-mode).
You can also use `TestJazzContextManager.fromAccountOrGuest()` to allow you to pass either.
\--- End of vanilla specific section ---
### Simulating connection state changes
You can use `MockConnectionStatus.setIsConnected(isConnected: boolean)` to simulate disconnected and connected states (depending on whether `isConnected` is set to `true` or `false`).
## Next Steps
You're ready to start writing your own tests for your Jazz apps now. For further details and reference, you can check how we do our testing below.
* [Unit test examples](https://github.com/garden-co/jazz/tree/main/packages/jazz-tools/src/tools/tests)
* [End-to-end examples](https://github.com/garden-co/jazz/tree/main/tests/e2e/tests)
\--- Section applies only to react ---
* [React-specific tests](https://github.com/garden-co/jazz/tree/main/packages/jazz-tools/src/react-core/tests)
\--- End of react specific section ---
\--- Section applies only to svelte ---
* [Svelte-specific tests](https://github.com/garden-co/jazz/tree/main/packages/jazz-tools/src/svelte/tests)
\--- End of svelte specific section ---
### Performance tips
# Tips for maximising Jazz performance
## Use the best crypto implementation for your platform
The fastest implementations are (in order):
1. [Node-API crypto](/docs/server-side/setup#node-api) (only available in some Node/Deno environments) or [RNCrypto](/docs/project-setup/providers#react-native-crypto) and [RNQuickCrypto](/docs/react-native/project-setup/providers#quick-crypto) on React Native
2. [WASM crypto](/docs/server-side/setup#wasm-on-edge-runtimes)
3. JavaScript fallback (slowest, but most compatible)
Check whether your environment supports Node-API. Some edge runtimes may not enable WASM by default.
## Minimise group extensions
Group extensions make it easy to cascade permissions and they’re fast enough for most cases. However, performance can slow down when many parent groups need to load in the dependency chain. To avoid this, create and reuse groups manually when their permissions stay the same for both CoValues over time.
**Note**: Implicit CoValue creation extends groups automatically. Be careful about how you create nested CoValues if you are likely to build long dependency chains.
```ts
const SubSubItem = co.map({
name: z.string(),
});
const SubItem = co.map({
subSubItem: SubSubItem,
});
const Item = co.map({
subItem: SubItem,
});
// Implicit CoValue creation
// Results in Group extension for subItem and subSubItem's owners.
const item = Item.create({
subItem: {
subSubItem: {
name: "Example",
},
},
});
// Explicit CoValue creation
// Does not result in Group extension.
const fasterItem = Item.create({
subItem: SubItem.create({
subSubItem: SubSubItem.create({
name: "Example",
}),
}),
});
// Alternative
const subSubItem = SubSubItem.create({ name: "Example" });
const subItem = SubItem.create({ subSubItem: subSubItem });
const fasterItem = Item.create({ subItem: subItem });
```
You can also configure Jazz to reuse the container CoValue's owner when creating nested CoValues:
```ts
import { setDefaultSchemaPermissions } from "jazz-tools";
setDefaultSchemaPermissions({
onInlineCreate: "sameAsContainer",
});
```
## Choose simple datatypes where possible
CoValues will always be slightly slower to load than their primitive counterparts. For most cases, this is negligible.
In data-heavy apps where lots of data has to be loaded at the same time, you can choose to trade off some of the flexibility of CoValues for speed by opting for primitive data types.
### `z.string()` vs CoTexts
In case you use a CoText, Jazz will enable character-by-character collaboration possibilities for you. However, in many cases, users do not expect to be able to collaborate on the text itself, and are happy with replacing the whole string at once, especially shorter strings. In this case, you could use a `z.string()` for better performance.
Examples:
* names
* URLs
* phone numbers
### `z.object()/z.tuple()` vs CoMaps
CoMaps allow granular updates to objects based on individual keys. If you expect your whole object to be updated at once, you could consider using the `z.object()` or `z.tuple()` type. Note that if you use these methods, you must replace the whole value if you choose to update it.
Examples:
* locations/co-ordinates
* data coming from external sources
* data which is rarely changed after it is created
```ts
const Sprite = co.map({
position: z.object({ x: z.number(), y: z.number() }),
});
const Location = co.map({
position: z.tuple([z.number(), z.number()]),
});
const mySprite = Sprite.create({ position: { x: 10, y: 10 } });
mySprite.$jazz.set("position", { x: 20, y: 20 });
// You cannot update 'x' and 'y' independently, only replace the whole object
const myLocation = Location.create({ position: [26.052, -80.209] });
myLocation.$jazz.set("position", [-33.868, -63.987]);
// Note: you cannot replace a single array element, only replace the whole tuple
```
\--- Section applies only to react ---
### Avoid expensive selectors \[!framework=react\]
Using selectors is a great way to avoid unnecessary re-renders in your app. However, an expensive selector will cause your app to run slowly as the selector will re-run every time the CoValue updates.
In case you need to run expensive computations on your CoValues, [extract this into a separate useMemo call](/docs/react/core-concepts/subscription-and-loading#avoiding-expensive-selectors).
\--- End of react specific section ---
### Forms
# How to write forms with Jazz
This guide shows you a simple and powerful way to implement forms for creating and updating CoValues.
[See the full example here.](https://github.com/garden-co/jazz/tree/main/examples/form)
## Updating a CoValue
To update a CoValue, we simply assign the new value directly as changes happen. These changes are synced to the server.
```tsx
order.$jazz.set("name", e.target.value)}
/>;
```
It's that simple!
## Creating a CoValue
When creating a CoValue, we can use a partial version that allows us to build up the data before submitting.
### Using a Partial CoValue
Let's say we have a CoValue called `BubbleTeaOrder`. We can create a partial version,`PartialBubbleTeaOrder`, which has some fields made optional so we can build up the data incrementally.
**File name: schema.ts**
```ts
import { co, z } from "jazz-tools";
export const BubbleTeaOrder = co.map({
name: z.string(),
});
export type BubbleTeaOrder = co.loaded;
export const PartialBubbleTeaOrder = BubbleTeaOrder.partial();
export type PartialBubbleTeaOrder = co.loaded;
```
## Writing the components in React
Let's write the form component that will be used for both create and update.
```tsx
import { co } from "jazz-tools";
import { BubbleTeaOrder, PartialBubbleTeaOrder } from "./schema";
export function OrderForm({
order,
onSave,
}: {
order: BubbleTeaOrder | PartialBubbleTeaOrder;
onSave?: (e: React.FormEvent) => void;
}) {
return (
);
}
```
### Writing the edit form
To make the edit form, simply pass the `BubbleTeaOrder`. Changes are automatically saved as you type.
```tsx
export function EditOrder(props: { id: string }) {
const order = useCoState(BubbleTeaOrder, props.id);
if (!order.$isLoaded) return;
return ;
}
```
### Writing the create form
For the create form, we need to:
1. Create a partial order.
2. Edit the partial order.
3. Convert the partial order to a "real" order on submit.
Here's how that looks like:
```tsx
export function CreateOrder(props: { id: string }) {
const orders = useAccount(JazzAccount, {
resolve: { root: { orders: true } },
select: (account) => (account.$isLoaded ? account.root.orders : undefined),
});
const newOrder = useCoState(PartialBubbleTeaOrder, props.id);
if (!newOrder.$isLoaded || !orders) return;
const handleSave = (e: React.FormEvent) => {
e.preventDefault();
// Convert to real order and add to the list
// Note: the name field is marked as required in the form, so we can assume that has been set in this case
// In a more complex form, you would need to validate the partial value before storing it
orders.$jazz.push(newOrder as BubbleTeaOrder);
};
return ;
}
```
## Editing with a save button
If you need a save button for editing (rather than automatic saving), you can use Jazz's branching feature. The example app shows how to create a private branch for editing that can be merged back when the user saves:
```tsx
import { Group } from "jazz-tools";
import { useState, useMemo } from "react";
export function EditOrderWithSave(props: { id: string }) {
// Create a new group for the branch, so that every time we open the edit page,
// we create a new private branch
const owner = useMemo(() => Group.create(), []);
const order = useCoState(BubbleTeaOrder, props.id, {
resolve: {
addOns: { $each: true, $onError: "catch" },
instructions: true,
},
unstable_branch: {
name: "edit-order",
owner,
},
});
function handleSave(e: React.FormEvent) {
e.preventDefault();
if (!order.$isLoaded) return;
// Merge the branch back to the original
order.$jazz.unstable_merge();
// Navigate away or show success message
}
function handleCancel() {
// Navigate away without saving - the branch will be discarded
}
if (!order.$isLoaded) return;
return ;
}
```
This approach creates a private branch using `unstable_branch` with a unique owner group. The user can edit the branch without affecting the original data, and changes are only persisted when they click save via `unstable_merge()`.
**Info:**
**Important:** Version control is currently unstable and we may ship breaking changes in patch releases.
## Handling different types of data
Forms can be more complex than just a single string field, so we've put together an example app that shows you how to handle single-select, multi-select, date, boolean inputs, and rich text.
[See the full example here.](https://github.com/garden-co/jazz/tree/main/examples/form)
### Organization/Team
# How to share data between users through Organizations
This guide shows you how to share a set of CoValues between users. Different apps have different names for this concept, such as "teams" or "workspaces".
We'll use the term Organization.
[See the full example here.](https://github.com/garden-co/jazz/tree/main/examples/organization)
## Defining the schema for an Organization
Create a CoMap shared by the users of the same organization to act as a root (or "main database") for the shared data within an organization.
For this example, users within an `Organization` will be sharing `Project`s.
**File name: schema.ts**
```ts
export const Project = co.map({
name: z.string(),
});
export const Organization = co.map({
name: z.string(),
// shared data between users of each organization
projects: co.list(Project),
});
export const ListOfOrganizations = co.list(Organization);
```
Learn more about [defining schemas](/docs/core-concepts/covalues/overview).
## Adding a list of Organizations to the user's Account
Let's add the list of `Organization`s to the user's Account `root` so they can access them.
```tsx
export const JazzAccountRoot = co.map({
organizations: co.list(Organization),
});
export const JazzAccount = co
.account({
root: JazzAccountRoot,
profile: co.profile(),
})
.withMigration((account) => {
if (!account.$jazz.has("root")) {
// Using a Group as an owner allows you to give access to other users
const organizationGroup = Group.create();
const organizations = co.list(Organization).create([
// Create the first Organization so users can start right away
Organization.create(
{
name: "My organization",
projects: co.list(Project).create([], organizationGroup),
},
organizationGroup,
),
]);
account.$jazz.set("root", { organizations });
}
});
```
This schema now allows users to create `Organization`s and add `Project`s to them.
[See the schema for the example app here.](https://github.com/garden-co/jazz/blob/main/examples/organization/src/schema.ts)
## Adding members to an Organization
Here are different ways to add members to an `Organization`.
* Send users an invite link.
* [The user requests to join.](/docs/permissions-and-sharing/sharing#requesting-invites)
This guide and the example app show you the first method.
### Adding members through invite links
Here's how you can generate an [invite link](/docs/permissions-and-sharing/sharing#invites).
When the user accepts the invite, add the `Organization` to the user's `organizations` list.
##### Vanilla:
```ts
import { consumeInviteLink } from "jazz-tools";
consumeInviteLink({
inviteURL: inviteLink,
invitedObjectSchema: Organization, // Pass the schema for the invited object
}).then(async (invitedObject) => {
if (!invitedObject) throw new Error("Failed to consume invite link");
const organization = await Organization.load(invitedObject?.valueID);
me.root.organizations.$jazz.push(organization);
});
```
##### React:
```tsx
import { useAcceptInvite } from "jazz-tools/react";
useAcceptInvite({
invitedObjectSchema: Organization,
onAccept: async (organizationID) => {
const organization = await Organization.load(organizationID);
if (!organization.$isLoaded)
throw new Error("Organization could not be loaded");
me.root.organizations.$jazz.push(organization);
// navigate to the organization page
},
});
```
##### Svelte:
```tsx
```
##### React Native:
```tsx
import { useAcceptInviteNative } from "jazz-tools/react-native";
useAcceptInviteNative({
invitedObjectSchema: Organization,
onAccept: async (organizationID) => {
const organization = await Organization.load(organizationID);
if (!organization.$isLoaded)
throw new Error("Organization could not be loaded");
me.root.organizations.$jazz.push(organization);
// navigate to the organization page
},
});
```
##### Expo:
```ts
import { useAcceptInviteNative } from "jazz-tools/expo";
useAcceptInviteNative({
invitedObjectSchema: Organization,
onAccept: async (organizationID) => {
const organization = await Organization.load(organizationID);
if (!organization.$isLoaded)
throw new Error("Organization could not be loaded");
me.root.organizations.$jazz.push(organization);
// navigate to the organization page
},
});
```
## Further reading
* [Allowing users to request an invite to join a Group](/docs/permissions-and-sharing/sharing#requesting-invites)
* [Groups as permission scopes](/docs/permissions-and-sharing/overview#adding-group-members-by-id)
### History Patterns
# History Patterns
Jazz's automatic history tracking enables powerful patterns for building collaborative features. Here's how to implement common history-based functionality.
## Audit Logs
Build a complete audit trail showing all changes to your data:
```ts
function getAuditLog(task: Task) {
const changes: {
field: string;
value: Task[keyof Task] | undefined;
by: Account | null;
at: Date;
}[] = [];
// Collect edits for all fields
const fields = Object.keys(task);
const edits = task.$jazz.getEdits();
for (const field of fields) {
const editField = field as keyof typeof edits;
if (!edits[editField]) continue;
for (const edit of edits[editField].all) {
changes.push({
field,
value: edit.value,
by: edit.by,
at: edit.madeAt,
});
}
}
// Sort by timestamp (newest first)
return changes.sort((a, b) => b.at.getTime() - a.at.getTime());
}
// Use it to show change history
const auditLog = getAuditLog(task);
auditLog.forEach((entry) => {
if (!entry.by?.profile?.$isLoaded) return;
const when = entry.at.toLocaleString();
const who = entry.by.profile.name;
const what = entry.field;
const value = entry.value;
console.log(`${when} - ${who} changed ${what} to "${value}"`);
// 22/05/2025, 12:00:00 - Alice changed title to "New task"
});
```
## Activity Feeds
Show recent activity across your application:
```ts
function getRecentActivity(projects: Project[], since: Date) {
const activity: {
project: string;
field: string;
value: Task[keyof Task] | undefined;
by: Account | null;
at: Date;
}[] = [];
for (const project of projects) {
// Get all fields that might have edits
const fields = Object.keys(project);
// Check each field for edit history
const edits = project.$jazz.getEdits();
for (const field of fields) {
const editField = field as keyof typeof edits;
// Skip if no edits exist for this field
if (!edits[editField]) continue;
for (const edit of edits[editField].all) {
// Only include edits made after the 'since' date
if (edit.madeAt > since) {
activity.push({
project: project.name,
field,
value: edit.value,
by: edit.by,
at: edit.madeAt,
});
}
}
}
}
return activity.sort((a, b) => b.at.getTime() - a.at.getTime());
}
// Show activity from the last hour
const hourAgo = new Date(Date.now() - 60 * 60 * 1000);
const recentActivity = getRecentActivity(myProjects, hourAgo);
// [{
// project: "New project",
// field: "name",
// value: "New project",
// by: Account,
// at: Date
// }]
```
## Change Indicators
Show when something was last updated:
```ts
function getLastUpdated(task: Task) {
// Find the most recent edit across all fields
let lastEdit: CoMapEdit | null = null;
const edits = task.$jazz.getEdits();
for (const field of Object.keys(task)) {
const editField = field as keyof typeof edits;
// Skip if no edits exist for this field
if (!edits[editField]) continue;
const fieldEdit = edits[editField];
if (fieldEdit && (!lastEdit || fieldEdit.madeAt > lastEdit.madeAt)) {
lastEdit = fieldEdit;
}
}
if (!lastEdit || !lastEdit.by?.profile?.$isLoaded) return null;
return {
updatedBy: lastEdit.by.profile.name,
updatedAt: lastEdit.madeAt,
message: `Last updated by ${lastEdit.by.profile.name} at ${lastEdit.madeAt.toLocaleString()}`,
};
}
const lastUpdated = getLastUpdated(task);
console.log(lastUpdated?.message);
// "Last updated by Alice at 22/05/2025, 12:00:00"
```
## Finding Specific Changes
Query history for specific events:
```ts
// Find when a task was completed
function findCompletionTime(task: Task): Date | null {
const statusEdits = task.$jazz.getEdits().status;
if (!statusEdits) return null;
// find() returns the FIRST completion time
// If status toggles (completed → in-progress → completed),
// this gives you the earliest completion, not the latest
const completionEdit = statusEdits.all.find(
(edit) => edit.value === "completed",
);
return completionEdit?.madeAt || null;
}
// To get the LATEST completion time instead reverse the array, then find:
function findLatestCompletionTime(task: Task): Date | null {
const statusEdits = task.$jazz.getEdits().status;
if (!statusEdits) return null;
// Reverse and find (stops at first match)
const latestCompletionEdit = statusEdits.all
.slice() // Create copy to avoid mutating original
.reverse()
.find((edit) => edit.value === "completed");
return latestCompletionEdit?.madeAt || null;
}
console.log(findCompletionTime(task)); // First completion
console.log(findLatestCompletionTime(task)); // Most recent completion
// Find who made a specific change
function findWhoChanged(task: Task, field: string, value: any) {
const taskEdits = task.$jazz.getEdits();
const fieldEdits = taskEdits[field as keyof typeof taskEdits];
if (!fieldEdits) return null;
const matchingEdit = fieldEdits.all.find((edit) => edit.value === value);
return matchingEdit?.by || null;
}
const account = findWhoChanged(task, "status", "completed");
if (account?.profile?.$isLoaded) {
console.log(account.profile.name);
}
// Alice
```
## Further Reading
* [History](/docs/key-features/history) \- Complete reference for the history API
* [Subscription & Loading](/docs/core-concepts/subscription-and-loading) \- Ensure CoValues are loaded before accessing history
## Resources
- [Documentation](https://jazz.tools/docs): Detailed documentation about Jazz
- [Examples](https://jazz.tools/examples): Code examples and tutorials
## music-player Example
### 1_schema.ts
```ts
import { co, Group, z } from "jazz-tools";
/** Walkthrough: Defining the data model with CoJSON
*
* Here, we define our main data model of tasks, lists of tasks and projects
* using CoJSON's collaborative map and list types, CoMap & CoList.
*
* CoMap values and CoLists items can contain:
* - arbitrary immutable JSON
* - other CoValues
**/
export const MusicTrackWaveform = co.map({
data: z.array(z.number()),
});
export type MusicTrackWaveform = co.loaded;
export const MusicTrack = co.map({
/**
* Attributes are defined using zod schemas
*/
title: z.string(),
duration: z.number(),
/**
* You can define relations between coValues using the other CoValue schema
* You can mark them optional using z.optional()
*/
waveform: MusicTrackWaveform,
/**
* In Jazz you can upload files using FileStream.
*
* As for any other coValue the music files we put inside FileStream
* is available offline and end-to-end encrypted 😉
*/
file: co.fileStream(),
isExampleTrack: z.optional(z.boolean()),
});
export type MusicTrack = co.loaded;
export const Playlist = co.map({
title: z.string(),
tracks: co.list(MusicTrack), // CoList is the collaborative version of Array
});
export type Playlist = co.loaded;
export const PlaylistWithTracks = Playlist.resolved({
tracks: { $each: true },
});
export type PlaylistWithTracks = co.loaded;
/** The account root is an app-specific per-user private `CoMap`
* where you can store top-level objects for that user */
export const MusicaAccountRoot = co.map({
// The root playlist works as container for the tracks that
// the user has uploaded
rootPlaylist: Playlist,
// Here we store the list of playlists that the user has created
// or that has been invited to
playlists: co.list(Playlist),
// We store the active track and playlist as coValue here
// so when the user reloads the page can see the last played
// track and playlist
// You can also add the position in time if you want make it possible
// to resume the song
activeTrack: co.optional(MusicTrack),
activePlaylist: Playlist,
exampleDataLoaded: z.optional(z.boolean()),
accountSetupCompleted: z.optional(z.boolean()),
});
export type MusicaAccountRoot = co.loaded;
export const MusicaAccountProfile = co
.profile({
avatar: co.optional(co.image()),
})
.withMigration((profile) => {
if (profile.$jazz.owner.getRoleOf("everyone") !== "reader") {
profile.$jazz.owner.addMember("everyone", "reader");
}
});
export type MusicaAccountProfile = co.loaded;
export const MusicaAccount = co
.account({
/** the default user profile with a name */
profile: MusicaAccountProfile,
root: MusicaAccountRoot,
})
.withMigration(async (account) => {
/**
* The account migration is run on account creation and on every log-in.
* You can use it to set up the account root and any other initial CoValues you need.
*/
if (!account.$jazz.has("root")) {
const rootPlaylist = Playlist.create({
tracks: [],
title: "",
});
account.$jazz.set("root", {
rootPlaylist,
playlists: [],
activeTrack: undefined,
activePlaylist: rootPlaylist,
exampleDataLoaded: false,
});
}
if (!account.$jazz.has("profile")) {
account.$jazz.set(
"profile",
MusicaAccountProfile.create(
{
name: "",
},
Group.create().makePublic(),
),
);
}
});
export type MusicaAccount = co.loaded;
export const MusicaAccountWithPlaylists = MusicaAccount.resolved({
root: {
playlists: {
$each: { $onError: "catch", ...PlaylistWithTracks.resolveQuery },
},
},
});
/** Walkthrough: Continue with ./2_main.tsx */
```
### 2_main.tsx
```tsx
import { Toaster } from "@/components/ui/toaster";
import { JazzInspector } from "jazz-tools/inspector";
/* eslint-disable react-refresh/only-export-components */
import React from "react";
import ReactDOM from "react-dom/client";
import { RouterProvider, createHashRouter } from "react-router-dom";
import { HomePage } from "./3_HomePage";
import { useMediaPlayer } from "./5_useMediaPlayer";
import { InvitePage } from "./6_InvitePage";
import { WelcomeScreen } from "./components/WelcomeScreen";
import "./index.css";
import { MusicaAccount } from "@/1_schema";
import { apiKey } from "@/apiKey.ts";
import { SidebarProvider } from "@/components/ui/sidebar";
import { JazzReactProvider } from "jazz-tools/react";
import { onAnonymousAccountDiscarded } from "./4_actions";
import { KeyboardListener } from "./components/PlayerControls";
import { usePrepareAppState } from "./lib/usePrepareAppState";
import {
AccountProvider,
useAccountSelector,
} from "@/components/AccountProvider.tsx";
/**
* Walkthrough: The top-level provider ``
*
* This shows how to use the top-level provider ``,
* which provides the rest of the app with a controlled account (used through `useAccount` later).
* Here we use `DemoAuth` which is great for prototyping you app without wasting time on figuring out
* the best way to do auth.
*
* `` also runs our account migration
*/
function AppContent({
mediaPlayer,
}: {
mediaPlayer: ReturnType;
}) {
const showWelcomeScreen = useAccountSelector({
select: (me) => !me.$isLoaded || !me.root.accountSetupCompleted,
});
const isReady = usePrepareAppState(mediaPlayer);
// Show welcome screen if account setup is not completed
if (showWelcomeScreen) {
return ;
}
const router = createHashRouter([
{
path: "/",
element: ,
},
{
path: "/playlist/:playlistId",
element: ,
},
{
path: "/invite/*",
element: ,
},
]);
if (!isReady) return null;
return (
<>
>
);
}
function Main() {
const mediaPlayer = useMediaPlayer();
return (
);
}
const peer =
(new URL(window.location.href).searchParams.get(
"peer",
) as `ws://${string}`) ?? `wss://cloud.jazz.tools/?key=${apiKey}`;
ReactDOM.createRoot(document.getElementById("root")!).render(
,
);
```
### 3_HomePage.tsx
```tsx
import { useParams } from "react-router";
import { PlaylistWithTracks } from "./1_schema";
import { uploadMusicTracks } from "./4_actions";
import { MediaPlayer } from "./5_useMediaPlayer";
import { FileUploadButton } from "./components/FileUploadButton";
import { MusicTrackRow } from "./components/MusicTrackRow";
import { PlayerControls } from "./components/PlayerControls";
import { EditPlaylistModal } from "./components/EditPlaylistModal";
import { PlaylistMembers } from "./components/PlaylistMembers";
import { MemberAccessModal } from "./components/MemberAccessModal";
import { SidePanel } from "./components/SidePanel";
import { Button } from "./components/ui/button";
import { SidebarInset, SidebarTrigger } from "./components/ui/sidebar";
import { usePlayState } from "./lib/audio/usePlayState";
import { useState } from "react";
import { useAccountSelector } from "@/components/AccountProvider.tsx";
import { useSuspenseCoState } from "jazz-tools/react-core";
export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
const playState = usePlayState();
const isPlaying = playState.value === "play";
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isMembersModalOpen, setIsMembersModalOpen] = useState(false);
async function handleFileLoad(files: FileList) {
/**
* Follow this function definition to see how we update
* values in Jazz and manage files!
*/
await uploadMusicTracks(files);
}
const params = useParams<{ playlistId: string }>();
const playlistId = useAccountSelector({
select: (me) => params.playlistId ?? me.root.$jazz.refs.rootPlaylist.id,
});
const playlist = useSuspenseCoState(PlaylistWithTracks, playlistId);
const membersIds = playlist.$jazz.owner.members.map((member) => member.id);
const isRootPlaylist = !params.playlistId;
const canEdit = useAccountSelector({
select: (me) => Boolean(playlist && me.canWrite(playlist)),
});
const isActivePlaylist = useAccountSelector({
select: (me) =>
me.$isLoaded && playlistId === me.root.activePlaylist?.$jazz.id,
});
const handlePlaylistShareClick = () => {
setIsMembersModalOpen(true);
};
const handleEditClick = () => {
setIsEditModalOpen(true);
};
return (
);
}
```
### components/RenameTrackDialog.tsx
```tsx
import { MusicTrack } from "@/1_schema";
import { updateMusicTrackTitle } from "@/4_actions";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { useState } from "react";
import { ConfirmDialog } from "./ConfirmDialog";
interface EditTrackDialogProps {
track: MusicTrack;
isOpen: boolean;
onOpenChange: (open: boolean) => void;
onDelete: () => void;
}
export function EditTrackDialog({
track,
isOpen,
onOpenChange,
onDelete,
}: EditTrackDialogProps) {
const [newTitle, setNewTitle] = useState(track.title);
const [isDeleteConfirmOpen, setIsDeleteConfirmOpen] = useState(false);
function handleSave() {
if (track && newTitle.trim()) {
updateMusicTrackTitle(track, newTitle.trim());
onOpenChange(false);
}
}
function handleCancel() {
setNewTitle(track?.title || "");
onOpenChange(false);
}
function handleDeleteClick() {
setIsDeleteConfirmOpen(true);
}
function handleDeleteConfirm() {
onDelete();
onOpenChange(false);
}
function handleKeyDown(event: React.KeyboardEvent) {
if (event.key === "Enter") {
handleSave();
} else if (event.key === "Escape") {
handleCancel();
}
}
return (
);
}
```
### components/SidePanel.tsx
```tsx
import { MusicaAccount } from "@/1_schema";
import { deletePlaylist } from "@/4_actions";
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
import { useAccount } from "jazz-tools/react";
import { Home, Music, Plus, Trash2 } from "lucide-react";
import { useNavigate, useParams } from "react-router";
import { useState } from "react";
import { AuthButton } from "./AuthButton";
import { CreatePlaylistModal } from "./CreatePlaylistModal";
export function SidePanel() {
const { playlistId } = useParams();
const navigate = useNavigate();
const playlists = useAccount(MusicaAccount, {
resolve: { root: { playlists: { $each: { $onError: "catch" } } } },
select: (me) => (me.$isLoaded ? me.root.playlists : undefined),
});
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
function handleAllTracksClick() {
navigate(`/`);
}
function handlePlaylistClick(playlistId: string) {
navigate(`/playlist/${playlistId}`);
}
async function handleDeletePlaylist(playlistId: string) {
if (confirm("Are you sure you want to delete this playlist?")) {
await deletePlaylist(playlistId);
navigate(`/`);
}
}
function handleCreatePlaylistClick() {
setIsCreateModalOpen(true);
}
function handlePlaylistCreated(playlistId: string) {
navigate(`/playlist/${playlistId}`);
}
return (
<>
Music Player
Go to all tracksPlaylistsAdd a new playlist
{playlists?.map(
(playlist) =>
playlist.$isLoaded && (
handlePlaylistClick(playlist.$jazz.id)}
isActive={playlist.$jazz.id === playlistId}
>