# Jazz
## Documentation
### Getting started
#### Introduction
# Learn some Jazz
**Jazz is a toolkit for building backendless apps**. You get data without needing a database — plus auth, permissions, files and multiplayer without needing a backend. Jazz lets you do everything right from the frontend and you'll ship better apps, faster.
Instead of wrestling with databases, APIs, and server infrastructure, you work with **CoValues** ("collaborative values") — your new cloud-synced building blocks that feel like local state but automatically sync across all devices and users in real-time.
---
## Quickstart
You can use [`create-jazz-app`](/docs/tools/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
```
Requires at least Node.js v20.
{/*
Or you can follow this [React step-by-step guide](/docs/react/guide) where we walk you through building an issue tracker app.
*/}
## Why Jazz is different
Most apps rebuild the same thing: shared state that syncs between users and devices. Jazz starts from that shared state, giving you:
- **No backend required** — Focus on building features, not infrastructure
- **Real-time sync** — Changes appear everywhere immediately
- **Multiplayer by default** — Collaboration just works
- **Local-first** — Your app works offline and feels instant
Think Figma, Notion, or Linear — but you don't need years to build a custom stack.
## How it works
1. **Define your data** with CoValues schemas
2. **Connect to sync infrastructure** (Jazz Cloud or self-hosted)
3. **Create and edit CoValues** like normal objects
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.
## Ready to see Jazz in action?
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.
## Core concepts
Learn how to structure your data using [collaborative values](/docs/schemas/covalues) — the building blocks that make Jazz apps work.
## Sync and storage
Sync and persist your data by setting up [sync and storage infrastructure](/docs/sync-and-storage) using Jazz Cloud, or host it yourself.
## Going deeper
Get better results with AI by [importing the Jazz docs](/docs/ai-tools) into your context window.
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.
#### Example apps
#### 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/resources/encryption).
### Project setup
#### Installation
### react-native-expo Implementation
# React Native (Expo) Installation and Setup
Jazz supports Expo through the dedicated `jazz-tools/expo` entry, which is specifically designed for Expo applications. If you're building for React Native without Expo, please refer to the [React Native](/docs/react-native/project-setup) guide instead.
Jazz requires an [Expo development build](https://docs.expo.dev/develop/development-builds/introduction/) using [Expo Prebuild](https://docs.expo.dev/workflow/prebuild/) for native code. It is **not compatible** with Expo Go. Jazz also supports the [New Architecture](https://docs.expo.dev/guides/new-architecture/).
Tested with:
```json
"expo": "~53.0.0",
"react-native": "0.79.2",
"react": "18.3.1"
```
## Installation
### Create a new project
(Skip this step if you already have one)
```bash
npx create-expo-app -e with-router-tailwind my-jazz-app
cd my-jazz-app
npx expo prebuild
```
### Install dependencies
```bash
# Expo dependencies
npx expo install expo-linking expo-secure-store expo-sqlite expo-file-system @react-native-community/netinfo @bam.tech/react-native-image-resizer
# React Native polyfills
npm i -S @azure/core-asynciterator-polyfill react-native-url-polyfill readable-stream react-native-get-random-values
# Jazz dependencies
npm i -S jazz-tools
```
- Requires at least Node.js v20.
- Hermes has added support for `atob` and `btoa` in React Native 0.74. If you are using earlier versions, you may also need to polyfill `atob` and `btoa` in your `package.json`. Packages to try include `text-encoding` and `base-64`, and you can drop `@bacons/text-decoder`.
#### Fix incompatible dependencies
If you encounter incompatible dependencies, you can try to fix them with the following command:
```bash
npx expo install --fix
```
### Configure Metro
#### Regular repositories
If you are not working within a monorepo, create a new file `metro.config.js` in the root of your project with the following content:
```ts twoslash
// @noErrors: 2304
// metro.config.js
const { getDefaultConfig } = require("expo/metro-config");
const config = getDefaultConfig(__dirname);
config.resolver.sourceExts = ["mjs", "js", "json", "ts", "tsx"];
config.resolver.requireCycleIgnorePatterns = [/(^|\/|\\)node_modules($|\/|\\)/];
module.exports = config;
```
#### Monorepos
For monorepos, use the following `metro.config.js`:
```ts twoslash
// @noErrors: 2304
// metro.config.js
const { getDefaultConfig } = require("expo/metro-config");
const { FileStore } = require("metro-cache");
const path = require("path");
// eslint-disable-next-line no-undef
const projectRoot = __dirname;
const workspaceRoot = path.resolve(projectRoot, "../..");
const config = getDefaultConfig(projectRoot);
config.watchFolders = [workspaceRoot];
config.resolver.nodeModulesPaths = [
path.resolve(projectRoot, "node_modules"),
path.resolve(workspaceRoot, "node_modules"),
];
config.resolver.sourceExts = ["mjs", "js", "json", "ts", "tsx"];
config.resolver.requireCycleIgnorePatterns = [/(^|\/|\\)node_modules($|\/|\\)/];
config.cacheStores = [
new FileStore({
root: path.join(projectRoot, "node_modules", ".cache", "metro"),
}),
];
module.exports = config;
```
### Additional monorepo configuration (for pnpm)
If you're using `pnpm`, you'll need to make sure that your expo app's `package.json` has this:
```json
// package.json
{
"main": "index.js",
...
}
```
For more information, refer to [this Expo monorepo example](https://github.com/byCedric/expo-monorepo-example#pnpm-workarounds).
### Add polyfills
Create a file `polyfills.js` at the project root with the following content:
```ts twoslash
// @noErrors: 7016
// polyfills.js
polyfillGlobal("ReadableStream", () => ReadableStream); // polyfill ReadableStream
```
Update `index.js`:
```ts twoslash
// @noErrors: 2307
// index.js
AppRegistry.registerComponent(appName, () => App);
```
Lastly, ensure that the `"main"` field in your `package.json` points to `index.js`:
```json
// package.json
{
"main": "index.js",
...
}
```
## Authentication
Jazz provides authentication to help users access their data across multiple devices. For details on implementing authentication with Expo, check our [Authentication Overview](/docs/authentication/overview) guide and see the [Expo Clerk Demo](https://github.com/garden-co/jazz/tree/main/examples/clerk-expo) for a complete example.
## Next Steps
Now that you've set up your Expo project for Jazz, you'll need to:
1. [Set up the Jazz Provider](/docs/react-native-expo/project-setup/providers) - Configure how your app connects to Jazz
2. [Add authentication](/docs/authentication/overview) (optional) - Enable users to access data across devices
3. Define your schema - See the [schema docs](/docs/schemas/covalues) for more information
4. Run your app:
```sh
npx expo run:ios
# or
npx expo run:android
```
## Verification
Ready to see if everything's working? Let's fire up your app:
```sh
npx expo run:ios
# or
npx expo run:android
```
If all goes well, your app should start up without any angry red error screens. Take a quick look at the Metro console too - no Jazz-related errors there means you're all set! If you see your app's UI come up smoothly, you've nailed the installation.
If you run into any issues that aren't covered in the Common Issues section, [drop by our Discord for help](https://discord.gg/utDMjHYg42).
## Common Issues
- **Metro bundler errors**: If you see errors about missing polyfills, ensure all polyfills are properly imported.
- **iOS build failures**: Make sure you've run `pod install` after adding the dependencies.
- **Android build failures**: Ensure you've run `npx expo prebuild` to generate native code.
- **Expo Go incompatibility**: Remember that Jazz requires a development build and won't work with Expo Go.
### Install CocoaPods
If you're compiling for iOS, you'll need to install CocoaPods for your project. If you need to install it, we recommend using [`pod-install`](https://www.npmjs.com/package/pod-install):
```bash
npx pod-install
```
---
### react-native Implementation
# React Native Installation and Setup
This guide covers setting up Jazz for React Native applications from scratch. If you're using Expo, please refer to the [React Native - Expo](/docs/react-native-expo/project-setup) guide instead. If you just want to get started quickly, you can use our [React Native Chat Demo](https://github.com/garden-co/jazz/tree/main/examples/chat-rn) as a starting point.
Jazz supports the [New Architecture](https://reactnative.dev/architecture/landing-page) for React Native.
Tested with:
```json
"react-native": "0.79.2",
"react": "18.3.1"
```
## Installation
### Create a new project
(Skip this step if you already have one)
```bash
npx @react-native-community/cli init myjazzapp
cd myjazzapp
```
If you intend to build for iOS, you can accept the invitation to install CocoaPods. If you decline, or you get an error, [you can install it with `pod-install`](#install-cocoapods).
### Install dependencies
```bash
# React Native dependencies
npm install @react-native-community/netinfo @bam.tech/react-native-image-resizer
# React Native polyfills
npm i -S @azure/core-asynciterator-polyfill react-native-url-polyfill readable-stream react-native-get-random-values @op-engineering/op-sqlite react-native-mmkv
# Jazz dependencies
npm i -S jazz-tools
```
- Requires at least Node.js v20.
- Hermes has added support for `atob` and `btoa` in React Native 0.74. If you are using earlier versions, you may also need to polyfill `atob` and `btoa` in your `package.json`. Packages to try include `text-encoding` and `base-64`, and you can drop `@bacons/text-decoder`.
### Configure Metro
#### Regular repositories
If you are not working within a monorepo, create a new file `metro.config.js` in the root of your project with the following content:
```ts twoslash
// metro.config.js
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
const config = {
resolver: {
sourceExts: ["mjs", "js", "json", "ts", "tsx"],
requireCycleIgnorePatterns: [/(^|\/|\\)node_modules($|\/|\\)/]
}
};
module.exports = mergeConfig(getDefaultConfig(__dirname), config);
```
#### Monorepos
For monorepos, use the following `metro.config.js`:
```ts twoslash
// metro.config.js
const path = require("path");
const { makeMetroConfig } = require("@rnx-kit/metro-config");
const MetroSymlinksResolver = require("@rnx-kit/metro-resolver-symlinks");
// Define workspace root
const projectRoot = __dirname;
const workspaceRoot = path.resolve(projectRoot, "../..");
// Add packages paths
const extraNodeModules = {
modules: path.resolve(workspaceRoot, "node_modules"),
};
const watchFolders = [
path.resolve(workspaceRoot, "node_modules"),
path.resolve(workspaceRoot, "packages"),
];
const nodeModulesPaths = [
path.resolve(projectRoot, "node_modules"),
path.resolve(workspaceRoot, "node_modules"),
];
module.exports = makeMetroConfig({
resolver: {
resolveRequest: MetroSymlinksResolver(),
extraNodeModules,
nodeModulesPaths,
sourceExts: ["mjs", "js", "json", "ts", "tsx"],
},
watchFolders,
});
```
### Additional monorepo configuration (for pnpm)
- Add `node-linker=hoisted` to the root `.npmrc` (create this file if it doesn't exist).
- Add the following to the root `package.json`:
```json
// package.json
"pnpm": {
"peerDependencyRules": {
"ignoreMissing": [
"@babel/*",
"typescript"
]
}
}
```
### Add polyfills
Create a file `polyfills.js` at the project root with the following content:
```ts twoslash
// @noErrors: 7016
// polyfills.js
polyfillGlobal("ReadableStream", () => ReadableStream); // polyfill ReadableStream
```
Update `index.js`:
```ts twoslash
// @noErrors: 2307
// index.js
AppRegistry.registerComponent(appName, () => App);
```
Lastly, ensure that the `"main"` field in your `package.json` points to `index.js`:
```json
// package.json
{
"main": "index.js",
...
}
```
## Authentication
Jazz provides authentication to help users access their data across multiple devices. For details on implementing authentication, check our [Authentication Overview](/docs/authentication/overview) guide and see the [React Native Chat Demo](https://github.com/garden-co/jazz/tree/main/examples/chat-rn) for a complete example.
## Next Steps
Now that you've set up your React Native project for Jazz, you'll need to:
1. [Set up the Jazz Provider](/docs/react-native/project-setup/providers) - Configure how your app connects to Jazz
2. [Add authentication](/docs/authentication/overview) (optional) - Enable users to access data across devices
3. Define your schema - See the [schema docs](/docs/schemas/covalues) for more information
4. Run your app:
```sh
npx react-native run-ios
npx react-native run-android
```
## Verification
Ready to see if everything's working? Let's fire up your app:
```sh
npx react-native run-ios
# or
npx react-native run-android
```
If all goes well, your app should start up without any angry red error screens. Take a quick look at the Metro console too - no Jazz-related errors there means you're all set! If you see your app's UI come up smoothly, you've nailed the installation.
If you run into any issues that aren't covered in the Common Issues section, [drop by our Discord for help](https://discord.gg/utDMjHYg42).
## Common Issues
- **Metro bundler errors**: If you see errors about missing polyfills, ensure all polyfills are properly imported in your `polyfills.js` file.
- **iOS build failures**: Make sure you've run `pod install` after adding the dependencies.
- **Android build failures**: Ensure your Android SDK and NDK versions are compatible with the native modules.
### Install CocoaPods
If you're compiling for iOS, you'll need to install CocoaPods for your project. If you need to install it, we recommend using [`pod-install`](https://www.npmjs.com/package/pod-install):
```bash
npx pod-install
```
---
### react Implementation
# React Installation and Setup
Add Jazz to your React application in minutes. This setup covers standard React apps, Next.js, and gives an overview of experimental SSR approaches.
Integrating Jazz with React is straightforward. You'll define data schemas that describe your application's structure, then wrap your app with a provider that handles sync and storage. The whole process takes just three steps:
1. [Install dependencies](#install-dependencies)
2. [Write your schema](#write-your-schema)
3. [Wrap your app in ``](#standard-react-setup)
Looking for complete examples? Check out our [example applications](/examples) for chat apps, collaborative editors, and more.
## Install dependencies
First, install the required packages:
```bash
pnpm install jazz-tools
```
Requires at least Node.js v20.
## Write your schema
Define your data schema using [CoValues](/docs/schemas/covalues) from `jazz-tools`.
```tsx twoslash
// schema.ts
export const TodoItem = co.map({
title: z.string(),
completed: z.boolean(),
});
export const AccountRoot = co.map({
todos: co.list(TodoItem),
});
export const MyAppAccount = co.account({
root: AccountRoot,
profile: co.map({ name: z.string() }),
});
```
See [CoValues](/docs/schemas/covalues) for more information on how to define your schema.
## Standard React Setup
Wrap your application with `` to connect to the Jazz network and define your data schema:
```tsx twoslash
// @filename: schema.ts
export const TodoItem = co.map({
title: z.string(),
completed: z.boolean(),
});
export const AccountRoot = co.map({
todos: co.list(TodoItem),
});
export const MyAppAccount = co.account({
root: AccountRoot,
profile: co.map({ name: z.string() }),
});
// @filename: app.tsx
function App() {
return
Hello, world!
;
}
// ---cut---
// app.tsx
createRoot(document.getElementById("root")!).render(
);
```
This setup handles:
- Connection to the Jazz sync server
- Schema registration for type-safe data handling
- Local storage configuration
With this in place, you're ready to start using Jazz hooks in your components. [Learn how to access and update your data](/docs/using-covalues/subscription-and-loading#subscription-hooks).
## Next.js Integration
Normally Jazz doesn't render the children until the account is loaded.
On the server there is no account, but we can ask Jazz to render the children using an "empty agent".
An agent is a read-only account that can be used without credentials to render the data available to the public.
```tsx twoslash
// @filename: schema.ts
export const TodoItem = co.map({
title: z.string(),
completed: z.boolean(),
});
export const AccountRoot = co.map({
todos: co.list(TodoItem),
});
export const MyAppAccount = co.account({
root: AccountRoot,
profile: co.map({ name: z.string() }),
});
// @filename: app.tsx
// ---cut---
export function JazzWrapper({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
Since the agent used is empty, all the `useCoState` and `useAccount` will return null.
If you want to render the data on the server you can use `createSSRJazzAgent` to generate a read-only account to load the data:
```tsx twoslash
// @filename: schema.ts
export const TodoItem = co.map({
title: z.string(),
completed: z.boolean(),
});
export const AccountRoot = co.map({
todos: co.list(TodoItem),
});
export const MyAppAccount = co.account({
root: AccountRoot,
profile: co.map({ name: z.string() }),
});
// @filename: app.tsx
// ---cut---
// This can be created in a centralized module, to reuse the same agent in all the server components
export const jazzSSR = createSSRJazzAgent({
peer: "wss://cloud.jazz.tools/",
});
export default async function ServerSidePage(props: {
params: Promise<{ itemId: string }>;
}) {
const { itemId } = await props.params;
const item = await TodoItem.load(itemId, {
loadAs: jazzSSR,
});
return (
SSR rendering example with Jazz
This is a server component!
);
}
```
Take a look on our [Next.js example](https://github.com/garden-co/jazz/tree/main/examples/jazz-nextjs) to see a complete example of how to use SSR with Jazz.
## Further Reading
- [Schemas](/docs/schemas/covalues) - Learn about defining your data model
- [Provider Configuration](/docs/project-setup/providers) - Learn about other configuration options for Providers
- [Authentication](/docs/authentication/overview) - Set up user authentication
- [Sync and Storage](/docs/sync-and-storage) - Learn about data persistence
---
### server-side Implementation
# Node.JS / server workers
The main detail to understand when using Jazz server-side is that Server Workers have Jazz `Accounts`, just like normal users do.
This lets you share CoValues with Server Workers, having precise access control by adding the Worker to `Groups` with specific roles just like you would with other users.
[See the full example here.](https://github.com/garden-co/jazz/tree/main/examples/jazz-paper-scissors)
Requires at least Node.js v20.
## 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.
## Storing & providing credentials
Server Worker credentials are typically stored and provided as environmental variables.
**Take extra care with the Account Secret — handle it like any other secret environment variable such as a DB password.**
## Starting a server worker
You can use `startWorker` from `jazz-nodejs` to start 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 it's private account root)
- takes a URL for a sync & storage server
`startWorker` expects credentials in the `JAZZ_WORKER_ACCOUNT` and `JAZZ_WORKER_SECRET` environment variables by default (as printed by `npx account create ...`), but you can also pass them manually as `accountID` and `accountSecret` parameters if you get them from elsewhere.
```ts twoslash
const MyWorkerAccount = co.account();
type MyWorkerAccount = co.loaded;
// ---cut---
const { worker } = await startWorker({
AccountSchema: MyWorkerAccount,
syncServer: 'wss://cloud.jazz.tools/?key=you@example.com',
});
```
`worker` acts like `me` (as returned by `useAccount` on the client) - you can use it to:
- load/subscribe to CoValues: `MyCoValue.subscribe(id, worker, {...})`
- create CoValues & Groups `const val = MyCoValue.create({...}, { owner: worker })`
## Using CoValues instead of requests
Just like traditional backend functions, you can use Server Workers to do useful stuff (computations, calls to third-party APIs etc.) and put the results back into CoValues, which subscribed clients automatically get notified about.
What's less clear is how you can trigger this work to happen.
- One option is to define traditional HTTP API handlers that use the Jazz Worker internally. This is helpful if you need to mutate Jazz state in response to HTTP requests such as for webhooks or non-Jazz API clients
- The other option is to have the Jazz Worker subscribe to CoValues which they will then collaborate on with clients.
- A common pattern is to implement a state machine represented by a CoValue, where the client will do some state transitions (such as `draft -> ready`), which the worker will notice and then do some work in response, feeding the result back in a further state transition (such as `ready -> success & data`, or `ready -> failure & error details`).
- This way, client and worker don't have to explicitly know about each other or communicate directly, but can rely on Jazz as a communication mechanism - with computation progressing in a distributed manner wherever and whenever possible.
---
### svelte Implementation
# Svelte Installation and Setup
Jazz can be used with Svelte or in a SvelteKit app.
To add some Jazz to your Svelte app, you can use the following steps:
## Install Jazz dependencies
```sh
pnpm install jazz-tools
```
Requires at least Node.js v20.
## Write your schema
See the [schema docs](/docs/schemas/covalues) for more information.
```ts
// src/lib/schema.ts
export class MyProfile extends Profile {
name = coField.string;
counter = coField.number; // This will be publically visible
}
export class MyAccount extends Account {
profile = coField.ref(MyProfile);
// ...
}
```
## Set up the Provider in your root layout
```svelte
```
## Use Jazz hooks in your components
```svelte
```
For a complete example of Jazz with Svelte, check out our [file sharing example](https://github.com/gardencmp/jazz/tree/main/examples/file-share-svelte) which demonstrates, Passkey authentication, file uploads and access control.
#### Sync and storage
# Sync and storage: Jazz Cloud or self-hosted
## Using Jazz Cloud
Simply use `wss://cloud.jazz.tools/?key=...` as the sync server URL.
Jazz Cloud will
- sync CoValues in real-time between users and devices
- safely persist CoValues on redundant storage nodes with additional backups
- make use of geographically distributed cache nodes for low latency
### Free public alpha
- Jazz Cloud is free during the public alpha, with no strict usage limits
- We plan to keep a free tier, so you'll always be able to get started with zero setup
- See [Jazz Cloud pricing](/cloud#pricing) for more details
- ⚠️ Please use a valid email address as your API key.
Your full sync server URL should look something like
```wss://cloud.jazz.tools/?key=you@example.com```
Once we support per-app API keys, we'll email you an API key you can use instead.
## Self-hosting your sync server
You can run your own sync server using:
```sh
npx jazz-run sync
```
And then use `ws://localhost:4200` as the sync server URL.
You can also run this simple sync server behind a proxy that supports WebSockets, for example to provide TLS.
In this case, provide the WebSocket endpoint your proxy exposes as the sync server URL.
### Command line options:
- `--port` / `-p` - the port to run the sync server on. Defaults to 4200.
- `--in-memory` - keep CoValues in-memory only and do sync only, no persistence. Persistence is enabled by default.
- `--db` - the path to the file where to store the data (SQLite). Defaults to `sync-db/storage.db`.
### Source code
The implementation of this simple sync server is available open-source [on GitHub](https://github.com/garden-co/jazz/blob/main/packages/jazz-run/src/startSyncServer.ts).
#### Node.JS / server workers
# Node.JS / server workers
The main detail to understand when using Jazz server-side is that Server Workers have Jazz `Accounts`, just like normal users do.
This lets you share CoValues with Server Workers, having precise access control by adding the Worker to `Groups` with specific roles just like you would with other users.
[See the full example here.](https://github.com/garden-co/jazz/tree/main/examples/jazz-paper-scissors)
Requires at least Node.js v20.
## 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.
## Storing & providing credentials
Server Worker credentials are typically stored and provided as environmental variables.
**Take extra care with the Account Secret — handle it like any other secret environment variable such as a DB password.**
## Starting a server worker
You can use `startWorker` from `jazz-nodejs` to start 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 it's private account root)
- takes a URL for a sync & storage server
`startWorker` expects credentials in the `JAZZ_WORKER_ACCOUNT` and `JAZZ_WORKER_SECRET` environment variables by default (as printed by `npx account create ...`), but you can also pass them manually as `accountID` and `accountSecret` parameters if you get them from elsewhere.
```ts twoslash
const MyWorkerAccount = co.account();
type MyWorkerAccount = co.loaded;
// ---cut---
const { worker } = await startWorker({
AccountSchema: MyWorkerAccount,
syncServer: 'wss://cloud.jazz.tools/?key=you@example.com',
});
```
`worker` acts like `me` (as returned by `useAccount` on the client) - you can use it to:
- load/subscribe to CoValues: `MyCoValue.subscribe(id, worker, {...})`
- create CoValues & Groups `const val = MyCoValue.create({...}, { owner: worker })`
## Using CoValues instead of requests
Just like traditional backend functions, you can use Server Workers to do useful stuff (computations, calls to third-party APIs etc.) and put the results back into CoValues, which subscribed clients automatically get notified about.
What's less clear is how you can trigger this work to happen.
- One option is to define traditional HTTP API handlers that use the Jazz Worker internally. This is helpful if you need to mutate Jazz state in response to HTTP requests such as for webhooks or non-Jazz API clients
- The other option is to have the Jazz Worker subscribe to CoValues which they will then collaborate on with clients.
- A common pattern is to implement a state machine represented by a CoValue, where the client will do some state transitions (such as `draft -> ready`), which the worker will notice and then do some work in response, feeding the result back in a further state transition (such as `ready -> success & data`, or `ready -> failure & error details`).
- This way, client and worker don't have to explicitly know about each other or communicate directly, but can rely on Jazz as a communication mechanism - with computation progressing in a distributed manner wherever and whenever possible.
#### Providers
### react-native-expo Implementation
# Providers
`` is the core component that connects your Expo application to Jazz. It handles:
- **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/schemas/accounts-and-migrations)
- **Authentication**: Connects your authentication system to Jazz
## Setting up the provider
Wrap your app components with the `` component:
```tsx twoslash
// @noErrors: 2307 7031 2304 2686 2664
// App.tsx
export function MyJazzProvider({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
## Provider Options
- `kvStore`
- `ExpoSecureStoreAdapter` (default)
- `AccountSchema`
- `Account` (default)
- `CryptoProvider`
- `PureJSCrypto` (default) - Pure JavaScript crypto provider
- `RNQuickCrypto` - C++ accelerated crypto provider
## Authentication in 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/authentication/overview) guide.
The authentication hooks must always be used inside the `` 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
```tsx twoslash
// @noErrors: 2307
function SignInScreen({ auth }: { auth: any }) {
return null;
}
// ---cut-before---
// Example with PassphraseAuth
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}
);
}
```
For a complete example, see the [Expo Chat Demo](https://github.com/garden-co/jazz/tree/main/examples/chat-rn-expo).
## Local Persistence
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
Local persistence is enabled by default with no additional configuration required. Your data will automatically persist across app restarts.
## 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 `JazzExpoProvider`:
```tsx twoslash
// @noErrors: 2307 2322
function SignInScreen({ auth }: { auth: any }) {
return null;
}
// ---cut-before---
function MyJazzProvider({ children }: { children: ReactNode }) {
return (
{children}
);
}
```
For configuration, use the RNQC Expo config plugin:
```json twoslash
// app.json
{
"expo": {
"plugins": [
[
"react-native-quick-crypto",
{
"sodiumEnabled": true
}
]
]
}
}
```
---
### react-native Implementation
# Providers
`` is the core component that connects your React Native application to Jazz. It handles:
- **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/schemas/accounts-and-migrations)
- **Authentication**: Connects your authentication system to Jazz
## Setting up the provider
Wrap your app components with the `` component:
```tsx twoslash
// @noErrors: 2307 2686 2664
// App.tsx
export function MyJazzProvider({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
## Provider Options
- `kvStore`
- `MMKVStoreAdapter` (default)
- `AccountSchema`
- `Account` (default)
- `CryptoProvider`
- `PureJSCrypto` (default) - Pure JavaScript crypto provider
- `RNQuickCrypto` - C++ accelerated crypto provider
## Authentication in 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/authentication/overview) guide.
The authentication hooks must always be used inside the `` 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
```tsx twoslash
// @noErrors: 2307
function SignInScreen({ auth }: { auth: any }) {
return null;
}
// ---cut-before---
// Example with PassphraseAuth
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
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
Local persistence is enabled by default with no additional configuration required. Your data will automatically persist across app restarts.
## 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 `JazzReactNativeProvider`:
```tsx twoslash
// @noErrors: 2307 2322
function SignInScreen({ auth }: { auth: any }) {
return null;
}
// ---cut-before---
function MyJazzProvider({ children }: { children: ReactNode }) {
return (
{children}
);
}
```
For configuration, add the following settings:
```ruby twoslash
# ios/Podfile
ENV['SODIUM_ENABLED'] = '1'
```
and
```groovy twoslash
// android/gradle.properties
sodiumEnabled=true
```
---
### react Implementation
# Providers
`` is the core component that connects your React application to Jazz. It handles:
- **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/schemas/accounts-and-migrations)
- **Authentication**: Connects your authentication system to Jazz
Our [Chat example app](https://jazz.tools/examples#chat) provides a complete implementation of JazzReactProvider with authentication and real-time data sync.
## Setting up the Provider
The `` accepts several configuration options:
```tsx twoslash
// @filename: schema.ts
export const TodoItem = co.map({
title: z.string(),
completed: z.boolean(),
});
export const AccountRoot = co.map({
todos: co.list(TodoItem),
});
export const MyAppAccount = co.account({
root: AccountRoot,
profile: co.map({ name: z.string() }),
});
// @filename: app.tsx
// ---cut---
// App.tsx
export function MyApp({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
## Provider Options
### Sync Options
The `sync` property configures how your application connects to the Jazz network:
```tsx twoslash
const syncConfig: SyncConfig = {
// Connection to Jazz Cloud or your own sync server
peer: "wss://cloud.jazz.tools/?key=your-api-key",
// When to sync: "always" (default), "never", or "signedUp"
when: "always",
}
```
See [Authentication States](/docs/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:
```tsx twoslash
// @filename: schema.ts
export const TodoItem = co.map({
title: z.string(),
completed: z.boolean(),
});
export const AccountRoot = co.map({
todos: co.list(TodoItem),
});
export const MyAppAccount = co.account({
root: AccountRoot,
profile: co.map({ name: z.string() }),
});
// @filename: app.tsx
const syncConfig: SyncConfig = {
peer: "wss://cloud.jazz.tools/?key=your-api-key",
when: "always",
}
// ---cut---
// app.tsx
export function MyApp ({ children }: { children: React.ReactNode }) {
// Use in provider
return (
{children}
);
}
```
### Additional Options
The provider accepts these additional options:
```tsx twoslash
const syncConfig: SyncConfig = {
peer: "wss://cloud.jazz.tools/?key=your-api-key",
when: "always",
}
// ---cut---
// app.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.id);
// Migrate data here
return Promise.resolve();
}}
>
{children}
);
}
```
See [Authentication States](/docs/authentication/authentication-states) for more information on authentication states, guest mode, and handling anonymous accounts.
## Authentication
`` 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/authentication/overview).
## 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.
---
### svelte Implementation
# Providers
`` is the core component that connects your Svelte application to Jazz. It handles:
- **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/schemas/accounts-and-migrations)
- **Authentication**: Connects your authentication system to Jazz
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.
## Setting up the Provider
The `` accepts several configuration options:
```svelte
{@render children()}
```
## Provider Options
### Sync Options
The `sync` property configures how your application connects to the Jazz network:
```ts twoslash
// @filename: src/routes/layout.svelte
// ---cut---
const syncConfig: SyncConfig = {
// Connection to Jazz Cloud or your own sync server
peer: "wss://cloud.jazz.tools/?key=your-api-key",
// When to sync: "always" (default), "never", or "signedUp"
when: "always",
}
```
See [Authentication States](/docs/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:
```svelte
>
{@render children()}
```
### Additional Options
The provider accepts these additional options:
```svelte
{@render children}
```
See [Authentication States](/docs/authentication/authentication-states) for more information on authentication states, guest mode, and handling anonymous accounts.
## Authentication
`` 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/authentication/overview).
## 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.
### Tools
#### AI tools
# 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.
llms-full.txt
## 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).
#### 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 you@example.com
```
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 you@example.com
# Specify a starter template
npx create-jazz-app@latest my-app --starter react-passkey-auth --api-key you@example.com
# Specify example app
npx create-jazz-app@latest my-app --example chat --api-key you@example.com
```
### 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/react/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.
[https://inspector.jazz.tools](https://inspector.jazz.tools)
## Exporting current account to Inspector from your app [!framework=react,svelte,vue,vanilla]
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.
```tsx
// [!code ++]
```
Install the custom element and render it.
```ts
document.body.appendChild(document.createElement("jazz-inspector"))
```
Or
```tsx
```
This will show the Inspector launch button on the right of your page.
### 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
Check out the [music player app](https://github.com/garden-co/jazz/blob/main/examples/music-player/src/2_main.tsx) for a full example.
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.
### Upgrade guides
#### 0.15.0 - Everything inside `jazz-tools`
# Jazz 0.15.0 - Moving everything inside `jazz-tools`
One of the pain points that our adopters have been facing while maintaining Jazz apps is keeping the different package versions aligned.
This becomes especially hard when using a monorepo setup for multi-platform apps.
To address this problem, we have decided to move all the bindings into a single package and export the different bindings using export paths.
## Overview:
So far, when building a Jazz app you would need to use multiple packages.
Now everything is inside `jazz-tools`:
```tsx
export const TodoItem = co.map({
name: z.string(),
});
export function TodoComponent({
todoId
}: {
todoId: string;
}) {
const todo = useCoState(TodoItem, todoId);
return (
(todo.name = e.target.value)} />
);
}
```
## Major breaking changes
- A single package for everything: `jazz-tools` now exports all the bindings using export paths
- Support for Vue is discontinued: maintaining multiple framework bindings is hard and we have decided to focus our efforts on the most used frameworks in our community
- A single package for everything: `jazz-tools` now exports all the bindings using export paths
- Support for Vue is discontinued: maintaining multiple framework bindings is hard and we have decided to focus our efforts on the most used frameworks in our community
- SSR is now stable: We have removed the experimental flag from `enableSSR` and unified `useAccount` and `useAccountOrGuest` hooks
### A single package for all
All our stable packages that were depending on `jazz-tools` are now part of the `jazz-tools` package:
- `jazz-react` -> `jazz-tools/react`
- `jazz-react-native` -> `jazz-tools/react-native`
- `jazz-expo` -> `jazz-tools/expo`
- `jazz-svelte` -> `jazz-tools/svelte`
- `jazz-nodejs` -> `jazz-tools/worker`
- `jazz-react-auth-clerk` -> `jazz-tools/react`
- `jazz-expo/auth/clerk` -> `jazz-tools/expo`
- `jazz-browser` -> `jazz-tools/browser`
- `jazz-browser-media-images` -> `jazz-tools/browser-media-images`
- `jazz-react-native-media-images` -> `jazz-tools/react-native-media-images`
- `jazz-inspector` -> `jazz-tools/inspector`
- `jazz-inspector-element` -> `jazz-tools/inspector/register-custom-element`
- `jazz-prosemirror` -> `jazz-tools/prosemirror`
- `jazz-tiptap` -> `jazz-tools/tiptap`
This means that now you can remove all these packages and keep only `jazz-tools`.
To reduce the probability of importing an API from the wrong entry, we have also done some renaming:
- `JazzProvider` becomes:
- `JazzReactProvider` in `jazz-tools/react`
- `JazzSvelteProvider` in `jazz-tools/svelte`
- `JazzReactNativeProvider` in `jazz-tools/react-native`
- `JazzExpoProvider` in `jazz-tools/expo`
- Clerk bindings are now exported directly from the framework entries and renamed as:
- `import { JazzReactProviderWithClerk } from "jazz-tools/react"`
- `import { JazzExpoProviderWithClerk } from "jazz-tools/expo"`
- We have added the `Native` suffix to the platform-specific hooks when imported from the `react-native` or `expo` entries:
- `useAcceptInvite` -> `useAcceptInviteNative`
- `useProgressiveImg` -> `useProgressiveImgNative`
- `ProgressiveImg` -> `ProgressiveImgNative`
- `useAcceptInvite` becomes `new InviteListener` in Svelte
We have not renamed APIs like `useCoState` or `useAccount` because their implementation is cross-platform.
When experimenting with VSCode, we've noticed that usually the import path suggested is `jazz-tools/react-core`, which is the same as importing from `jazz-tools/react` or the specific framework entry.
To make this upgrade possible we have fully transtioned to [Package Exports](https://nodejs.org/docs/latest-v18.x/api/packages.html#package-entry-points) in `jazz-tools`.
This means that if you are using an old version of React Native you will need to enable the [enablePackageExports](https://metrobundler.dev/docs/configuration/#unstable_enablepackageexports-experimental) flag in your metro config.
### SSR is now stable
The `enableSSR` was marked as experimental because the `useAccount` return type was not matching the returned values in SSR mode.
In this release, we have refactored `useAccount` to make "me" always nullable, matching what happens when rendering on the server.
We have also removed `useAccountOrGuest` and made `useAccount` return an `agent` value that can be used to load data in all rendering modes.
For more details, check the [SSR docs](/docs/react/project-setup#nextjs-integration).
#### 0.14.0 - Zod-based schemas
# Jazz 0.14.0 Introducing Zod-based schemas
We're excited to move from our own schema syntax to using Zod v4.
This is the first step in a series of releases to make Jazz more familiar and to make CoValues look more like regular data structures.
## Overview:
So far, Jazz has relied on our own idiosyncratic schema definition syntax where you had to extend classes and be careful to use `co.ref` for references.
```ts
// BEFORE
export class Message extends CoMap {
text = co.ref(CoPlainText);
image = co.optional.ref(ImageDefinition);
important = co.boolean;
}
export class Chat extends CoList.Of(co.ref(Message)) {}
```
While this had certain ergonomic benefits it relied on unclean hacks to work.
In addition, many of our adopters expressed a preference for avoiding class syntax, and LLMs consistently expected to be able to use Zod.
For this reason, we completely overhauled how you define and use CoValue schemas:
```ts
// AFTER
export const Message = co.map({
text: co.plainText(),
image: z.optional(co.image()),
important: z.boolean(),
});
export const Chat = co.list(Message);
```
## Major breaking changes
### Schema definitions
You now define CoValue schemas using two new exports from `jazz-tools`:
- a new `co` definer that mirrors Zod's object/record/array syntax to define CoValue types
- `co.map()`, `co.record()`, `co.list()`, `co.feed()`
- `co.account()`, `co.profile()`
- `co.plainText()`, `co.richText()`,
- `co.fileStream()`, `co.image()`
- see the updated [Defining CoValue Schemas](/docs/schemas/covalues)
- `z` re-exported from Zod v4
- primitives like `z.string()`, `z.number()`, `z.literal()`
- **note**: additional constraints like `z.min()` and `z.max()` are not yet enforced, we'll add validation in future releases
- complex types like `z.object()` and `z.array()` to define JSON-like fields without internal collaboration
- combinators like `z.optional()` and `z.discriminatedUnion()`
- these also work on CoValue types!
- see the updated [Docs on Primitive Fields](/docs/schemas/covalues#primitive-fields),
[Docs on Optional References](/docs/schemas/covalues#optional-references)
and [Docs on Unions of CoMaps](/docs/schemas/covalues#unions-of-comaps-declaration)
Similar to Zod v4's new object syntax, recursive and mutually recursive types are now [much easier to express](/docs/react/schemas/covalues#recursive-references).
### How to pass loaded CoValues
Calls to `useCoState()` work just the same, but they return a slightly different type than before.
And while you can still read from the type just as before...
```tsx
const Pet = co.map({
name: z.string(),
age: z.number(),
});
type Pet = co.loaded;
const Person = co.map({
name: z.string(),
age: z.number(),
pets: co.list(Pet),
});
type Person = co.loaded;
function MyComponent({ id }: { id: string }) {
const person = useCoState(Person, id);
return person && ;
}
function PersonName({ person }: {
person: Person
}) {
return
{person.name}
;
}
```
`co.loaded` can also take a second argument to specify the loading depth of the expected CoValue, mirroring the `resolve` options for `useCoState`, `load`, `subscribe`, etc.
```tsx
const Pet = co.map({
name: z.string(),
age: z.number(),
});
type Pet = co.loaded;
const Person = co.map({
name: z.string(),
age: z.number(),
pets: co.list(Pet),
});
type Person = co.loaded;
function MyComponent({ id }: { id: string }) {
const personWithPets = useCoState(Person, id, {
resolve: { pets: { $each: true } } // [!code ++]
});
return personWithPets && ;
}
function PersonAndFirstPetName({ person }: {
person: co.loaded // [!code ++]
}) {
return
{person.name} & {person.pets[0].name}
;
}
```
We've removed the `useCoState`, `useAccount` and `useAccountOrGuest` hooks.
You should now use the `CoState` and `AccountCoState` reactive classes instead. These provide greater stability and are significantly easier to work with.
Calls to `new CoState()` work just the same, but they return a slightly different type than before.
And while you can still read from the type just as before...
```ts
// @filename: schema.ts
const Pet = co.map({
name: z.string(),
age: z.number(),
});
type Pet = co.loaded;
const Person = co.map({
name: z.string(),
age: z.number(),
pets: co.list(Pet),
});
type Person = co.loaded;
```
```svelte
// @filename: app.svelte
{person.current?.name}
```
`co.loaded` can also take a second argument to specify the loading depth of the expected CoValue, mirroring the `resolve` options for `CoState`, `load`, `subscribe`, etc.
```svelte
{props.person.name}
{#each props.person.pets as pet}
{pet.name}
{/each}
```
### Removing AccountSchema registration
We have removed the Typescript AccountSchema registration.
It was causing some deal of confusion to new adopters so we have decided to replace the magic inference with a more explicit approach.
You still need to pass your custom AccountSchema to your provider!
```tsx
declare module "jazz-react" { // [!code --]
interface Register { // [!code --]
Account: MyAccount; // [!code --]
} // [!code --]
} // [!code --]
export function MyApp({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
When using `useAccount` you should now pass the `Account` schema directly:
```tsx
function MyComponent() {
const { me } = useAccount(MyAccount, {
resolve: {
profile: true,
},
});
return
{me?.profile.name}
;
}
```
```svelte
// [!code --]
{@render children()}
```
When using `AccountCoState` you should now pass the `Account` schema directly:
```svelte
{account.current?.profile.name}
```
### Defining migrations
Now account schemas need to be defined with `co.account()` and migrations can be declared using `withMigration()`:
```ts
const Pet = co.map({
name: z.string(),
age: z.number(),
});
const MyAppRoot = co.map({
pets: co.list(Pet),
});
const MyAppProfile = co.profile({
name: z.string(),
age: z.number().optional(),
});
export const MyAppAccount = co.account({
root: MyAppRoot,
profile: MyAppProfile,
}).withMigration((account, creationProps?: { name: string }) => {
if (account.root === undefined) {
account.root = MyAppRoot.create({
pets: co.list(Pet).create([]),
});
}
if (account.profile === undefined) {
const profileGroup = Group.create();
profileGroup.addMember("everyone", "reader");
account.profile = MyAppProfile.create({
name: creationProps?.name ?? "New user",
}, profileGroup);
}
});
```
### Defining Schema helper methods
You can no longer define helper methods directly within your schema, create standalone functions instead. See
[Docs on Helper methods](/docs/schemas/covalues#helper-methods) for an example.
## Minor breaking changes
### `_refs` and `_edits` are now potentially null
The type of `_refs` and `_edits` is now nullable.
```ts
const Person = co.map({
name: z.string(),
age: z.number(),
});
const person = Person.create({ name: "John", age: 30 });
person._refs; // now nullable
person._edits; // now nullable
```
### `members` and `by` now return basic `Account`
We have removed the Account schema registration, so now `members` and `by` methods now always return basic `Account`.
This means that you now need to rely on `useCoState` on them to load their using your account schema.
```tsx
function GroupMembers({ group }: { group: Group }) {
const members = group.members;
return (
);
}
```
### Defining schemas
#### CoValues
# 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_replicated_data_type).
- 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/using-covalues/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.
```ts twoslash
// schema.ts
const ListOfTasks = co.list(z.string());
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 twoslash
// @filename: schema.ts
export const ListOfTasks = co.list(z.string());
export const TodoProject = co.map({
title: z.string(),
tasks: ListOfTasks,
});
// @filename: app.ts
// ---cut---
// app.ts
const project = TodoProject.create(
{
title: "New Project",
tasks: ListOfTasks.create([], Group.create()),
},
Group.create()
);
```
## 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 twoslash
// schema.ts
// ---cut---
const Task = co.map({
title: z.string(),
completed: z.boolean(),
});
```
Or record-like CoMaps (key-value pairs, where keys are always `string`):
```ts twoslash
const Fruit = co.map({
name: z.string(),
color: z.string(),
});
// ---cut---
const ColorToHex = co.record(z.string(), z.string());
const ColorToFruit = co.record(z.string(), Fruit);
```
See the corresponding sections for [creating](/docs/using-covalues/comaps#creating-comaps),
[subscribing/loading](/docs/using-covalues/subscription-and-loading),
[reading from](/docs/using-covalues/comaps#reading-from-comaps) and
[updating](/docs/using-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 twoslash
const Task = co.map({
title: z.string(),
completed: z.boolean(),
});
// ---cut---
const ListOfColors = co.list(z.string());
const ListOfTasks = co.list(Task);
```
See the corresponding sections for [creating](/docs/using-covalues/colists#creating-colists),
[subscribing/loading](/docs/using-covalues/subscription-and-loading),
[reading from](/docs/using-covalues/colists#reading-from-colists) and
[updating](/docs/using-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 twoslash
const Task = co.map({
title: z.string(),
completed: z.boolean(),
});
// ---cut---
const FeedOfTasks = co.feed(Task);
```
See the corresponding sections for [creating](/docs/using-covalues/cofeeds#creating-cofeeds),
[subscribing/loading](/docs/using-covalues/subscription-and-loading),
[reading from](/docs/using-covalues/cofeeds#reading-from-cofeeds) and
[writing to](/docs/using-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 twoslash
// ---cut---
const Document = co.map({
title: z.string(),
file: co.fileStream(),
});
```
See the corresponding sections for [creating](/docs/using-covalues/filestreams#creating-filestreams),
[subscribing/loading](/docs/using-covalues/subscription-and-loading),
[reading from](/docs/using-covalues/filestreams#reading-from-filestreams) and
[writing to](/docs/using-covalues/filestreams#writing-to-filestreams) FileStreams.
**Note: For images, we have a special, higher-level `co.image()` helper, see [ImageDefinition](/docs/using-covalues/imagedef).**
### Unions of CoMaps (declaration)
You can declare unions of CoMaps that have discriminating fields, using `z.discriminatedUnion()`.
```ts twoslash
// ---cut---
const ButtonWidget = co.map({
type: z.literal("button"),
label: z.string(),
});
const SliderWidget = co.map({
type: z.literal("slider"),
min: z.number(),
max: z.number(),
});
const WidgetUnion = z.discriminatedUnion("type", [ButtonWidget, SliderWidget]);
```
See the corresponding sections for [creating](/docs/using-covalues/schemaunions#creating-schemaunions),
[subscribing/loading](/docs/using-covalues/subscription-and-loading) and
[narrowing](/docs/using-covalues/schemaunions#narrowing) SchemaUnions.
## 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/)):
```ts twoslash
const Person = co.map({
title: z.string(),
})
export const ListOfColors = co.list(z.string());
```
Here's a quick overview of the primitive types you can use:
```ts twoslash
// ---cut---
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 twoslash
// ---cut---
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 twoslash
// ---cut---
const Sprite = 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/using-covalues/subscription-and-loading).
In Schemas, you declare references by just using the schema of the referenced CoValue:
```ts twoslash
// ---cut---
// schema.ts
const Person = co.map({
name: z.string(),
});
const ListOfPeople = co.list(Person);
const Company = co.map({
members: ListOfPeople,
});
```
#### Optional References
You can make references optional with `z.optional()`:
```ts twoslash
const Pet = co.map({
name: z.string(),
});
// ---cut---
const Person = co.map({
pet: z.optional(Pet),
});
```
#### Recursive References
You can refer to the same schema from within itself using getters:
```ts twoslash
// ---cut---
const Person = co.map({
name: z.string(),
get bestFriend() {
return Person;
}
});
```
You can use the same technique for mutually recursive references, but you'll need to help TypeScript along:
```ts twoslash
// ---cut---
const Person = co.map({
name: z.string(),
get friends(): CoListSchema {
return ListOfPeople;
}
});
const ListOfPeople = co.list(Person);
```
Note: similarly, if you use modifiers like `z.optional()` you'll need to help TypeScript along:
```ts twoslash
// ---cut---
const Person = co.map({
name: z.string(),
get bestFriend(): z.ZodOptional {
return z.optional(Person);
}
});
```
### 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 twoslash
function differenceInYears(date1: Date, date2: Date) {
const diffTime = Math.abs(date1.getTime() - date2.getTime());
return Math.ceil(diffTime / (1000 * 60 * 60 * 24 * 365.25));
}
// ---cut---
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}`;
}
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 twoslash
// ---cut---
const Person = co.map({
firstName: z.string(),
lastName: z.string(),
});
type Person = co.loaded;
export function updatePersonName(person: Person, fullName: string) {
const [firstName, lastName] = fullName.split(" ");
person.firstName = firstName;
person.lastName = lastName;
}
const person = Person.create({
firstName: "John",
lastName: "Doe",
});
console.log(person.firstName, person.lastName) // John Doe
updatePersonName(person, "Jane Doe");
console.log(person.firstName, person.lastName) // Jane Doe
```
#### Accounts & migrations
# Accounts & Migrations
## CoValues as a graph of data rooted in accounts
Compared to traditional relational databases with tables and foreign keys,
Jazz is more like a graph database, or GraphQL APIs —
where CoValues can arbitrarily refer to each other and you can resolve references without having to do a join.
(See [Subscribing & deep loading](/docs/using-covalues/subscription-and-loading)).
To find all data related to a user, the account acts as a root node from where you can resolve all the data they have access to.
These root references are modeled explicitly in your schema, distinguishing between data that is typically public
(like a user's profile) and data that is private (like their messages).
### `Account.root` - private data a user cares about
Every Jazz app that wants to refer to per-user data needs to define a custom root `CoMap` schema and declare it in a custom `Account` schema as the `root` field:
```ts twoslash
const Chat = co.map({});
// ---cut---
const MyAppRoot = co.map({
myChats: co.list(Chat),
});
export const MyAppAccount = co.account({
root: MyAppRoot,
profile: co.profile(),
});
```
### `Account.profile` - public data associated with a user
The built-in `Account` schema class comes with a default `profile` field, which is a CoMap (in a Group with `"everyone": "reader"` - so publicly readable permissions)
that is set up for you based on the username the `AuthMethod` provides on account creation.
Their pre-defined schemas roughly look like this:
```ts twoslash
// @noErrors: 2416
// ---cut---
// ...somewhere in jazz-tools itself...
const Account = co.account({
root: co.map({}),
profile: co.profile({
name: z.string(),
}),
});
```
If you want to keep the default `co.profile()` schema, but customise your account's private `root`, all you have to do is define a new `root` field in your account schema and use `co.profile()` without options:
```ts twoslash
const Chat = co.map({});
// ---cut---
const MyAppRoot = co.map({ // [!code ++:3]
myChats: co.list(Chat),
});
export const MyAppAccount = co.account({
root: MyAppRoot, // [!code ++]
profile: co.profile(),
});
```
If you want to extend the `profile` to contain additional fields (such as an avatar `co.image()`), you can declare your own profile schema class using `co.profile({...})`:
```ts twoslash
const Chat = co.map({});
// ---cut---
export const MyAppRoot = co.map({
myChats: co.list(Chat),
});
export const MyAppProfile = co.profile({ // [!code ++:4]
name: z.string(), // compatible with default Profile schema
avatar: z.optional(co.image()),
});
export const MyAppAccount = co.account({
root: MyAppRoot,
profile: MyAppProfile, // [!code ++]
});
```
## Resolving CoValues starting at `profile` or `root`
To use per-user data in your app, you typically use `useAccount` somewhere in a high-level component, pass it your custom Account schema and specify which references to resolve using a resolve query (see [Subscribing & deep loading](/docs/using-covalues/subscription-and-loading)).
```tsx twoslash
const Chat = co.map({});
const MyAppRoot = co.map({
myChats: co.list(Chat),
});
const MyAppProfile = co.profile();
const MyAppAccount = co.account({
root: MyAppRoot,
profile: MyAppProfile,
});
class ChatPreview extends React.Component<{ chat: co.loaded }> {};
class ContactPreview extends React.Component<{ contact: co.loaded }> {};
// ---cut---
function DashboardPageComponent() {
const { me } = useAccount(MyAppAccount, { resolve: {
profile: true,
root: {
myChats: { $each: true },
}
}});
return (
Dashboard
{me ? (
Logged in as {me.profile.name}
My chats
{me.root.myChats.map((chat) => (
))}
) : (
"Loading..."
)}
);
}
```
## Populating and evolving `root` and `profile` schemas with migrations
As you develop your app, you'll likely want to
- initialise data in a user's `root` and `profile`
- add more data to your `root` and `profile` schemas
You can achieve both by overriding the `migrate()` method on your `Account` schema class.
### When migrations run
Migrations are run after account creation and every time a user logs in.
Jazz waits for the migration to finish before passing the account to your app's context.
### Initialising user data after account creation
```ts twoslash
const Chat = co.map({});
const Bookmark = co.map({});
const MyAppRoot = co.map({
myChats: co.list(Chat),
});
const MyAppProfile = co.profile({
name: z.string(),
bookmarks: co.list(Bookmark),
});
// ---cut---
export const MyAppAccount = co.account({
root: MyAppRoot,
profile: MyAppProfile,
}).withMigration((account, creationProps?: { name: string }) => {
// we specifically need to check for undefined,
// because the root might simply be not loaded (`null`) yet
if (account.root === undefined) {
account.root = MyAppRoot.create({
// Using a group to set the owner is always a good idea.
// This way if in the future we want to share
// this coValue we can do so easily.
myChats: co.list(Chat).create([], Group.create()),
});
}
if (account.profile === undefined) {
const profileGroup = Group.create();
// Unlike the root, we want the profile to be publicly readable.
profileGroup.makePublic();
account.profile = MyAppProfile.create({
name: creationProps?.name ?? "New user",
bookmarks: co.list(Bookmark).create([], profileGroup),
}, profileGroup);
}
});
```
### Adding/changing fields to `root` and `profile`
To add new fields to your `root` or `profile` schemas, amend their corresponding schema classes with new fields,
and then implement a migration that will populate the new fields for existing users (by using initial data, or by using existing data from old fields).
To do deeply nested migrations, you might need to use the asynchronous `ensureLoaded()` method before determining whether the field already exists, or is simply not loaded yet.
Now let's say we want to add a `myBookmarks` field to the `root` schema:
```ts twoslash
const Chat = co.map({});
const Bookmark = co.map({});
const MyAppProfile = co.profile({
name: z.string(),
bookmarks: co.list(Bookmark),
});
// ---cut---
const MyAppRoot = co.map({
myChats: co.list(Chat),
myBookmarks: z.optional(co.list(Bookmark)), // [!code ++:1]
});
export const MyAppAccount = co.account({
root: MyAppRoot,
profile: MyAppProfile,
}).withMigration(async (account) => {
if (account.root === undefined) {
account.root = MyAppRoot.create({
myChats: co.list(Chat).create([], Group.create()),
});
}
// We need to load the root field to check for the myContacts field
const { root } = await account.ensureLoaded({
resolve: { root: true }
});
// we specifically need to check for undefined,
// because myBookmarks might simply be not loaded (`null`) yet
if (root.myBookmarks === undefined) { // [!code ++:3]
root.myBookmarks = co.list(Bookmark).create([], Group.create());
}
});
```
{/*
TODO: Add best practice: only ever add fields
Note: explain and reassure that there will be more guardrails in the future
https://github.com/garden-co/jazz/issues/1160
*/}
### Using CoValues
#### 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/schemas/covalues) for more details on primitive fields):
```ts twoslash
const Member = co.map({
name: z.string(),
});
// ---cut---
const Project = co.map({
name: z.string(),
startDate: z.date(),
status: z.literal(["planning", "active", "completed"]),
coordinator: z.optional(Member),
});
export type Project = co.loaded;
```
You can create either struct-like CoMaps with fixed fields (as above) or record-like CoMaps for key-value pairs:
```ts twoslash
// ---cut---
const Inventory = co.record(z.string(), z.number());
```
To instantiate a CoMap:
```ts twoslash
const me = await createJazzTestAccount();
const Member = co.map({
name: z.string(),
});
const Project = co.map({
name: z.string(),
startDate: z.date(),
status: z.literal(["planning", "active", "completed"]),
coordinator: z.optional(Member),
});
const Inventory = co.record(z.string(), z.number());
// ---cut---
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 twoslash
const me = await createJazzTestAccount();
const memberAccount = await createJazzTestAccount();
const Member = co.map({
name: z.string(),
});
const Project = co.map({
name: z.string(),
startDate: z.date(),
status: z.literal(["planning", "active", "completed"]),
coordinator: z.optional(Member),
});
// ---cut---
// 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/groups/intro) 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 twoslash
const me = await createJazzTestAccount();
const Member = co.map({
name: z.string(),
});
const Project = co.map({
name: z.string(),
startDate: z.date(),
status: z.literal(["planning", "active", "completed"]),
coordinator: z.optional(Member),
});
const project = Project.create(
{
name: "Spring Planting",
startDate: new Date("2025-03-20"),
status: "planning",
},
);
// ---cut---
console.log(project.name); // "Spring Planting"
console.log(project.status); // "planning"
```
### Handling Optional Fields
Optional fields require checks before access:
```ts twoslash
const me = await createJazzTestAccount();
const Member = co.map({
name: z.string(),
});
const Project = co.map({
name: z.string(),
startDate: z.date(),
status: z.literal(["planning", "active", "completed"]),
coordinator: z.optional(Member),
});
const project = Project.create(
{
name: "Spring Planting",
startDate: new Date("2025-03-20"),
status: "planning"
},
);
// ---cut---
if (project.coordinator) {
console.log(project.coordinator.name); // Safe access
}
```
### Recursive references
CoMaps can reference themselves recursively:
```ts twoslash
const Member = co.map({
name: z.string(),
});
// ---cut---
const Project = co.map({
name: z.string(),
startDate: z.date(),
status: z.literal(["planning", "active", "completed"]),
coordinator: z.optional(Member),
get subProject() {
return Project.optional();
}
});
export type Project = co.loaded;
```
When the recursive references involve more complex types, it is required to specify the getter return type:
```ts twoslash
const Member = co.map({
name: z.string(),
});
// ---cut---
const Project = co.map({
name: z.string(),
startDate: z.date(),
status: z.literal(["planning", "active", "completed"]),
coordinator: z.optional(Member),
get subProjects(): z.ZodOptional> {
return z.optional(co.list(Project));
}
});
export type Project = co.loaded;
```
### Working with Record CoMaps
For record-type CoMaps, you can access values using bracket notation:
```ts twoslash
const me = await createJazzTestAccount();
const Inventory = co.record(z.string(), z.number());
// ---cut---
const inventory = Inventory.create({
tomatoes: 48,
peppers: 24,
basil: 12
});
console.log(inventory["tomatoes"]); // 48
```
## Updating CoMaps
Updating CoMap properties uses standard JavaScript assignment:
```ts twoslash
const me = await createJazzTestAccount();
const Member = co.map({
name: z.string(),
});
const Project = co.map({
name: z.string(),
startDate: z.date(),
status: z.literal(["planning", "active", "completed"]),
coordinator: z.optional(Member),
});
const Inventory = co.record(z.string(), z.number());
const project = Project.create(
{
name: "Spring Planting",
startDate: new Date("2025-03-20"),
status: "planning"
},
);
// ---cut---
project.name = "Spring Vegetable Garden"; // Update name
project.startDate = new Date("2025-03-20"); // Update date
```
### Type Safety
CoMaps are fully typed in TypeScript, giving you autocomplete and error checking:
```ts twoslash
const me = await createJazzTestAccount();
const Member = co.map({
name: z.string(),
});
const Project = co.map({
name: z.string(),
startDate: z.date(),
status: z.literal(["planning", "active", "completed"]),
coordinator: z.optional(Member),
});
const Inventory = co.record(z.string(), z.number());
const project = Project.create(
{
name: "Spring Planting",
startDate: new Date("2025-03-20"),
status: "planning"
},
);
// ---cut---
project.name = "Spring Vegetable Planting"; // ✓ Valid string
// @errors: 2322
project.startDate = "2025-03-15"; // ✗ Type error: expected Date
```
### Soft Deletion
Implementing a soft deletion pattern by using a `deleted` flag allows you to maintain data for potential recovery and auditing.
```ts twoslash
// ---cut---
const Project = co.map({
name: z.string(),
deleted: z.optional(z.boolean()), // [!code ++]
});
```
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 twoslash
const me = await createJazzTestAccount();
const Member = co.map({
name: z.string(),
});
const Project = co.map({
name: z.string(),
startDate: z.date(),
status: z.literal(["planning", "active", "completed"]),
coordinator: z.optional(Member),
});
const Inventory = co.record(z.string(), z.number());
const project = Project.create(
{
name: "Spring Planting",
startDate: new Date("2025-03-20"),
status: "planning"
},
);
const inventory = Inventory.create({
tomatoes: 48,
peppers: 24,
basil: 12
});
// ---cut---
delete inventory["basil"]; // Remove a key-value pair
// For optional fields in struct-like CoMaps
project.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/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 twoslash
// ---cut---
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.priority = "medium";
// Upgrade the version so the migration won't run again
task.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 twoslash
// ---cut---
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) => {
// @ts-expect-error - check if we need to run the migration
if (task.version === 1) {
task.version = 2;
task.priority = "medium";
}
});
// Export the discriminated union; because some users might
// not be able to run the migration
export const Task = z.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 twoslash
const me = await createJazzTestAccount();
// ---cut---
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 twoslash
const Task = co.map({
text: z.string(),
});
// ---cut---
// 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 twoslash
const Task = co.map({
text: z.string(),
});
const Project = co.map({
name: z.string(),
startDate: z.date(),
endDate: z.optional(z.date()),
});
const project = Project.create({
name: "My project",
startDate: new Date("2025-04-01"),
endDate: new Date("2025-04-04"),
});
// ---cut---
// Given the project owner, myTask will have always the same id
const learnJazzTask = await Task.create({
text: "Let's learn some Jazz!",
}, {
unique: "learning-jazz",
owner: project._owner, // Different owner, different id
});
```
Now you can use `CoMap.loadUnique` to easily load the CoMap using the human-readable identifier:
```ts twoslash
const Task = co.map({
text: z.string(),
});
const Project = co.map({
name: z.string(),
startDate: z.date(),
endDate: z.optional(z.date()),
});
const project = Project.create({
name: "My project",
startDate: new Date("2025-04-01"),
endDate: new Date("2025-04-04"),
});
// ---cut---
const learnJazzTask = await Task.loadUnique(
"learning-jazz",
project._owner.id
);
```
It's also possible to combine the create+load operation using `CoMap.upsertUnique`:
```ts twoslash
const Task = co.map({
text: z.string(),
});
const Project = co.map({
name: z.string(),
startDate: z.date(),
endDate: z.optional(z.date()),
});
const project = Project.create({
name: "My project",
startDate: new Date("2025-04-01"),
endDate: new Date("2025-04-04"),
});
// ---cut---
const learnJazzTask = await Task.upsertUnique(
{
value: {
text: "Let's learn some Jazz!",
},
unique: "learning-jazz",
owner: project._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)
#### 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 twoslash
const Task = co.map({
title: z.string(),
status: z.literal(["todo", "in-progress", "complete"]),
});
// ---cut---
const ListOfResources = co.list(z.string());
export type ListOfResources = co.loaded;
const ListOfTasks = co.list(Task);
export type ListOfTasks = co.loaded;
```
To create a `CoList`:
```ts twoslash
const Task = co.map({
title: z.string(),
status: z.literal(["todo", "in-progress", "complete"]),
});
// ---cut---
// Create an empty list
const resources = co.list(z.string()).create([]);
// Create a list with initial items
const tasks = co.list(Task).create([
Task.create({ title: "Prepare soil beds", status: "in-progress" }),
Task.create({ title: "Order compost", status: "todo" })
]);
```
### Ownership
Like other CoValues, you can specify ownership when creating CoLists.
```ts twoslash
const me = await createJazzTestAccount();
const colleagueAccount = await createJazzTestAccount();
const Task = co.map({
title: z.string(),
status: z.string(),
});
// ---cut---
// 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/groups/intro) for more information on how to use groups to control access to CoLists.
## Reading from CoLists
CoLists support standard array access patterns:
```ts twoslash
const Task = co.map({
title: z.string(),
status: z.literal(["todo", "in-progress", "complete"]),
});
const ListOfTasks = co.list(Task);
const tasks = ListOfTasks.create([
Task.create({ title: "Prepare soil beds", status: "todo" }),
Task.create({ title: "Order compost", status: "todo" }),
]);
// ---cut---
// 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
Update `CoList`s just like you would JavaScript arrays:
```ts twoslash
const Task = co.map({
title: z.string(),
status: z.literal(["todo", "in-progress", "complete"]),
});
const ListOfTasks = co.list(Task);
const ListOfResources = co.list(z.string());
const resources = ListOfResources.create([]);
const tasks = ListOfTasks.create([]);
// ---cut---
// Add items
resources.push("Tomatoes"); // Add to end
resources.unshift("Lettuce"); // Add to beginning
tasks.push(Task.create({ // Add complex items
title: "Install irrigation",
status: "todo"
}));
// Replace items
resources[0] = "Cucumber"; // Replace by index
// Modify nested items
tasks[0].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 twoslash
// ---cut---
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.push(Task.create({
title: "Tomatoes",
status: "todo",
deleted: false
}));
myTaskList.push(Task.create({
title: "Cucumbers",
status: "todo",
deleted: true
}));
myTaskList.push(Task.create({
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
Remove specific items by index with `splice`, or remove the first or last item with `pop` or `shift`:
```ts twoslash
const ListOfResources = co.list(z.string());
const resources = ListOfResources.create([
"Tomatoes",
"Cucumber",
"Peppers",
]);
// ---cut---
// Remove 2 items starting at index 1
resources.splice(1, 2);
console.log(resources); // ["Cucumber", "Peppers"]
// Remove a single item at index 0
resources.splice(0, 1);
console.log(resources); // ["Peppers"]
// Remove items
const lastItem = resources.pop(); // Remove and return last item
resources.shift(); // Remove first item
```
### Array Methods
`CoList`s support the standard JavaScript array methods you already know:
```ts twoslash
const ListOfResources = co.list(z.string());
const resources = ListOfResources.create([]);
// ---cut---
// Add multiple items at once
resources.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"]
// Sort (modifies the CoList in-place)
resources.sort();
console.log(resources); // ["Basil", "Peppers", "Tomatoes"]
```
### Type Safety
CoLists maintain type safety for their items:
```ts twoslash
const Task = co.map({
title: z.string(),
status: z.literal(["todo", "in-progress", "complete"]),
});
const ListOfTasks = co.list(Task);
const ListOfResources = co.list(z.string());
const resources = ListOfResources.create([]);
const tasks = ListOfTasks.create([]);
// ---cut---
// TypeScript catches type errors
resources.push("Carrots"); // ✓ Valid string
// @errors: 2345
resources.push(42); // ✗ Type error: expected 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:
```tsx twoslash
const Task = co.map({
title: z.string(),
status: z.literal(["todo", "in-progress", "complete"]),
});
// ---cut---
const ListOfTasks = co.list(Task);
// React example
function TaskList({ tasks }: { tasks: co.loaded }) {
return (
{tasks.map(task => (
task ? (
{task.title} - {task.status}
): null
))}
);
}
```
#### Managing Relations
CoLists can be used to create one-to-many relationships:
```ts twoslash
const Task = co.map({
title: z.string(),
status: z.literal(["todo", "in-progress", "complete"]),
get project(): z.ZodOptional {
return z.optional(Project);
}
});
const ListOfTasks = co.list(Task);
const Project = co.map({
name: z.string(),
get tasks(): CoListSchema {
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.push(task);
// Access the project from the task
console.log(task.project); // { name: "Garden Project", tasks: [task] }
```
#### 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 twoslash
// ---cut---
// 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 twoslash
const me = await createJazzTestAccount();
const colleagueAccount = await createJazzTestAccount();
const Activity = co.map({
timestamp: z.date(),
action: z.literal(["watering", "planting", "harvesting", "maintenance"]),
notes: z.optional(z.string()),
});
const ActivityFeed = co.feed(Activity);
// ---cut---
const teamGroup = Group.create();
teamGroup.addMember(colleagueAccount, "writer");
const teamFeed = ActivityFeed.create([], { owner: teamGroup });
```
See [Groups as permission scopes](/docs/groups/intro) 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 twoslash
const me = await createJazzTestAccount();
const Activity = co.map({
timestamp: z.date(),
action: z.literal(["watering", "planting", "harvesting", "maintenance"]),
notes: z.optional(z.string()),
});
const ActivityFeed = co.feed(Activity);
const activityFeed = ActivityFeed.create([]);
const sessionId = `${me.id}_session_z1` as SessionID;
// ---cut---
// Get the feed for a specific session
const sessionFeed = activityFeed.perSession[sessionId];
// Latest entry from a session
console.log(sessionFeed?.value?.action); // "watering"
```
For convenience, you can also access the latest entry from the current session with `inCurrentSession`:
```ts twoslash
const me = await createJazzTestAccount();
const Activity = co.map({
timestamp: z.date(),
action: z.literal(["watering", "planting", "harvesting", "maintenance"]),
notes: z.optional(z.string()),
});
const ActivityFeed = co.feed(Activity);
const activityFeed = ActivityFeed.create([]);
const sessionId = `${me.id}_session_z1` as SessionID;
// ---cut---
// Get the feed for the current session
const currentSessionFeed = activityFeed.inCurrentSession;
// Latest entry from the current session
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 twoslash
const me = await createJazzTestAccount();
const Activity = co.map({
timestamp: z.date(),
action: z.literal(["watering", "planting", "harvesting", "maintenance"]),
notes: z.optional(z.string()),
});
const ActivityFeed = co.feed(Activity);
const activityFeed = ActivityFeed.create([]);
const accountId = me.id;
// ---cut---
// Get the feed for a specific account
const accountFeed = activityFeed.perAccount[accountId];
// Latest entry from the account
console.log(accountFeed.value?.action); // "watering"
```
For convenience, you can also access the latest entry from the current account with `byMe`:
```ts twoslash
const me = await createJazzTestAccount();
const Activity = co.map({
timestamp: z.date(),
action: z.literal(["watering", "planting", "harvesting", "maintenance"]),
notes: z.optional(z.string()),
});
const ActivityFeed = co.feed(Activity);
const activityFeed = ActivityFeed.create([]);
const accountId = me.id;
// ---cut---
// Get the feed for the current account
const myLatestEntry = activityFeed.byMe;
// Latest entry from the current account
console.log(myLatestEntry?.value?.action); // "harvesting"
```
### Feed Entries
#### All Entries
To retrieve all entries from a CoFeed:
```ts twoslash
const me = await createJazzTestAccount();
const Activity = co.map({
timestamp: z.date(),
action: z.literal(["watering", "planting", "harvesting", "maintenance"]),
notes: z.optional(z.string()),
});
const ActivityFeed = co.feed(Activity);
const activityFeed = ActivityFeed.create([]);
const accountId = me.id;
const sessionId = `${me.id}_session_z1` as SessionID;
// ---cut---
// 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) {
console.log(entry.value);
}
// Iterate over all entries from the session
for (const entry of sessionFeed.all) {
console.log(entry.value);
}
```
#### Latest Entry
To retrieve the latest entry from a CoFeed, ie. the last update:
```ts twoslash
const me = await createJazzTestAccount();
const Activity = co.map({
timestamp: z.date(),
action: z.literal(["watering", "planting", "harvesting", "maintenance"]),
notes: z.optional(z.string()),
});
const ActivityFeed = co.feed(Activity);
const activityFeed = ActivityFeed.create([]);
// ---cut---
// Get the latest entry from the current account
const latestEntry = activityFeed.byMe;
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?.name,
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 twoslash
const me = await createJazzTestAccount();
const Activity = co.map({
timestamp: z.date(),
action: z.literal(["watering", "planting", "harvesting", "maintenance"]),
notes: z.optional(z.string()),
});
const ActivityFeed = co.feed(Activity);
const activityFeed = ActivityFeed.create([]);
// ---cut---
// Log a new activity
activityFeed.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 twoslash
const me = await createJazzTestAccount();
const Activity = co.map({
timestamp: z.date(),
action: z.literal(["watering", "planting", "harvesting", "maintenance"]),
notes: z.optional(z.string()),
});
const ActivityFeed = co.feed(Activity);
const fromMobileFeed = ActivityFeed.create([]);
const fromBrowserFeed = ActivityFeed.create([]);
// ---cut---
// On mobile device:
fromMobileFeed.push(Activity.create({
timestamp: new Date(),
action: "harvesting",
notes: "Vegetable patch"
}));
// On web browser (same user):
fromBrowserFeed.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 twoslash
const me = await createJazzTestAccount();
const Activity = co.map({
timestamp: z.date(),
action: z.literal(["watering", "planting", "harvesting", "maintenance"]),
notes: z.optional(z.string()),
});
const ActivityFeed = co.feed(Activity);
const activityFeed = ActivityFeed.create([]);
const accountId = me.id;
// ---cut---
const accountFeed = activityFeed.perAccount[accountId];
// Get the account that made the last entry
console.log(accountFeed?.by);
```
### MadeAt
The `madeAt` property is a timestamp of when the entry was added to the feed.
```ts twoslash
const me = await createJazzTestAccount();
const Activity = co.map({
timestamp: z.date(),
action: z.literal(["watering", "planting", "harvesting", "maintenance"]),
notes: z.optional(z.string()),
});
const ActivityFeed = co.feed(Activity);
const activityFeed = ActivityFeed.create([]);
const accountId = me.id;
// ---cut---
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 twoslash
const me = await createJazzTestAccount();
// ---cut---
const note = co.plainText().create("Meeting notes", { owner: me });
// Update the text
note.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 twoslash
// ---cut---
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 twoslash
const me = await createJazzTestAccount();
// ---cut---
// Create plaintext with default ownership (current user)
const note = co.plainText().create("Meeting notes", { owner: me });
// Create rich text with HTML content
const document = co.richText().create("
Project overview
",
{ owner: me }
);
```
### Ownership
Like other CoValues, you can specify ownership when creating CoTexts.
```ts twoslash
const me = await createJazzTestAccount();
const colleagueAccount = await createJazzTestAccount();
// ---cut---
// 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/groups/intro) for more information on how to use groups to control access to CoText values.
## Reading Text
CoText values work similarly to JavaScript strings:
```ts twoslash
const me = await createJazzTestAccount();
const note = co.plainText().create("Meeting notes", { owner: me });
// ---cut---
// Get the text content
console.log(note.toString()); // "Meeting notes"
console.log(`${note}`); // "Meeting notes"
// Check the text length
console.log(note.length); // 14
```
When using CoTexts in JSX, you can read the text directly:
```tsx twoslash
const me = await createJazzTestAccount();
const note = co.plainText().create("Meeting notes", { owner: me });
// ---cut---
<>
{note.toString()}
{note}
>
```
## Making Edits
Insert and delete text with intuitive methods:
```ts twoslash
const me = await createJazzTestAccount();
const note = co.plainText().create("Meeting notes", { owner: me });
// ---cut---
// 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.applyDiff("Team meeting notes for Tuesday");
```
### Applying Diffs
Use `applyDiff` to efficiently update text with minimal changes:
```ts twoslash
const me = await createJazzTestAccount();
// ---cut---
// Original text: "Team status update"
const minutes = co.plainText().create("Team status update", { owner: me });
// Replace the entire text with a new version
minutes.applyDiff("Weekly team status update for Project X");
// Make partial changes
let text = minutes.toString();
text = text.replace("Weekly", "Monday");
minutes.applyDiff(text); // Efficiently updates only what changed
```
Perfect for handling user input in form controls:
```tsx twoslash
const me = await createJazzTestAccount();
// ---cut---
function TextEditor({ textId }: { textId: string }) {
const note = useCoState(co.plainText(), textId);
return (
note &&
```ts twoslash
const me = await createJazzTestAccount();
// ---cut---
const note = co.plainText().create("", { owner: me });
// Create and set up the textarea
const textarea = document.createElement('textarea');
textarea.value = note.toString();
// Add event listener for changes
textarea.addEventListener('input', (e: Event) => {
const target = e.target as HTMLTextAreaElement;
// Efficiently update only what the user changed
note.applyDiff(target.value);
});
// Add the textarea to the document
document.body.appendChild(textarea);
```
```vue twoslash
```
```svelte twoslash
## Using Rich Text with ProseMirror
Jazz provides a dedicated plugin for integrating `co.richText()` with the popular ProseMirror editor that enables bidirectional synchronization between your co.richText() instances and ProseMirror editors.
### ProseMirror Plugin Features
- **Bidirectional Sync**: Changes in the editor automatically update the `co.richText()` and vice versa
- **Real-time Collaboration**: Multiple users can edit the same document simultaneously
- **HTML Conversion**: Automatically converts between HTML (used by `co.richText()`) and ProseMirror's document model
### Installation
```bash
pnpm add prosemirror-view \
prosemirror-state \
prosemirror-schema-basic
```
### Integration
We don't currently have a React Native-specific example, but you need help you can [request one](https://github.com/garden-co/jazz/issues/new), or ask on [Discord](https://discord.gg/utDMjHYg42).
We don't currently have a React Native Expo-specific example, but you need help please [request one](https://github.com/garden-co/jazz/issues/new), or ask on [Discord](https://discord.gg/utDMjHYg42).
For use with React:
```tsx twoslash
const JazzProfile = co.profile({
bio: co.richText(),
});
const JazzAccount = co.account({
profile: JazzProfile,
root: co.map({})
});
// ---cut---
// RichTextEditor.tsx
function RichTextEditor() {
const { me } = useAccount(JazzAccount, { resolve: { profile: true } });
const editorRef = useRef(null);
const viewRef = useRef(null);
useEffect(() => {
if (!me?.profile.bio || !editorRef.current) return;
// Create the Jazz plugin for ProseMirror
// Providing a co.richText() instance to the plugin to automatically sync changes
const jazzPlugin = createJazzPlugin(me.profile.bio); // [!code ++]
// Set up ProseMirror with the Jazz plugin
if (!viewRef.current) {
viewRef.current = new EditorView(editorRef.current, {
state: EditorState.create({
schema,
plugins: [
...exampleSetup({ schema }),
jazzPlugin, // [!code ++]
],
}),
});
}
return () => {
if (viewRef.current) {
viewRef.current.destroy();
viewRef.current = null;
}
};
}, [me?.profile.bio?.id]);
if (!me) return null;
return (
);
}
```
We don't currently have a Svelte-specific example, but you need help you can [request one](https://github.com/garden-co/jazz/issues/new), or ask on [Discord](https://discord.gg/utDMjHYg42).
We don't currently have a Vue-specific example, but you need help you can [request one](https://github.com/garden-co/jazz/issues/new), or ask on [Discord](https://discord.gg/utDMjHYg42).
For use without a framework:
```js twoslash
function setupRichTextEditor(coRichText, container) {
// Create the Jazz plugin for ProseMirror
// Providing a co.richText() instance to the plugin to automatically sync changes
const jazzPlugin = createJazzPlugin(coRichText); // [!code ++]
// Set up ProseMirror with Jazz plugin
const view = new EditorView(container, {
state: EditorState.create({
schema,
plugins: [
...exampleSetup({ schema }),
jazzPlugin, // [!code ++]
],
}),
});
// Return cleanup function
return () => {
view.destroy();
};
}
// Usage
const document = co.richText().create("
Initial content
", { owner: me });
const editorContainer = document.getElementById("editor");
const cleanup = setupRichTextEditor(document, editorContainer);
// Later when done with the editor
cleanup();
```
#### FileStreams
# FileStreams
FileStreams handle binary data in Jazz applications - think documents, audio files, and other non-text content. They're essentially collaborative versions of `Blob`s that sync automatically across devices.
Use FileStreams when you need to:
- Distribute documents across devices
- Store audio or video files
- Sync any binary data between users
**Note:** For images specifically, Jazz provides the higher-level `ImageDefinition` abstraction which manages multiple image resolutions - see the [ImageDefinition documentation](/docs/using-covalues/imagedef) for details.
FileStreams provide automatic chunking when using the `createFromBlob` method, track upload progress, and handle MIME types and metadata.
In your schema, reference FileStreams like any other CoValue:
```ts twoslash
// schema.ts
const Document = co.map({
title: z.string(),
file: co.fileStream(), // Store a document file
});
```
## Creating FileStreams
There are two main ways to create FileStreams: creating empty ones for manual data population or creating directly from existing files or blobs.
### Creating from Blobs and Files
For files from input elements or drag-and-drop interfaces, use `createFromBlob`:
```ts twoslash
const myGroup = Group.create();
const progressBar: HTMLElement = document.querySelector('.progress-bar')!;
// ---cut---
// From a file input
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
fileInput.addEventListener('change', async () => {
const file = fileInput.files?.[0];
if (!file) return;
// Create FileStream from user-selected file
const fileStream = await co.fileStream().createFromBlob(file, { owner: myGroup });
// Or with progress tracking for better UX
const fileWithProgress = await co.fileStream().createFromBlob(file, {
onProgress: (progress) => {
// progress is a value between 0 and 1
const percent = Math.round(progress * 100);
console.log(`Upload progress: ${percent}%`);
progressBar.style.width = `${percent}%`;
},
owner: myGroup
});
});
```
### Creating Empty FileStreams
Create an empty FileStream when you want to manually [add binary data in chunks](/docs/using-covalues/filestreams#writing-to-filestreams):
```ts twoslash
const myGroup = Group.create();
// ---cut---
// Create a new empty FileStream
const fileStream = FileStream.create({ owner: myGroup } );
```
### Ownership
Like other CoValues, you can specify ownership when creating FileStreams.
```ts twoslash
const me = await createJazzTestAccount();
const colleagueAccount = await createJazzTestAccount();
// ---cut---
// Create a team group
const teamGroup = Group.create();
teamGroup.addMember(colleagueAccount, "writer");
// Create a FileStream with shared ownership
const teamFileStream = FileStream.create({ owner: teamGroup });
```
See [Groups as permission scopes](/docs/groups/intro) for more information on how to use groups to control access to FileStreams.
## Reading from FileStreams
`FileStream`s provide several ways to access their binary content, from raw chunks to convenient Blob objects.
### Getting Raw Data Chunks
To access the raw binary data and metadata:
```ts twoslash
const fileStream = FileStream.create();
// ---cut---
// Get all chunks and metadata
const fileData = fileStream.getChunks();
if (fileData) {
console.log(`MIME type: ${fileData.mimeType}`);
console.log(`Total size: ${fileData.totalSizeBytes} bytes`);
console.log(`File name: ${fileData.fileName}`);
console.log(`Is complete: ${fileData.finished}`);
// Access raw binary chunks
for (const chunk of fileData.chunks) {
// Each chunk is a Uint8Array
console.log(`Chunk size: ${chunk.length} bytes`);
}
}
```
By default, `getChunks()` only returns data for completely synced `FileStream`s. To start using chunks from a `FileStream` that's currently still being synced use the `allowUnfinished` option:
```ts twoslash
const fileStream = FileStream.create();
// ---cut---
// Get data even if the stream isn't complete
const partialData = fileStream.getChunks({ allowUnfinished: true });
```
### Converting to Blobs
For easier integration with web APIs, convert to a `Blob`:
```ts twoslash
const fileStream = FileStream.create();
// ---cut---
// Convert to a Blob
const blob = fileStream.toBlob();
// Get the filename from the metadata
const filename = fileStream.getChunks()?.fileName;
if (blob) {
// Use with URL.createObjectURL
const url = URL.createObjectURL(blob);
// Create a download link
const link = document.createElement('a');
link.href = url;
link.download = filename || 'document.pdf';
link.click();
// Clean up when done
URL.revokeObjectURL(url);
}
```
### Loading FileStreams as Blobs
You can directly load a `FileStream` as a `Blob` when you only have its ID:
```ts twoslash
const fileStreamId = "co_z123" as ID;
// ---cut---
// Load directly as a Blob when you have an ID
const blob = await FileStream.loadAsBlob(fileStreamId);
// By default, waits for complete uploads
// For in-progress uploads:
const partialBlob = await FileStream.loadAsBlob(fileStreamId, {
allowUnfinished: true,
});
```
### Checking Completion Status
Check if a `FileStream` is fully synced:
```ts twoslash
const fileStream = FileStream.create();
// ---cut---
if (fileStream.isBinaryStreamEnded()) {
console.log('File is completely synced');
} else {
console.log('File upload is still in progress');
}
```
## Writing to FileStreams
When creating a `FileStream` manually (not using `createFromBlob`), you need to manage the upload process yourself. This gives you more control over chunking and progress tracking.
### The Upload Lifecycle
`FileStream` uploads follow a three-stage process:
1. **Start** - Initialize with metadata
2. **Push** - Send one or more chunks of data
3. **End** - Mark the stream as complete
### Starting a `FileStream`
Begin by providing metadata about the file:
```ts twoslash
const myGroup = Group.create();
// ---cut---
// Create an empty FileStream
const fileStream = FileStream.create({ owner: myGroup });
// Initialize with metadata
fileStream.start({
mimeType: 'application/pdf', // MIME type (required)
totalSizeBytes: 1024 * 1024 * 2, // Size in bytes (if known)
fileName: 'document.pdf' // Original filename (optional)
});
```
### Pushing Data
Add binary data in chunks - this helps with large files and progress tracking:
```ts twoslash
const fileStream = FileStream.create();
const file = [0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x57, 0x6F, 0x72, 0x6C, 0x64]; // "Hello World" in ASCII
const bytes = new Uint8Array(file);
const arrayBuffer = bytes.buffer;
// ---cut---
const data = new Uint8Array(arrayBuffer);
// For large files, break into chunks (e.g., 100KB each)
const chunkSize = 1024 * 100;
for (let i = 0; i < data.length; i += chunkSize) {
// Create a slice of the data
const chunk = data.slice(i, i + chunkSize);
// Push chunk to the FileStream
fileStream.push(chunk);
// Track progress
const progress = Math.min(100, Math.round((i + chunk.length) * 100 / data.length));
console.log(`Upload progress: ${progress}%`);
}
```
### Completing the Upload
Once all chunks are pushed, mark the `FileStream` as complete:
```ts twoslash
const fileStream = FileStream.create();
// ---cut---
// Finalize the upload
fileStream.end();
console.log('Upload complete!');
```
## Subscribing to `FileStream`s
Like other CoValues, you can subscribe to `FileStream`s to get notified of changes as they happen. This is especially useful for tracking upload progress when someone else is uploading a file.
### Loading by ID
Load a `FileStream` when you have its ID:
```ts twoslash
const fileStreamId = "co_z123";
// ---cut---
// Load a FileStream by ID
const fileStream = await FileStream.load(fileStreamId);
if (fileStream) {
console.log('FileStream loaded successfully');
// Check if it's complete
if (fileStream.isBinaryStreamEnded()) {
// Process the completed file
const blob = fileStream.toBlob();
}
}
```
### Subscribing to Changes
Subscribe to a `FileStream` to be notified when chunks are added or when the upload is complete:
```ts twoslash
const fileStreamId = "co_z123";
// ---cut---
// Subscribe to a FileStream by ID
const unsubscribe = FileStream.subscribe(fileStreamId, (fileStream: FileStream) => {
// Called whenever the FileStream changes
console.log('FileStream updated');
// Get current status
const chunks = fileStream.getChunks({ allowUnfinished: true });
if (chunks) {
const uploadedBytes = chunks.chunks.reduce((sum: number, chunk: Uint8Array) => sum + chunk.length, 0);
const totalBytes = chunks.totalSizeBytes || 1;
const progress = Math.min(100, Math.round(uploadedBytes * 100 / totalBytes));
console.log(`Upload progress: ${progress}%`);
if (fileStream.isBinaryStreamEnded()) {
console.log('Upload complete!');
// Now safe to use the file
const blob = fileStream.toBlob();
// Clean up the subscription if we're done
unsubscribe();
}
}
});
```
### Waiting for Upload Completion
If you need to wait for a `FileStream` to be fully synchronized across devices:
```ts twoslash
const fileStream = FileStream.create();
// ---cut---
// Wait for the FileStream to be fully synced
await fileStream.waitForSync({
timeout: 5000 // Optional timeout in ms
});
console.log('FileStream is now synced to all connected devices');
```
This is useful when you need to ensure that a file is available to other users before proceeding with an operation.
#### ImageDefinition
### react-native-expo Implementation
# ImageDefinition
`ImageDefinition` is a specialized CoValue designed specifically for managing images in Jazz. It extends beyond basic file storage by supporting multiple resolutions of the same image, optimized for mobile devices.
**Note**: This guide applies to both Expo and framework-less React Native implementations. The functionality described here is identical regardless of which implementation you're using
Jazz offers several tools to work with images in React Native:
- [`createImageNative()`](#creating-images) - function to create an `ImageDefinition` from a base64 image data URI
- [`ProgressiveImgNative`](#displaying-images-with-progressiveimgnative) - React component to display an image with progressive loading
- [`useProgressiveImgNative`](#using-useprogressiveimgnative-hook) - React hook to load an image in your own component
For examples of use, see our example apps:
- [React Native Chat](https://github.com/gardencmp/jazz/tree/main/examples/chat-rn) (Framework-less implementation)
- [Expo Chat](https://github.com/gardencmp/jazz/tree/main/examples/chat-rn-expo) (Expo implementation)
- [Expo Clerk](https://github.com/gardencmp/jazz/tree/main/examples/clerk-expo) (Expo with Clerk-based authentication)
## Creating Images
The easiest way to create and use images in your Jazz application is with the `createImageNative()` function:
```tsx
async function handleImagePicker() {
try {
// Launch the image picker
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
base64: true,
quality: 1,
});
if (!result.canceled) {
const base64Uri = `data:image/jpeg;base64,${result.assets[0].base64}`;
// Creates ImageDefinition with multiple resolutions automatically
const image = await createImageNative(base64Uri, {
owner: me.profile._owner,
maxSize: 2048, // Optional: limit maximum resolution
});
// Store the image
me.profile.image = image;
}
} catch (error) {
console.error("Error creating image:", error);
}
}
```
The `createImageNative()` function:
- Creates an `ImageDefinition` with the right properties
- Generates a small placeholder for immediate display
- Creates multiple resolution variants of your image
- Returns the created `ImageDefinition`
### Configuration Options
You can configure `createImageNative()` with additional options:
```tsx
// Configuration options
const options = {
owner: me, // Owner for access control
maxSize: 1024 // Maximum resolution to generate
};
// Setting maxSize controls which resolutions are generated:
// 256: Only creates the smallest resolution (256px on longest side)
// 1024: Creates 256px and 1024px resolutions
// 2048: Creates 256px, 1024px, and 2048px resolutions
// undefined: Creates all resolutions including the original size
const image = await createImageNative(base64Uri, options);
```
## Displaying Images with `ProgressiveImgNative`
For a complete progressive loading experience, use the `ProgressiveImgNative` component:
```tsx
function GalleryView({ image }) {
return (
{({ src }) => (
)}
);
}
const styles = StyleSheet.create({
galleryImage: {
width: '100%',
height: 200,
borderRadius: 8,
}
});
```
The `ProgressiveImgNative` component handles:
- Showing a placeholder while loading
- Automatically selecting the appropriate resolution
- Progressive enhancement as higher resolutions become available
- Cleaning up resources when unmounted
## Using `useProgressiveImgNative` Hook
For more control over image loading, you can implement your own progressive image component:
```tsx
function CustomImageComponent({ image }) {
const {
src, // Data URI containing the image data as a base64 string,
// or a placeholder image URI
res, // The current resolution
originalSize // The original size of the image
} = useProgressiveImgNative({
image: image, // The image definition to load
targetWidth: 800 // Limit to resolutions up to 800px wide
});
// When image is not available yet
if (!src) {
return (
Loading image...
);
}
// When using placeholder
if (res === "placeholder") {
return (
);
}
// Full image display with custom overlay
return (
Resolution: {res}
);
}
```
## Understanding ImageDefinition
Behind the scenes, `ImageDefinition` is a specialized CoValue that stores:
- The original image dimensions (`originalSize`)
- An optional placeholder (`placeholderDataURL`) for immediate display
- Multiple resolution variants of the same image as [`FileStream`s](../using-covalues/filestreams)
Each resolution is stored with a key in the format `"widthxheight"` (e.g., `"1920x1080"`, `"800x450"`).
```tsx
// Structure of an ImageDefinition
const image = ImageDefinition.create({
originalSize: [1920, 1080],
placeholderDataURL: "data:image/jpeg;base64,/9j/4AAQSkZJRg...",
});
// Accessing the highest available resolution
const highestRes = image.highestResAvailable();
if (highestRes) {
console.log(`Found resolution: ${highestRes.res}`);
console.log(`Stream: ${highestRes.stream}`);
}
```
For more details on using `ImageDefinition` directly, see the [VanillaJS docs](/docs/vanilla/using-covalues/imagedef).
### Fallback Behavior
`highestResAvailable` returns the largest resolution that fits your constraints. If a resolution has incomplete data, it falls back to the next available lower resolution.
```tsx
const image = ImageDefinition.create({
originalSize: [1920, 1080],
});
image["1920x1080"] = FileStream.create(); // Empty image upload
image["800x450"] = await FileStream.createFromBlob(mediumSizeBlob);
const highestRes = image.highestResAvailable();
console.log(highestRes.res); // 800x450
```
---
### react-native Implementation
# ImageDefinition
`ImageDefinition` is a specialized CoValue designed specifically for managing images in Jazz. It extends beyond basic file storage by supporting multiple resolutions of the same image, optimized for mobile devices.
**Note**: This guide applies to both Expo and framework-less React Native implementations.
Jazz offers several tools to work with images in React Native:
- [`createImageNative()`](#creating-images) - function to create an `ImageDefinition` from a base64 image data URI
- [`ProgressiveImgNative`](#displaying-images-with-progressiveimgnative) - React component to display an image with progressive loading
- [`useProgressiveImgNative`](#using-useprogressiveimgnative-hook) - React hook to load an image in your own component
For examples of use, see our example apps:
- [React Native Chat](https://github.com/gardencmp/jazz/tree/main/examples/chat-rn) (Framework-less implementation)
- [Expo Chat](https://github.com/gardencmp/jazz/tree/main/examples/chat-rn-expo) (Expo implementation)
- [Expo Clerk](https://github.com/gardencmp/jazz/tree/main/examples/clerk-expo) (Expo with Clerk-based authentication)
## Creating Images
The easiest way to create and use images in your Jazz application is with the `createImageNative()` function:
```tsx
async function handleImagePicker() {
try {
// Launch the image picker
const result = await launchImageLibrary({
mediaType: 'photo',
includeBase64: true,
quality: 1,
});
if (!result.canceled) {
const base64Uri = `data:image/jpeg;base64,${result.assets[0].base64}`;
// Creates ImageDefinition with multiple resolutions automatically
const image = await createImageNative(base64Uri, {
owner: me.profile._owner,
maxSize: 2048, // Optional: limit maximum resolution
});
// Store the image
me.profile.image = image;
}
} catch (error) {
console.error("Error creating image:", error);
}
}
```
The `createImageNative()` function:
- Creates an `ImageDefinition` with the right properties
- Generates a small placeholder for immediate display
- Creates multiple resolution variants of your image
- Returns the created `ImageDefinition`
### Configuration Options
You can configure `createImageNative()` with additional options:
```tsx
// Configuration options
const options = {
owner: me, // Owner for access control
maxSize: 1024 // Maximum resolution to generate
};
// Setting maxSize controls which resolutions are generated:
// 256: Only creates the smallest resolution (256px on longest side)
// 1024: Creates 256px and 1024px resolutions
// 2048: Creates 256px, 1024px, and 2048px resolutions
// undefined: Creates all resolutions including the original size
const image = await createImageNative(base64Uri, options);
```
## Displaying Images with `ProgressiveImgNative`
For a complete progressive loading experience, use the `ProgressiveImgNative` component:
```tsx
function GalleryView({ image }) {
return (
{({ src }) => (
)}
);
}
const styles = StyleSheet.create({
galleryImage: {
width: '100%',
height: 200,
borderRadius: 8,
}
});
```
The `ProgressiveImgNative` component handles:
- Showing a placeholder while loading
- Automatically selecting the appropriate resolution
- Progressive enhancement as higher resolutions become available
- Cleaning up resources when unmounted
## Using `useProgressiveImgNative` Hook
For more control over image loading, you can implement your own progressive image component:
```tsx
function CustomImageComponent({ image }) {
const {
src, // Data URI containing the image data as a base64 string,
// or a placeholder image URI
res, // The current resolution
originalSize // The original size of the image
} = useProgressiveImgNative({
image: image, // The image definition to load
targetWidth: 800 // Limit to resolutions up to 800px wide
});
// When image is not available yet
if (!src) {
return (
Loading image...
);
}
// When using placeholder
if (res === "placeholder") {
return (
);
}
// Full image display with custom overlay
return (
Resolution: {res}
);
}
```
## Understanding ImageDefinition
Behind the scenes, `ImageDefinition` is a specialized CoValue that stores:
- The original image dimensions (`originalSize`)
- An optional placeholder (`placeholderDataURL`) for immediate display
- Multiple resolution variants of the same image as [`FileStream`s](../using-covalues/filestreams)
Each resolution is stored with a key in the format `"widthxheight"` (e.g., `"1920x1080"`, `"800x450"`).
```tsx
// Structure of an ImageDefinition
const image = ImageDefinition.create({
originalSize: [1920, 1080],
placeholderDataURL: "data:image/jpeg;base64,/9j/4AAQSkZJRg...",
});
// Accessing the highest available resolution
const highestRes = image.highestResAvailable();
if (highestRes) {
console.log(`Found resolution: ${highestRes.res}`);
console.log(`Stream: ${highestRes.stream}`);
}
```
For more details on using `ImageDefinition` directly, see the [VanillaJS docs](/docs/vanilla/using-covalues/imagedef).
### Fallback Behavior
`highestResAvailable` returns the largest resolution that fits your constraints. If a resolution has incomplete data, it falls back to the next available lower resolution.
```tsx
const image = ImageDefinition.create({
originalSize: [1920, 1080],
});
image["1920x1080"] = FileStream.create(); // Empty image upload
image["800x450"] = await FileStream.createFromBlob(mediumSizeBlob);
const highestRes = image.highestResAvailable();
console.log(highestRes.res); // 800x450
```
---
### react Implementation
# ImageDefinition
`ImageDefinition` is a specialized CoValue designed specifically for managing images in Jazz applications. It extends beyond basic file storage by supporting multiple resolutions of the same image and progressive loading patterns.
Beyond [`ImageDefinition`](#understanding-imagedefinition), Jazz offers higher-level functions and components that make it easier to use images:
- [`createImage()`](#creating-images) - function to create an `ImageDefinition` from a file
- [`ProgressiveImg`](#displaying-images-with-progressiveimg) - React component to display an image with progressive loading
- [`useProgressiveImg`](#using-useprogressiveimg-hook) - React hook to load an image in your own component
The [Image Upload example](https://github.com/gardencmp/jazz/tree/main/examples/image-upload) demonstrates use of `ProgressiveImg` and `ImageDefinition`.
## Creating Images
The easiest way to create and use images in your Jazz application is with the `createImage()` function:
```ts twoslash
const MyProfile = co.profile({
name: z.string(),
image: z.optional(co.image()),
});
const MyAccount = co.account({
root: co.map({}),
profile: MyProfile,
});
MyAccount.withMigration((account, creationProps) => {
if (account.profile === undefined) {
const profileGroup = Group.create();
profileGroup.makePublic();
account.profile = MyProfile.create(
{
name: creationProps?.name ?? "New user",
},
profileGroup,
);
}
});
const me = await MyAccount.create({});
const myGroup = Group.create();
// ---cut---
// Create an image from a file input
async function handleFileUpload(event: React.ChangeEvent) {
const file = event.target.files?.[0];
if (file) {
// Creates ImageDefinition with multiple resolutions automatically
const image = await createImage(file, { owner: myGroup });
// Store the image in your application data
me.profile.image = image;
}
}
```
**Note:** `createImage()` requires a browser environment as it uses browser APIs to process images.
The `createImage()` function:
- Creates an `ImageDefinition` with the right properties
- Generates a small placeholder for immediate display
- Creates multiple resolution variants of your image
- Returns the created `ImageDefinition`
### Configuration Options
You can configure `createImage()` with additional options:
```ts twoslash
const me = await createJazzTestAccount();
const file = new File([], "test.jpg", { type: "image/jpeg" });
// ---cut---
// Configuration options
const options = {
owner: me, // Owner for access control
maxSize: 1024 as 1024 // Maximum resolution to generate
};
// Setting maxSize controls which resolutions are generated:
// 256: Only creates the smallest resolution (256px on longest side)
// 1024: Creates 256px and 1024px resolutions
// 2048: Creates 256px, 1024px, and 2048px resolutions
// undefined: Creates all resolutions including the original size
const image = await createImage(file, options);
```
### Ownership
Like other CoValues, you can specify ownership when creating image definitions.
```ts twoslash
const me = await createJazzTestAccount();
const colleagueAccount = await createJazzTestAccount();
const file = new File([], "test.jpg", { type: "image/jpeg" });
// ---cut---
const teamGroup = Group.create();
teamGroup.addMember(colleagueAccount, "writer");
// Create an image with shared ownership
const teamImage = await createImage(file, { owner: teamGroup });
```
See [Groups as permission scopes](/docs/groups/intro) for more information on how to use groups to control access to images.
## Displaying Images with `ProgressiveImg`
For a complete progressive loading experience, use the `ProgressiveImg` component:
```tsx twoslash
// ---cut---
const Image = co.image();
function GalleryView({ image }: { image: co.loaded }) {
return (
{({ src }) => (
)}
);
}
```
The `ProgressiveImg` component handles:
- Showing a placeholder while loading
- Automatically selecting the appropriate resolution
- Progressive enhancement as higher resolutions become available
- Cleaning up resources when unmounted
## Using `useProgressiveImg` Hook
For more control over image loading, you can implement your own progressive image component:
```tsx twoslash
const Image = co.image();
// ---cut---
function CustomImageComponent({ image }: { image: co.loaded }) {
const {
src, // Data URI containing the image data as a base64 string,
// or a placeholder image URI
res, // The current resolution
originalSize // The original size of the image
} = useProgressiveImg({
image: image, // The image definition to load
targetWidth: 800 // Limit to resolutions up to 800px wide
});
// When image is not available yet
if (!src) {
return
Loading image...
;
}
// When image is loading, show a placeholder
if (res === "placeholder") {
return ;
}
// Full image display with custom overlay
return (
Resolution: {res}
);
}
```
## Understanding ImageDefinition
Behind the scenes, `ImageDefinition` is a specialized CoValue that stores:
- The original image dimensions (`originalSize`)
- An optional placeholder (`placeholderDataURL`) for immediate display
- Multiple resolution variants of the same image as [`FileStream`s](../using-covalues/filestreams)
Each resolution is stored with a key in the format `"widthxheight"` (e.g., `"1920x1080"`, `"800x450"`).
```ts twoslash
// ---cut---
// Structure of an ImageDefinition
const image = ImageDefinition.create({
originalSize: [1920, 1080],
placeholderDataURL: "data:image/jpeg;base64,/9j/4AAQSkZJRg...",
});
// Accessing the highest available resolution
const highestRes = ImageDefinition.highestResAvailable(image);
if (highestRes) {
console.log(`Found resolution: ${highestRes.res}`);
console.log(`Stream: ${highestRes.stream}`);
}
```
For more details on using `ImageDefinition` directly, see the [VanillaJS docs](/docs/vanilla/using-covalues/imagedef).
### Fallback Behavior
`highestResAvailable` returns the largest resolution that fits your constraints. If a resolution has incomplete data, it falls back to the next available lower resolution.
```ts twoslash
const mediumSizeBlob = new Blob([], { type: "image/jpeg" });
// ---cut---
const image = ImageDefinition.create({
originalSize: [1920, 1080],
});
image["1920x1080"] = FileStream.create(); // Empty image upload
image["800x450"] = await FileStream.createFromBlob(mediumSizeBlob);
const highestRes = ImageDefinition.highestResAvailable(image);
console.log(highestRes?.res); // 800x450
```
#### Connecting CoValues
# Connecting CoValues with direct linking
CoValues can form relationships with each other by **linking directly to other CoValues**. This creates a powerful connection where one CoValue can point to the unique identity of another.
Instead of embedding all of the details of one coValue directly within another, you use its Jazz-Tools schema as the field type. This allows multiple CoValues to point to the same piece of data effortlessly.
```ts twoslash
export const Location = co.map({
city: z.string(),
country: z.string(),
});
export type Location = co.loaded;
// co.ref can be used within CoMap fields to point to other CoValues
const Actor = co.map({
name: z.string,
imageURL: z.string,
birthplace: Location // Links directly to the Location CoMap above.
})
export type Actor = co.loaded;
// actual actor data is stored in the separate Actor CoValue
const Movie = co.map({
title: z.string,
director: z.string,
cast: co.list(Actor), // ordered, mutable
})
export type Movie = co.loaded;
// A User CoMap can maintain a CoFeed of co.ref(Movie) to track their favorite movies
const User = co.map({
username: z.string,
favoriteMovies: co.feed(Movie), // append-only
})
export type User = co.loaded;
```
### Understanding CoList and CoFeed
- CoList is a collaborative list where each item is a reference to a CoValue
- CoFeed contains an append-only list of references to CoValues.
This direct linking approach offers a single source of truth. When you update a referenced CoValue, all other CoValues that point to it are automatically updated, ensuring data consistency across your application.
By connecting CoValues through these direct references, you can build robust and collaborative applications where data is consistent, efficient to manage, and relationships are clearly defined. The ability to link different CoValue types to the same underlying data is fundamental to building complex applications with Jazz.
#### Subscriptions & Deep Loading
# Subscriptions & Deep Loading
Jazz's Collaborative Values (such as [CoMaps](/docs/using-covalues/comaps) or [CoLists](/docs/using-covalues/colists)) work like reactive state. By subscribing to them, you can react to both local and remote updates. This is the main way to consume data in your application.
Subscriptions also take care of loading CoValues that are not yet loaded locally and can do so *deeply* — by resolving nested CoValues. To make use of this, we'll show you how to specify the depth of data you need with resolve queries.
With each update you can also handle loading states and inaccessible CoValues.
## Manual subscriptions
You can subscribe to a CoValue from anywhere in your code (if you have its ID) by using `CoValue.subscribe()`.
If you're using React in your project, check out our [React hooks](/docs/react/using-covalues/subscription-and-loading#subscription-hooks) which provide a more streamlined experience with automatic subscription management.
**Note:** Unless you're using vanilla JavaScript, this is only used outside of React components - for example in server-side code or in tests. See the section below for convenient subscription *hooks* that you typically use in React.
```ts twoslash
const taskId = "co_123";
// ---cut-before---
const Task = co.map({
title: z.string(),
description: z.string(),
status: z.literal(["todo", "in-progress", "completed"]),
assignedTo: z.optional(z.string()),
});
// ...
// Subscribe to a Task by ID
const unsubscribe = Task.subscribe(taskId, {}, (updatedTask) => {
console.log("Task updated:", updatedTask.title);
console.log("New status:", updatedTask.status);
});
// Clean up when you're done
unsubscribe();
```
If you already have a CoValue instance, you can subscribe to it by calling its `subscribe` method.
```ts twoslash
const Task = co.map({
title: z.string(),
description: z.string(),
status: z.literal(["todo", "in-progress", "completed"]),
assignedTo: z.optional(z.string()),
});
const otherProps = {} as any;
// ---cut-before---
const task = Task.create({
title: "Cut the grass",
...otherProps
});
const unsubscribe = task.subscribe((updatedTask) => {
console.log("Task updated:", updatedTask.title);
});
// Clean up when you're done
unsubscribe();
```
## Subscription hooks
### `useCoState`
Jazz provides a `useCoState` hook that provides a convenient way to subscribe to CoValues and handle loading states:
```tsx twoslash
const Task = co.map({
title: z.string(),
status: z.literal(["todo", "in-progress", "completed"]),
});
const Project = co.map({
name: z.string(),
tasks: co.list(Task),
});
// ---cut-before---
function GardenPlanner({ projectId }: { projectId: string }) {
// Subscribe to a project and its tasks
const project = useCoState(Project, projectId, {
resolve: {
tasks: { $each: true },
},
});
if (!project) {
return project === null
? "Project not found or not accessible"
: "Loading project ...";
}
return (
);
}
```
The `useCoState` hook handles subscribing when the component mounts and unsubscribing when it unmounts, making it easy to keep your UI in sync with the underlying data.
### `useAccount`
`useAccount` is used to access the current user's account.
You can use this at the top-level of your app to subscribe to the current user's [account profile and root](../schemas/accounts-and-migrations#covalues-as-a-graph-of-data-rooted-in-accounts).
Like `useCoState`, you can specify a resolve query to also subscribe to CoValues referenced in the account profile or root.
```tsx twoslash
const Task = co.map({
title: z.string(),
});
const Project = co.map({
name: z.string(),
tasks: co.list(Task),
});
const AccountRoot = co.map({
myProjects: co.list(Project),
});
const MyAppAccount = co.account({
root: AccountRoot,
profile: co.profile(),
});
// ---cut-before---
function ProjectList() {
const { me } = useAccount(MyAppAccount, {
resolve: {
profile: true,
root: {
myProjects: {
$each: {
tasks: true,
},
},
},
},
});
if (!me) {
return
Loading...
;
}
return (
{me.profile.name}'s projects
{me.root.myProjects.map((project) => (
{project.name} ({project.tasks.length} tasks)
))}
);
}
```
## Loading States and Permission Checking
When subscribing to or loading a CoValue, you need to handle three possible states:
- `undefined`: The initial loading state, indicating the value is being fetched
- `null`: The CoValue was not found or is not accessible (e.g., due to permissions)
- `Value`: The successfully loaded CoValue instance
This allows you to handle loading, error, and success states in your application:
```ts twoslash
const Task = co.map({
title: z.string(),
});
const taskId = "co_123";
// ---cut-before---
Task.subscribe(taskId, {}, (task: co.loaded) => {
if (task === undefined) {
console.log("Task is loading...");
} else if (task === null) {
console.log("Task not found or not accessible");
} else {
console.log("Task loaded:", task.title);
}
});
```
## Deep Loading
When working with related CoValues (like tasks in a project), you often need to load not just the top-level object but also its nested references. This is especially important when working with [CoMaps](/docs/using-covalues/comaps) that contain references to other CoValues or with [CoLists](/docs/using-covalues/colists) that contain multiple items. Jazz provides a flexible mechanism for specifying exactly how much of the object graph to load.
### Resolve queries
Resolve queries let you declare exactly which references to load and how deep to go using the `resolve` property:
```ts twoslash
const projectId = "co_123";
// ---cut-before---
const TeamMember = co.map({
name: z.string(),
});
const Task = co.map({
title: z.string(),
assignee: z.optional(TeamMember),
get subtasks(): CoListSchema { return co.list(Task) },
});
const Project = co.map({
name: z.string(),
tasks: co.list(Task),
owner: TeamMember,
});
// Load just the project, not its references
const project = await Project.load(projectId);
if (!project) { throw new Error("Project not found or not accessible"); }
// string - primitive fields are always loaded
project.name;
// undefined | null | ListOfTasks - non-requested references might not be loaded, or inaccessible
project.tasks;
// Load the project and shallowly load its list of tasks
const projectWithTasksShallow = await Project.load(projectId, {
resolve: {
tasks: true
}
});
if (!projectWithTasksShallow) { throw new Error("Project or required references not found or not accessible"); }
// ListOfTasks - shallowly loaded
projectWithTasksShallow.tasks;
// number - length of the list
projectWithTasksShallow.tasks.length;
// undefined | null | Task - items might not be loaded, or inaccessible
projectWithTasksShallow.tasks[0];
// Load the project and its tasks
const projectWithTasks = await Project.load(projectId, {
resolve: {
tasks: {
$each: true
}
}
});
if (!projectWithTasks) { throw new Error("Project or required references not found or not accessible"); }
// ListOfTasks - fully loaded
projectWithTasks.tasks;
// Task - fully loaded
projectWithTasks.tasks[0];
// string - primitive fields are always loaded
projectWithTasks.tasks[0].title;
// undefined | null | ListOfTasks - subtasks might not be loaded, or inaccessible
projectWithTasks.tasks[0].subtasks;
// Load the project, its tasks, and their subtasks
const projectDeep = await Project.load(projectId, {
resolve: {
tasks: {
$each: {
subtasks: {
$each: true
},
assignee: true
}
}
}
});
if (!projectDeep) { throw new Error("Project or required references not found or not accessible"); }
// string - primitive fields are always loaded
projectDeep.tasks[0].subtasks[0].title;
// undefined | null | TeamMember - since assignee is optional:
// TeamMember - set and definitely loaded
// null - set but unavailable/inaccessible
// undefined - not set, or loading (in case of subscription)
projectDeep.tasks[0].assignee;
```
The resolve query defines which parts of the graph you want to load, making it intuitive to express complex loading patterns.
### Loading states and permissions
When loading data with references, the load operation will fail if one of the references is unavailable or if the user doesn't have read access to it. Let's explore what happens in various scenarios:
#### Resolved References
When a user tries to load a reference they don't have access to:
```ts twoslash
const TeamMember = co.map({
name: z.string(),
});
const Task = co.map({
title: z.string(),
assignee: z.optional(TeamMember),
get subtasks(): CoListSchema { return co.list(Task) },
});
const Project = co.map({
name: z.string(),
tasks: co.list(Task),
owner: TeamMember,
});
const taskId = "co_123";
// ---cut-before---
// If assignee is not accessible to the user:
const task = await Task.load(taskId, {
resolve: { assignee: true }
});
task // => null
```
The load operation will fail and return `null` if any requested reference is inaccessible. This maintains data consistency by ensuring all requested references are available before returning the object.
The behavior is the same for optional and required references.
#### List References
When a list contains references to items the user can't access:
```ts twoslash
const TeamMember = co.map({
name: z.string(),
});
const Task = co.map({
title: z.string(),
assignee: z.optional(TeamMember),
get subtasks(): CoListSchema { return co.list(Task) },
});
const Project = co.map({
name: z.string(),
tasks: co.list(Task),
owner: TeamMember,
});
const projectId = "co_123";
// ---cut-before---
// If any item in the list is not accessible:
const project = await Project.load(projectId, {
resolve: { tasks: { $each: true } }
});
project // => null
```
If any item in a list is inaccessible to the user, the entire load operation will fail and return `null`. This is because lists expect all their items to be accessible - a partially loaded list could lead to data inconsistencies.
#### Reading a non-resolved inaccessible reference
When trying to load an object with an inaccessible reference without directly resolving it:
```ts twoslash
const TeamMember = co.map({
name: z.string(),
});
const Task = co.map({
title: z.string(),
assignee: z.optional(TeamMember),
get subtasks(): CoListSchema { return co.list(Task) },
});
const Project = co.map({
name: z.string(),
tasks: co.list(Task),
owner: TeamMember,
});
const projectId = "co_123";
// ---cut-before---
const project = await Project.load(projectId, {
resolve: true
});
project // => Project
// The user doesn't have access to the owner
project?.owner // => always null
```
The load operation will succeed and return the object, but the inaccessible reference will always be `null`.
#### Deep loading lists with shared items
When loading a list with shared items, you can use the `$onError` option to safely load the list skipping any inaccessible items.
This is especially useful when in your app access to these items might be revoked.
This way the inaccessible items are replaced with `null` in the returned list.
```ts twoslash
const me = await createJazzTestAccount();
const account2 = await createJazzTestAccount();
const Person = co.map({
name: z.string(),
});
const Friends = co.list(Person);
const privateGroup = Group.create({ owner: account2 });
const publicGroup = Group.create({ owner: me });
// ---cut-before---
const source = co.list(Person).create(
[
Person.create(
{
name: "Jane",
},
privateGroup, // We don't have access to Jane
),
Person.create(
{
name: "Alice",
},
publicGroup, // We have access to Alice
),
],
publicGroup,
);
const friends = await co.list(Person).load(source.id, {
resolve: {
$each: { $onError: null }
},
loadAs: me,
});
// Thanks to $onError catching the errors, the list is loaded
// because we have access to friends
console.log(friends); // Person[]
// Jane is null because we lack access rights
// and we have used $onError to catch the error on the list items
console.log(friends?.[0]); // null
// Alice is not null because we have access
// the type is nullable because we have used $onError
console.log(friends?.[1]); // Person
```
The `$onError` works as a "catch" clause option to block any error in the resolved children.
```ts twoslash
const me = await createJazzTestAccount();
const account2 = await createJazzTestAccount();
const Dog = co.map({
name: z.string(),
});
const Person = co.map({
name: z.string(),
dog: Dog,
});
const User = co.map({
name: z.string(),
friends: co.list(Person),
});
const privateGroup = Group.create({ owner: account2 });
const publicGroup = Group.create({ owner: me });
// ---cut-before---
const source = co.list(Person).create(
[
Person.create(
{
name: "Jane",
dog: Dog.create(
{ name: "Rex" },
privateGroup,
), // We don't have access to Rex
},
publicGroup,
),
],
publicGroup,
);
const friends = await co.list(Person).load(source.id, {
resolve: {
$each: { dog: true, $onError: null }
},
loadAs: me,
});
// Jane is null because we don't have access to Rex
// and we have used $onError to catch the error on the list items
console.log(friends?.[0]); // null
```
We can actually use `$onError` everywhere in the resolve query, so we can use it to catch the error on dog:
```ts twoslash
const me = await createJazzTestAccount();
const account2 = await createJazzTestAccount();
const Dog = co.map({
name: z.string(),
});
const Person = co.map({
name: z.string(),
dog: Dog,
});
const User = co.map({
name: z.string(),
friends: co.list(Person),
});
const privateGroup = Group.create({ owner: account2 });
const publicGroup = Group.create({ owner: me });
const source = co.list(Person).create(
[
Person.create(
{
name: "Jane",
dog: Dog.create(
{ name: "Rex" },
privateGroup,
), // We don't have access to Rex
},
publicGroup,
),
],
publicGroup,
);
// ---cut-before---
const friends = await co.list(Person).load(source.id, {
resolve: {
$each: { dog: { $onError: null } }
},
loadAs: me,
});
// Jane now is not-nullable at type level because
// we have moved $onError down to the dog field
//
// This also means that if we don't have access to Jane
// the entire friends list will be null
console.log(friends?.[0]); // => Person
// Jane's dog is null because we don't have access to Rex
// and we have used $onError to catch the error
console.log(friends?.[0]?.dog); // => null
```
## Type Safety with `co.loaded` Type
Jazz provides the `co.loaded` type to help you define and enforce the structure of deeply loaded data in your application. This makes it easier to ensure that components receive the data they expect with proper TypeScript validation.
The `co.loaded` type is especially useful when passing data between components, as it guarantees that all necessary nested data has been loaded:
```tsx twoslash
const TeamMember = co.map({
name: z.string(),
});
const Task = co.map({
title: z.string(),
assignee: z.optional(TeamMember),
get subtasks(): CoListSchema {
return co.list(Task);
},
});
const Project = co.map({
name: z.string(),
tasks: co.list(Task),
owner: TeamMember,
});
// ---cut-before---
// Define a type that includes loaded nested data
type ProjectWithTasks = co.loaded<
typeof Project,
{
tasks: { $each: true };
}
>;
// Component that expects a fully loaded project
function TaskList({ project }: { project: ProjectWithTasks }) {
// TypeScript knows tasks are loaded, so this is type-safe
return (
{project.tasks.map((task) => (
{task.title}
))}
);
}
// For more complex resolutions
type FullyLoadedProject = co.loaded<
typeof Project,
{
tasks: {
$each: {
subtasks: true;
assignee: true;
};
};
owner: true;
}
>;
// Function that requires deeply loaded data
function processProject(project: FullyLoadedProject) {
// Safe access to all loaded properties
console.log(`Project ${project.name} owned by ${project.owner.name}`);
project.tasks.forEach((task) => {
console.log(`Task: ${task.title}, Assigned to: ${task.assignee?.name}`);
console.log(`Subtasks: ${task.subtasks.length}`);
});
}
```
```ts twoslash
const TeamMember = co.map({
name: z.string(),
});
const Task = co.map({
title: z.string(),
assignee: z.optional(TeamMember),
get subtasks(): CoListSchema {
return co.list(Task);
},
});
const Project = co.map({
name: z.string(),
tasks: co.list(Task),
owner: TeamMember,
});
// ---cut-before---
// Define a type that includes loaded nested data
type ProjectWithTasks = co.loaded<
typeof Project,
{
tasks: { $each: true };
}
>;
// Function that expects loaded data
async function taskList({ project }: { project: ProjectWithTasks }) {
// TypeScript knows tasks are loaded, so this is type-safe
return project.tasks.map((task) => task.title).join(`\n - `);
}
// For more complex resolutions
type FullyLoadedProject = co.loaded<
typeof Project,
{
tasks: {
$each: {
title: true;
subtasks: true;
assignee: true;
};
};
owner: true;
}
>;
// Function that requires deeply loaded data
function processProject(project: FullyLoadedProject) {
// Safe access to all loaded properties
console.log(`Project ${project.name} owned by ${project.owner.name}`);
project.tasks.forEach((task) => {
console.log(`Task: ${task.title}, Assigned to: ${task.assignee?.name}`);
console.log(`Subtasks: ${task.subtasks.length}`);
});
}
```
Using the `co.loaded` type helps catch errors at compile time rather than runtime, ensuring that your components and functions receive data with the proper resolution depth. This is especially useful for larger applications where data is passed between many components.
## Ensuring Data is Loaded
Sometimes you need to make sure data is loaded before proceeding with an operation. The `ensureLoaded` method lets you guarantee that a CoValue and its referenced data are loaded to a specific depth:
```ts twoslash
const TeamMember = co.map({
name: z.string(),
});
const Task = co.map({
title: z.string(),
status: z.literal(["todo", "in-progress", "completed"]),
assignee: z.string().optional(),
get subtasks(): CoListSchema {
return co.list(Task);
},
});
const Project = co.map({
name: z.string(),
tasks: co.list(Task),
owner: TeamMember,
});
// ---cut-before---
async function completeAllTasks(projectId: string) {
// Ensure the project is loaded
const project = await Project.load(projectId, { resolve: true });
if (!project) return;
// Ensure tasks are loaded
const loadedProject = await project.ensureLoaded({
resolve: {
tasks: {
$each: true,
},
},
});
// Now we can safely access and modify tasks
loadedProject.tasks.forEach((task) => {
task.status = "completed";
});
}
```
## Best Practices
1. **Be explicit about resolution depths**: Always specify exactly what you need
2. **Use framework integrations**: They handle subscription lifecycle automatically
3. **Clean up subscriptions**: Always store and call the unsubscribe function when you're done
4. **Handle all loading states**: Check for undefined (loading), null (not found), and success states
5. **Use the `co.loaded` type**: Add compile-time type safety for components that require specific resolution patterns
#### 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 twoslash
// ---cut---
const Task = co.map({
title: z.string(),
status: z.literal(["todo", "in-progress", "completed"]),
});
export type Task = co.loaded;
```
## The _edits Property
Every CoValue has an `_edits` property that contains the complete history for each field. Here's
how to get the edit history for `task.status`:
```ts twoslash
const me = await createJazzTestAccount();
const Task = co.map({
title: z.string(),
status: z.literal(["todo", "in-progress", "completed"]),
});
const task = Task.create({ title: "New task", status: "todo" }, { owner: me });
// ---cut---
// Access edit history for a field
task._edits.status
// Returns the latest edit
task._edits.status?.all
// Returns array of all edits in chronological order
// Check if edits exist
if (task._edits.status) {
const name = task._edits.status.by?.profile?.name;
console.log(`Last changed by ${name}`);
}
```
## Edit Structure
Each edit contains:
```ts twoslash
const me = await createJazzTestAccount();
const Task = co.map({
title: z.string(),
status: z.literal(["todo", "in-progress", "completed"]),
});
const task = Task.create({ title: "New task", status: "todo" }, { owner: me });
task.status = "in-progress";
// ---cut---
const edit = task._edits.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 twoslash
const me = await createJazzTestAccount();
const Task = co.map({
title: z.string(),
status: z.literal(["todo", "in-progress", "completed"]),
});
const task = Task.create({ title: "New task", status: "todo" }, { owner: me });
// ---cut---
// Direct access to latest edit
const latest = task._edits.title;
if (latest) {
console.log(`Title is now "${latest.value}"`);
}
```
### All Edits
Get the complete history for a field:
```ts twoslash
const me = await createJazzTestAccount();
const Task = co.map({
title: z.string(),
status: z.literal(["todo", "in-progress", "completed"]),
});
const task = Task.create({ title: "New task", status: "todo" }, { owner: me });
task.status = "in-progress";
task.status = "completed";
// ---cut---
// Get all edits (chronologically)
const allStatusEdits = task._edits.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 twoslash
const me = await createJazzTestAccount();
const Task = co.map({
title: z.string(),
status: z.literal(["todo", "in-progress", "completed"]),
});
const task = Task.create({ title: "New task", status: "todo" }, { owner: me });
task.status = "in-progress";
// ---cut---
const allEdits = task._edits.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 `_createdAt` and `_lastUpdatedAt` getters.
```tsx twoslash
const Task = co.map({
title: z.string(),
status: z.literal(["todo", "in-progress", "completed"]),
});
const task = Task.create({ title: "New task", status: "todo" });
// ---cut---
console.log(new Date(task._createdAt));
console.log(new Date(task._lastUpdatedAt));
```
## Requirements
- CoValues must be loaded to access history (see [Subscription & Loading](/docs/using-covalues/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/design-patterns/history-patterns):
- Building audit logs
- Creating activity feeds
- Implementing undo/redo
- Showing change indicators
- Querying historical data
### Groups, permissions & sharing
#### Groups as permission scopes
# Groups as permission scopes
Every CoValue has an owner, which can be a `Group` or an `Account`.
You can use a `Group` to grant access to a CoValue to **multiple users**. These users can
have different roles, such as "writer", "reader" or "admin".
## Creating a Group
Here's how you can create a `Group`.
```tsx twoslash
const group = Group.create();
```
The `Group` itself is a CoValue, and whoever owns it is the initial admin.
You typically add members using [public sharing](/docs/groups/sharing#public-sharing) or [invites](/docs/groups/sharing#invites).
But if you already know their ID, you can add them directly (see below).
## Adding group members by ID
You can add group members by ID by using `Account.load` and `Group.addMember`.
```tsx twoslash
const bobsID = "co_z123";
// ---cut---
const group = Group.create();
const bob = await co.account().load(bobsID);
if (bob) {
group.addMember(bob, "writer");
}
```
**Note:** if the account ID is of type `string`, because it comes from a URL parameter or something similar, you need to cast it to `ID` first:
```tsx twoslash
const bobsID = "co_z123";
const group = Group.create();
// ---cut---
const bob = await co.account().load(bobsID);
if (bob) {
group.addMember(bob, "writer");
}
```
## Changing a member's role
To change a member's role, use the `addMember` method.
```ts twoslash
const bob = await createJazzTestAccount();
const group = Group.create();
// ---cut---
group.addMember(bob, "reader");
```
Bob just went from a writer to a reader.
**Note:** only admins can change a member's role.
## Removing a member
To remove a member, use the `removeMember` method.
```ts twoslash
const bob = await createJazzTestAccount();
const group = Group.create();
// ---cut---
group.removeMember(bob);
```
Rules:
- All roles can remove themselves.
- Only admins can remove other users.
- An admin cannot remove other admins.
- As an admin, you cannot remove yourself if you are the only admin in the Group, because there has to be at least one admin present.
## Getting the Group of an existing CoValue
You can get the group of an existing CoValue by using `coValue._owner`.
```ts twoslash
const existingCoValue = await createJazzTestAccount();
const MyCoMap = co.map({
color: z.string(),
});
// ---cut---
const group = existingCoValue._owner;
const newValue = MyCoMap.create(
{ color: "red"},
{ owner: group }
);
```
Because `._owner` can be an `co.account` or a `Group`, in cases where you specifically need to use `Group` methods (such as for adding members or getting your own role), you can cast it to assert it to be a Group:
```ts twoslash
const bob = await createJazzTestAccount();
const MyCoMap = co.map({
color: z.string(),
});
const existingCoValue = MyCoMap.create(
{ color: "red"},
{ owner: bob }
);
// ---cut---
const group = existingCoValue._owner.castAs(Group);
group.addMember(bob, "writer");
const role = group.getRoleOf(bob.id);
```
## Checking the permissions
You can check the permissions of an account on a CoValue by using the `canRead`, `canWrite` and `canAdmin` methods.
```ts twoslash
const MyCoMap = co.map({
color: z.string(),
});
// ---cut---
const value = await MyCoMap.create({ color: "red"})
const me = await co.account().getMe();
if (me.canAdmin(value)) {
console.log("I can share value with others");
} else if (me.canWrite(value)) {
console.log("I can edit value");
} else if (me.canRead(value)) {
console.log("I can view value");
} else {
console.log("I cannot access value");
}
```
To check the permissions of another account, you need to load it first:
```ts twoslash
const MyCoMap = co.map({
color: z.string(),
});
const account = await createJazzTestAccount();
const accountID = account.id;
// ---cut---
const value = await MyCoMap.create({ color: "red"})
const bob = await co.account().load(accountID);
if (bob) {
if (bob.canAdmin(value)) {
console.log("Bob can share value with others");
} else if (bob.canWrite(value)) {
console.log("Bob can edit value");
} else if (bob.canRead(value)) {
console.log("Bob can view value");
} else {
console.log("Bob cannot access value");
}
}
```
#### Public sharing & invites
# Public sharing and invites
## Public sharing
You can share CoValues publicly by setting the `owner` to a `Group`, and granting access to "everyone".
```ts twoslash
// ---cut---
const group = Group.create();
group.addMember("everyone", "writer");
```
You can also use `makePublic(role)` alias to grant access to everyone with a specific role (defaults to `reader`).
```ts twoslash
// ---cut---
const group = Group.create();
group.addMember("everyone", "writer"); // [!code --]
group.makePublic("writer"); // [!code ++]
// group.makePublic(); // Defaults to "reader" access
```
This is done in the [chat example](https://github.com/garden-co/jazz/tree/main/examples/chat) where anyone can join the chat, and send messages.
You can also [add members by Account ID](/docs/groups/intro#adding-group-members-by-id).
## Invites
You can grant users access to a CoValue by sending them an invite link.
This is used in the [todo example](https://github.com/garden-co/jazz/tree/main/examples/todo).
```ts twoslash
const Organization = co.map({
name: z.string(),
});
const organization = Organization.create({ name: "Garden Computing" });
// ---cut---
createInviteLink(organization, "writer"); // or reader, admin, writeOnly
```
It generates a URL that looks like `.../invite/[CoValue ID]/[inviteSecret]`
In your app, you need to handle this route, and let the user accept the invitation,
as done [here](https://github.com/garden-co/jazz/tree/main/examples/todo/src/2_main.tsx).
```ts twoslash
const Organization = co.map({
name: z.string(),
});
const organization = Organization.create({ name: "Garden Computing" });
const organizationID = organization.id;
// ---cut---
useAcceptInvite({
invitedObjectSchema: Organization,
onAccept: (organizationID) => {
console.log("Accepted invite!")
// navigate to the organization page
},
});
```
```ts twoslash
const Organization = co.map({
name: z.string(),
});
const organization = Organization.create({ name: "Garden Computing" });
const organizationID = organization.id;
// ---cut---
useAcceptInviteNative({
invitedObjectSchema: Organization,
onAccept: (organizationID) => {
console.log("Accepted invite!")
// navigate to the organization page
},
});
```
```ts twoslash
const Organization = co.map({
name: z.string(),
});
const organization = Organization.create({ name: "Garden Computing" });
const organizationID = organization.id;
// ---cut---
useAcceptInviteNative({
invitedObjectSchema: Organization,
onAccept: (organizationID) => {
console.log("Accepted invite!")
// navigate to the organization page
},
});
```
```ts twoslash
const Organization = co.map({
name: z.string(),
});
const organization = Organization.create({ name: "Garden Computing" });
const organizationID = organization.id;
// ---cut---
new InviteListener({
invitedObjectSchema: Organization,
onAccept: (organizationID) => {
console.log("Accepted invite!")
// navigate to the organization page
},
});
```
You can accept an invitation programmatically by using the `acceptInvite` method on an account.
Pass the ID of the CoValue you're being invited to, the secret from the invite link, and the schema of the CoValue.
```ts twoslash
const Organization = co.map({
name: z.string(),
});
const account = {} as unknown as Account;
const organizationId = "";
const inviteSecret = "inviteSecret_z";
// ---cut---
await account.acceptInvite(
organizationId,
inviteSecret,
Organization
);
```
### Requesting Invites
To allow a non-group member to request an invitation to a group you can use the `writeOnly` role.
This means that users only have write access to a specific requests list (they can't read other requests).
However, Administrators can review and approve these requests.
Create the data models.
```ts twoslash
// ---cut---
const JoinRequest = co.map({
account: co.account,
status: z.literal(["pending", "approved", "rejected"]),
});
const RequestsList = co.list(JoinRequest);
```
Set up the request system with appropriate access controls.
```ts twoslash
const JoinRequest = co.map({
account: co.account(),
status: z.literal(["pending", "approved", "rejected"]),
});
const RequestsList = co.list(JoinRequest);
const Account = co.account();
type Account = co.loaded;
// ---cut-before---
function createRequestsToJoin() {
const requestsGroup = Group.create();
requestsGroup.addMember("everyone", "writeOnly");
return RequestsList.create([], requestsGroup);
}
async function sendJoinRequest(
requestsList: co.loaded,
account: Account,
) {
const request = JoinRequest.create(
{
account,
status: "pending",
},
requestsList._owner // Inherit the access controls of the requestsList
);
requestsList.push(request);
return request;
}
```
Using the write-only access users can submit requests that only administrators can review and approve.
```ts twoslash
const Account = co.account();
type Account = co.loaded;
const JoinRequest = co.map({
account: Account,
status: z.literal(["pending", "approved", "rejected"]),
});
const RequestsList = co.list(JoinRequest);
const RequestsToJoin = co.map({
writeOnlyInvite: z.string(),
requests: RequestsList,
});
// ---cut-before---
async function approveJoinRequest(
joinRequest: co.loaded,
targetGroup: Group,
) {
const account = await Account.load(joinRequest._refs.account.id);
if (account) {
targetGroup.addMember(account, "reader");
joinRequest.status = "approved";
return true;
} else {
return false;
}
}
```
#### Groups as members
# Groups as members
Groups can be added to other groups using the `addMember` method.
When a group is added as a member of another group, members of the added group will become part of the containing group.
## Basic usage
Here's how to add a group as a member of another group:
```ts twoslash
// ---cut---
const playlistGroup = Group.create();
const trackGroup = Group.create();
// Tracks are now visible to the members of playlist
trackGroup.addMember(playlistGroup);
```
When you add groups as members:
- Members of the added group become members of the container group
- Their roles are inherited (with some exceptions, see [below](#the-rules-of-role-inheritance))
- Revoking access from the member group also removes its access to the container group
## Levels of inheritance
Adding a group as a member of another is not limited in depth:
```ts twoslash
// ---cut---
const grandParentGroup = Group.create();
const parentGroup = Group.create();
const childGroup = Group.create();
childGroup.addMember(parentGroup);
parentGroup.addMember(grandParentGroup);
```
Members of the grandparent group will get access to all descendant groups based on their roles.
## Roles
### The rules of role inheritance
If the account is already a member of the container group, it will get the more permissive role:
```ts twoslash
const bob = await createJazzTestAccount();
// ---cut---
const addedGroup = Group.create();
addedGroup.addMember(bob, "reader");
const containingGroup = Group.create();
addedGroup.addMember(bob, "writer");
containingGroup.addMember(addedGroup);
// Bob stays a writer because his role is higher
// than the inherited reader role.
```
When adding a group to another group, only admin, writer and reader roles are inherited:
```ts twoslash
const bob = await createJazzTestAccount();
// ---cut---
const addedGroup = Group.create();
addedGroup.addMember(bob, "writeOnly");
const containingGroup = Group.create();
containingGroup.addMember(addedGroup);
// Bob does not become a member of the containing group
```
To add a group to another group:
1. The current account must be an admin in the containing group
2. The current account must be a member of the added group
```ts twoslash
const group = Group.create();
const Company = co.map({});
const company = Company.create({ owner: group });
// ---cut---
const companyGroup = company._owner.castAs(Group);
const teamGroup = Group.create();
// Works only if I'm a member of `companyGroup`
teamGroup.addMember(companyGroup);
```
### Overriding the added group's roles
In some cases you might want to inherit all members from an added group but override their roles to the same specific role in the containing group. You can do so by passing an "override role" as a second argument to `addMember`:
```ts twoslash
const bob = await createJazzTestAccount();
// ---cut---
const organizationGroup = Group.create();
organizationGroup.addMember(bob, "admin");
const billingGroup = Group.create();
// This way the members of the organization
// can only read the billing data
billingGroup.addMember(organizationGroup, "reader");
```
The "override role" works in both directions:
```ts twoslash
const alice = await createJazzTestAccount();
const bob = await createJazzTestAccount();
// ---cut---
const addedGroup = Group.create();
addedGroup.addMember(bob, "reader");
addedGroup.addMember(alice, "admin");
const containingGroup = Group.create();
containingGroup.addMember(addedGroup, "writer");
// Bob and Alice are now writers in the containing group
```
### Permission changes
When you remove a member from an added group, they automatically lose access to all containing groups. We handle key rotation automatically to ensure security.
```ts twoslash
const bob = await createJazzTestAccount();
const addedGroup = Group.create();
// ---cut---
// Remove member from added group
await addedGroup.removeMember(bob);
// Bob loses access to both groups.
// If Bob was also a member of the containing group,
// he wouldn't have lost access.
```
## Removing groups from other groups
You can remove a group from another group by using the `removeMember` method:
```ts twoslash
// ---cut---
const addedGroup = Group.create();
const containingGroup = Group.create();
containingGroup.addMember(addedGroup);
// Revoke the extension
await containingGroup.removeMember(addedGroup);
```
## Getting all added groups
You can get all of the groups added to a group by calling the `getParentGroups` method:
```ts twoslash
// ---cut---
const containingGroup = Group.create();
const addedGroup = Group.create();
containingGroup.addMember(addedGroup);
console.log(containingGroup.getParentGroups()); // [addedGroup]
```
## Example: Team Hierarchy
Here's a practical example of using group inheritance for team permissions:
```ts twoslash
const CEO = await createJazzTestAccount();
const teamLead = await createJazzTestAccount();
const developer = await createJazzTestAccount();
const client = await createJazzTestAccount();
// ---cut---
// Company-wide group
const companyGroup = Group.create();
companyGroup.addMember(CEO, "admin");
// Team group with elevated permissions
const teamGroup = Group.create();
teamGroup.addMember(companyGroup); // Inherits company-wide access
teamGroup.addMember(teamLead, "admin");
teamGroup.addMember(developer, "writer");
// Project group with specific permissions
const projectGroup = Group.create();
projectGroup.addMember(teamGroup); // Inherits team permissions
projectGroup.addMember(client, "reader"); // Client can only read project items
```
This creates a hierarchy where:
- The CEO has admin access to everything
- Team members get writer access to team and project content
- Team leads get admin access to team and project content
- The client can only read project content
### Authentication
#### Overview
# Authentication in Jazz
Jazz authentication is based on cryptographic keys ("Account keys"). Their public part represents a user's identity, their secret part lets you act as that user.
## Authentication Flow
When a user first opens your app, they'll be in one of these states:
- **Anonymous Authentication**: Default starting point where Jazz automatically creates a local account on first visit. Data persists on one device and can be upgraded to a full account.
- **Authenticated Account**: Full account accessible across multiple devices using [passkeys](./passkey), [passphrases](./passphrase), or third-party authentications, such as [Clerk](./clerk).
- **Guest Mode**: No account, read-only access to public content. Users can browse but can't save data or sync.
Learn more about these states in the [Authentication States](./authentication-states) documentation.
Without authentication, users are limited to using the application on only one device.
When a user logs out of an Authenticated Account, they return to the Anonymous Authentication state with a new local account.
Here's what happens during registration and login:
- **Register**: When a user registers with an authentication provider, their Anonymous account credentials are stored in the auth provider, and the account is marked as Authenticated. The user keeps all their existing data.
- **Login**: When a user logs in with an authentication provider, their Anonymous account is discarded and the credentials are loaded from the auth provider. Data from the Anonymous account can be transferred using the [onAnonymousAccountDiscarded handler](./authentication-states#migrating-data-from-anonymous-to-authenticated-account).
## Available Authentication Methods
Jazz provides several ways to authenticate users:
- [**Passkeys**](./passkey): Secure, biometric authentication using WebAuthn
- [**Passphrases**](./passphrase): Bitcoin-style word phrases that users store
- [**Clerk Integration**](./clerk): Third-party authentication service with OAuth support
#### Authentication States
# Authentication States
Jazz provides three distinct authentication states that determine how users interact with your app: **Anonymous Authentication**, **Guest Mode**, and **Authenticated Account**.
## Anonymous Authentication
When a user loads a Jazz application for the first time, we create a new Account by generating keys and storing them locally:
- Users have full accounts with unique IDs
- Data persists between sessions on the same device
- Can be upgraded to a full account (passkey, passphrase, etc.)
- Data syncs across the network (if enabled)
## Authenticated Account
**Authenticated Account** provides full multi-device functionality:
- Persistent identity across multiple devices
- Full access to all application features
- Data can sync across all user devices
- Multiple authentication methods available
## Guest Mode
**Guest Mode** provides a completely accountless context:
- No persistent identity or account
- Only provides access to publicly readable content
- Cannot save or sync user-specific data
- Suitable for read-only access to public resources
## Detecting Authentication State
You can detect the current authentication state using `useAccount` and `useIsAuthenticated`.
```tsx twoslash
// ---cut---
function AuthStateIndicator() {
const { agent } = useAccount();
const isAuthenticated = useIsAuthenticated();
// Check if guest mode is enabled in JazzReactProvider
const isGuest = agent._type !== "Account"
// Anonymous authentication: has an account but not fully authenticated
const isAnonymous = agent._type === "Account" && !isAuthenticated;
return (
);
}
```
## Migrating data from anonymous to authenticated account
When a user signs up, their anonymous account is transparently upgraded to an authenticated account, preserving all their data.
However, if a user has been using your app anonymously and later logs in with an existing account, their anonymous account data would normally be discarded. To prevent data loss, you can use the `onAnonymousAccountDiscarded` handler.
This example from our [music player example app](https://github.com/garden-co/jazz/tree/main/examples/music-player) shows how to migrate data:
```ts twoslash
const MusicTrack = co.map({
title: z.string(),
duration: z.number(),
isExampleTrack: z.boolean().optional(),
});
const Playlist = co.map({
title: z.string(),
tracks: co.list(MusicTrack),
});
const MusicaAccountRoot = co.map({
rootPlaylist: Playlist,
});
const MusicaAccount = co.account({
root: MusicaAccountRoot,
profile: co.profile({}),
});
type MusicaAccount = co.loaded
// ---cut---
export async function onAnonymousAccountDiscarded(
anonymousAccount: MusicaAccount,
) {
const { root: anonymousAccountRoot } = await anonymousAccount.ensureLoaded({
resolve: {
root: {
rootPlaylist: {
tracks: {
$each: true,
},
},
},
},
});
const me = await MusicaAccount.getMe().ensureLoaded({
resolve: {
root: {
rootPlaylist: {
tracks: true,
},
},
},
});
for (const track of anonymousAccountRoot.rootPlaylist.tracks) {
if (track.isExampleTrack) continue;
const trackGroup = track._owner.castAs(Group);
trackGroup.addMember(me, "admin");
me.root.rootPlaylist.tracks.push(track);
}
}
```
To see how this works, try uploading a song in the [music player demo](https://music.demo.jazz.tools/) and then log in with an existing account.
## Provider Configuration for Authentication
You can configure how authentication states work in your app with the [JazzReactProvider](/docs/project-setup/providers/). The provider offers several options that impact authentication behavior:
- `guestMode`: Enable/disable Guest Mode
- `onAnonymousAccountDiscarded`: Handle data migration when switching accounts
- `sync.when`: Control when data synchronization happens
- `defaultProfileName`: Set default name for new user profiles
For detailed information on all provider options, see [Provider Configuration options](/docs/project-setup/providers/#additional-options).
## Controlling sync for different authentication states
You can control network sync with [Providers](/docs/project-setup/providers/) based on authentication state:
- `when: "always"`: Sync is enabled for both Anonymous Authentication and Authenticated Account
- `when: "signedUp"`: Sync is enabled when the user is authenticated
- `when: "never"`: Sync is disabled, content stays local
```tsx twoslash
const apiKey = "you@example.com";
function App() {
return
Hello World
;
}
// ---cut---
```
### Disable sync for Anonymous Authentication
You can disable network sync to make your app local-only under specific circumstances.
For example, you may want to give users with Anonymous Authentication the opportunity to try your app locally-only (incurring no sync traffic), then enable network sync only when the user is fully authenticated.
```tsx twoslash
const apiKey = "you@example.com";
function App() {
return
Hello World
;
}
// ---cut---
```
### Configuring Guest Mode Access
You can configure Guest Mode access with the `guestMode` prop for [Providers](/docs/project-setup/providers/).
```tsx twoslash
const apiKey = "you@example.com";
function App() {
return
Hello World
;
}
// ---cut---
```
For more complex behaviours, you can manually control sync by statefully switching when between `"always"` and `"never"`.
#### Passkey
# Passkey Authentication
Passkey authentication is fully local-first and the most secure of the auth methods that Jazz provides because keys are managed by the device/operating system itself.
## How it works
Passkey authentication is based on the [Web Authentication API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API) and uses familiar FaceID/TouchID flows that users already know how to use.
## Key benefits
- **Most secure**: Keys are managed by the device/OS
- **User-friendly**: Uses familiar biometric verification (FaceID/TouchID)
- **Cross-device**: Works across devices with the same biometric authentication
- **No password management**: Users don't need to remember or store anything
- **Wide support**: Available in most modern browsers
## Implementation
Using passkeys in Jazz is as easy as this:
```tsx twoslash
type AuthModalProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
}
// ---cut---
export function AuthModal({ open, onOpenChange }: AuthModalProps) {
const [username, setUsername] = useState("");
const auth = usePasskeyAuth({ // Must be inside the JazzProvider!
appName: "My super-cool web app",
});
if (auth.state === "signedIn") { // You can also use `useIsAuthenticated()`
return
);
}
```
## Examples
You can try passkey authentication using our [passkey example](https://passkey.demo.jazz.tools/) or the [music player demo](https://music.demo.jazz.tools/).
## When to use Passkeys
Passkeys are ideal when:
- Security is a top priority
- You want the most user-friendly authentication experience
- You're targeting modern browsers and devices
- You want to eliminate the risk of password-based attacks
## Limitations and considerations
- Requires hardware/OS support for biometric authentication
- Not supported in older browsers (see browser support below)
- Requires a fallback method for unsupported environments
### Browser Support
[Passkeys are supported in most modern browsers](https://caniuse.com/passkeys).
For older browsers, we recommend using [passphrase authentication](./passphrase) as a fallback.
## Additional resources
For more information about the Web Authentication API and passkeys:
- [WebAuthn.io](https://webauthn.io/)
- [MDN Web Authentication API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API)
#### Passphrase
# Passphrase Authentication
Passphrase authentication lets users log into any device using a recovery phrase consisting of multiple words (similar to cryptocurrency wallets). Users are responsible for storing this passphrase safely.
## How it works
When a user creates an account with passphrase authentication:
1. Jazz generates a unique recovery phrase derived from the user's cryptographic keys
2. This phrase consists of words from a wordlist
3. Users save this phrase and enter it when logging in on new devices
You can use one of the ready-to-use wordlists from the [BIP39 repository](https://github.com/bitcoinjs/bip39/tree/a7ecbfe2e60d0214ce17163d610cad9f7b23140c/src/wordlists) or create your own.
## Key benefits
- **Portable**: Works across any device, even without browser or OS support
- **User-controlled**: User manages their authentication phrase
- **Flexible**: Works with any wordlist you choose
- **Offline capable**: No external dependencies
## Implementation
```tsx twoslash
// @filename: wordlist.ts
export const wordlist = ["apple", "banana", "cherry", "date", "elderberry", "fig", "grape", "honeydew", "kiwi", "lemon", "mango", "orange", "pear", "quince", "raspberry", "strawberry", "tangerine", "uva", "watermelon", "xigua", "yuzu", "zucchini"];
// @filename: AuthModal.tsx
type AuthModalProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
}
// ---cut---
export function AuthModal({ open, onOpenChange }: AuthModalProps) {
const [loginPassphrase, setLoginPassphrase] = useState("");
const auth = usePassphraseAuth({ // Must be inside the JazzProvider!
wordlist: wordlist,
});
if (auth.state === "signedIn") { // You can also use `useIsAuthenticated()`
return
);
}
```
```tsx twoslash
// @filename: wordlist.ts
export const wordlist = ["apple", "banana", "cherry", "date", "elderberry", "fig", "grape", "honeydew", "kiwi", "lemon", "mango", "orange", "pear", "quince", "raspberry", "strawberry", "tangerine", "uva", "watermelon", "xigua", "yuzu", "zucchini"];
// @filename: AuthModal.tsx
type AuthModalProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
}
// ---cut---
export function AuthModal({ open, onOpenChange }: AuthModalProps) {
const [loginPassphrase, setLoginPassphrase] = useState("");
const auth = usePassphraseAuth({
wordlist: wordlist,
});
if (auth.state === "signedIn") {
return You are already signed in;
}
const handleSignUp = async () => {
await auth.signUp();
onOpenChange(false);
};
const handleLogIn = async () => {
await auth.logIn(loginPassphrase);
onOpenChange(false);
};
return (
Your current passphraseLog in with your passphrase
);
}
```
## Examples
You can see passphrase authentication in our [passphrase example](https://passphrase.demo.jazz.tools/) or the [todo list demo](https://todo.demo.jazz.tools/).
## When to use Passphrases
Passphrase authentication is ideal when:
- You need to support older browsers without WebAuthn capabilities
- Your users need to access the app on many different devices
- You want a fallback authentication method alongside passkeys
## Limitations and considerations
- **User responsibility**: Users must securely store their passphrase
- **Recovery concerns**: If a user loses their passphrase, they cannot recover their account
- **Security risk**: Anyone with the passphrase can access the account
- **User experience**: Requires users to enter a potentially long phrase
Make sure to emphasize to your users:
1. Store the passphrase in a secure location (password manager, written down in a safe place)
2. The passphrase is the only way to recover their account
3. Anyone with the passphrase can access the account
#### Clerk
### react-native Implementation
# Clerk Authentication
We do not currently support Clerk in React Native, but we do have support for [React Native Expo](/docs/react-native-expo/authentication/clerk).
### Design patterns
#### Autosaving forms
# How to write autosaving forms to create and update CoValues
This guide shows you a simple and powerful way to implement forms for creating and updating CoValues.
We'll build:
1. An update form that saves changes as you make them, removing the need for a save button.
2. A create form that autosaves your changes into a draft, so you can come back to it later.
[See the full example here.](https://github.com/garden-co/jazz/tree/main/examples/form)
**Note**: If you do need a save button on your update form, this guide is not for you. Another option is to use [react-hook-form](https://www.react-hook-form.com).
## 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.name = e.target.value}
/>
```
It's that simple!
## Creating a CoValue
However, when creating a CoValue, the CoValue does not exist yet, so we don't have the advantages previously mentioned.
There's a way around this, and it provides unexpected benefits too.
### Using a Draft CoValue
Let's say we have a CoValue called `BubbleTeaOrder`. We can create a "draft" CoValue,
which is an empty version of a `BubbleTeaOrder`, that we can then modify when we are "creating"
a new CoValue.
A `DraftBubbleTeaOrder` is essentially a copy of `BubbleTeaOrder`, but with all the fields made optional.
```tsx twoslash
// ---cut---
// schema.ts
export const BubbleTeaOrder = co.map({
name: z.string(),
});
export const DraftBubbleTeaOrder = co.map({
name: z.optional(z.string()),
});
```
## Writing the components in React
Let's write the form component that will be used for both create and update.
```tsx twoslash
export const BubbleTeaOrder = co.map({
name: z.string(),
});
export const DraftBubbleTeaOrder = co.map({
name: z.optional(z.string()),
});
// ---cut---
// OrderForm.tsx
export function OrderForm({
order,
onSave,
}: {
order: co.loaded | co.loaded;
onSave?: (e: React.FormEvent) => void;
}) {
return (
);
}
```
### Writing the edit form
To make the edit form, simply pass the `BubbleTeaOrder`.
```tsx twoslash
export const BubbleTeaOrder = co.map({
name: z.string(),
});
export const DraftBubbleTeaOrder = co.map({
name: z.optional(z.string()),
});
export function OrderForm({
order,
onSave,
}: {
order: co.loaded | co.loaded;
onSave?: (e: React.FormEvent) => void;
}) {
return (
);
}
// ---cut---
// EditOrder.tsx
export function EditOrder(props: { id: string }) {
const order = useCoState(BubbleTeaOrder, props.id);
if (!order) return;
return ;
}
```
### Writing the create form
For the create form, we need to:
1. Create a draft order.
2. Edit the draft order.
3. Convert the draft order to a "real" order on submit.
Here's how that looks like:
```tsx twoslash
export const BubbleTeaOrder = co.map({
name: z.string(),
});
export const DraftBubbleTeaOrder = co.map({
name: z.optional(z.string()),
});
export const AccountRoot = co.map({
draft: DraftBubbleTeaOrder,
});
export const JazzAccount = co.account({
root: AccountRoot,
profile: co.map({ name: z.string() }),
});
export function OrderForm({
order,
onSave,
}: {
order: co.loaded | co.loaded;
onSave?: (e: React.FormEvent) => void;
}) {
return (
);
}
// ---cut---
// CreateOrder.tsx
export function CreateOrder() {
const { me } = useAccount();
const [draft, setDraft] = useState>();
useEffect(() => {
setDraft(DraftBubbleTeaOrder.create({}));
}, [me?.id]);
const onSave = (e: React.FormEvent) => {
e.preventDefault();
if (!draft || !draft.name) return;
const order = draft as co.loaded; // TODO: this should narrow correctly
console.log("Order created:", order);
};
if (!draft) return;
return ;
}
```
## Validation
In a `BubbleTeaOrder`, the `name` field is required, so it would be a good idea to validate this before turning the draft into a real order.
Update the schema to include a `validate` helper.
```ts twoslash
// ---cut---
// schema.ts
export const DraftBubbleTeaOrder = co.map({
name: z.optional(z.string()),
}).withHelpers((Self) => ({ // [!code ++:11]
validate(draft: co.loaded) {
const errors: string[] = [];
if (!draft.name) {
errors.push("Please enter a name.");
}
return { errors };
},
}));
```
Then perform the validation on submit.
```tsx twoslash
export const BubbleTeaOrder = co.map({
name: z.string(),
});
export const DraftBubbleTeaOrder = co.map({
name: z.optional(z.string()),
}).withHelpers((Self) => ({
validate(draft: co.loaded) {
const errors: string[] = [];
if (!draft.name) {
errors.push("Please enter a name.");
}
return { errors };
},
}));
export const AccountRoot = co.map({
draft: DraftBubbleTeaOrder,
});
export const JazzAccount = co.account({
root: AccountRoot,
profile: co.map({ name: z.string() }),
});
export function OrderForm({
order,
onSave,
}: {
order: co.loaded | co.loaded;
onSave?: (e: React.FormEvent) => void;
}) {
return (
);
}
// ---cut---
// CreateOrder.tsx
export function CreateOrder() {
const { me } = useAccount();
const [draft, setDraft] = useState>();
useEffect(() => {
setDraft(DraftBubbleTeaOrder.create({}));
}, [me?.id]);
const onSave = (e: React.FormEvent) => {
e.preventDefault();
if (!draft) return;
const validation = DraftBubbleTeaOrder.validate(draft); // [!code ++:5]
if (validation.errors.length > 0) {
console.log(validation.errors);
return;
}
const order = draft as co.loaded;
console.log("Order created:", order);
};
if (!draft) return;
return ;
}
```
## Saving the user's work-in-progress
It turns out that using this pattern also provides a UX improvement.
By storing the draft in the user's account, they can come back to it anytime without losing their work. 🙌
```ts twoslash
// ---cut---
// schema.ts
export const BubbleTeaOrder = co.map({
name: z.string(),
});
export const DraftBubbleTeaOrder = co.map({
name: z.optional(z.string()),
});
export const AccountRoot = co.map({ // [!code ++:15]
draft: DraftBubbleTeaOrder,
});
export const JazzAccount = co.account({
root: AccountRoot,
profile: co.map({ name: z.string() }),
}).withMigration((account, creationProps?: { name: string }) => {
if (account.root === undefined) {
const draft = DraftBubbleTeaOrder.create({});
account.root = AccountRoot.create({ draft });
}
});
```
Let's not forget to update the `AccountSchema`.
```ts twoslash
// @filename: schema.tsx
export const BubbleTeaOrder = co.map({
name: z.string(),
});
export const DraftBubbleTeaOrder = co.map({
name: z.optional(z.string()),
});
export const AccountRoot = co.map({
draft: DraftBubbleTeaOrder,
});
export const JazzAccount = co.account({
root: AccountRoot,
profile: co.map({ name: z.string() }),
}).withMigration((account, creationProps?: { name: string }) => {
if (account.root === undefined) {
const draft = DraftBubbleTeaOrder.create({});
account.root = AccountRoot.create({ draft });
}
});
// @filename: App.tsx
// ---cut---
export function MyJazzProvider({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
Instead of creating a new draft every time we use the create form, let's use the draft from the account root.
```tsx twoslash
// @filename: schema.ts
export const BubbleTeaOrder = co.map({
name: z.string(),
});
export const DraftBubbleTeaOrder = co.map({
name: z.optional(z.string()),
}).withHelpers((Self) => ({
validate(draft: co.loaded) {
const errors: string[] = [];
if (!draft.name) {
errors.push("Please enter a name.");
}
return { errors };
},
}));
export const AccountRoot = co.map({
draft: DraftBubbleTeaOrder,
});
export const JazzAccount = co.account({
root: AccountRoot,
profile: co.map({ name: z.string() }),
}).withMigration((account, creationProps?: { name: string }) => {
if (account.root === undefined) {
const draft = DraftBubbleTeaOrder.create({});
account.root = AccountRoot.create({ draft });
}
});
// @filename: CreateOrder.tsx
export function OrderForm({
order,
onSave,
}: {
order: co.loaded | co.loaded;
onSave?: (e: React.FormEvent) => void;
}) {
return (
);
}
// ---cut---
// CreateOrder.tsx
export function CreateOrder() {
const { me } = useAccount(JazzAccount, { // [!code ++:5]
resolve: { root: { draft: true } },
});
if (!me?.root) return;
const onSave = (e: React.FormEvent) => {
e.preventDefault();
const draft = me.root.draft; // [!code ++:2]
if (!draft) return;
const validation = DraftBubbleTeaOrder.validate(draft);
if (validation.errors.length > 0) {
console.log(validation.errors);
return;
}
const order = draft as co.loaded;
console.log("Order created:", order);
// create a new empty draft
me.root.draft = DraftBubbleTeaOrder.create( // [!code ++:3]
{},
);
};
return
}
function CreateOrderForm({ // [!code ++:13]
id,
onSave,
}: {
id: string
onSave: (e: React.FormEvent) => void;
}) {
const draft = useCoState(DraftBubbleTeaOrder, id);
if (!draft) return;
return ;
}
```
When the new draft is created, we need to call `useCoState` again, so that we are passing the new draft to ``.
There you have it! Notice that when you refresh the page, you will see your unsaved changes.
## Draft indicator
To improve the UX even further, in just a few more steps, we can tell the user that they currently have unsaved changes.
Simply add a `hasChanges` helper to your schema.
```ts twoslash
// ---cut---
// schema.ts
export const DraftBubbleTeaOrder = co.map({
name: z.optional(z.string()),
}).withHelpers((Self) => ({
validate(draft: co.loaded) {
const errors: string[] = [];
if (!draft.name) {
errors.push("Plese enter a name.");
}
return { errors };
},
hasChanges(draft?: co.loaded) { // [!code ++:3]
return draft ? Object.keys(draft._edits).length : false;
},
}));
```
In the UI, you can choose how you want to show the draft indicator.
```tsx twoslash
export const BubbleTeaOrder = co.map({
name: z.string(),
});
export const DraftBubbleTeaOrder = co.map({
name: z.optional(z.string()),
}).withHelpers((Self) => ({
validate(draft: co.loaded) {
const errors: string[] = [];
if (!draft.name) {
errors.push("Plese enter a name.");
}
return { errors };
},
hasChanges(draft?: co.loaded) { // [!code ++:3]
return draft ? Object.keys(draft._edits).length : false;
},
}));
export const AccountRoot = co.map({
draft: DraftBubbleTeaOrder,
});
export const JazzAccount = co.account({
root: AccountRoot,
profile: co.map({ name: z.string() }),
}).withMigration((account, creationProps?: { name: string }) => {
if (account.root === undefined) {
const draft = DraftBubbleTeaOrder.create({});
account.root = AccountRoot.create({ draft });
}
});
// ---cut---
// DraftIndicator.tsx
export function DraftIndicator() {
const { me } = useAccount(JazzAccount, {
resolve: { root: { draft: true } },
});
if (DraftBubbleTeaOrder.hasChanges(me?.root.draft)) {
return (
You have a draft
);
}
}
```
A more subtle way is to show a small dot next to the Create button.
## 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, and boolean inputs.
[See the full example here.](https://github.com/garden-co/jazz/tree/main/examples/form)
```tsx twoslash
export const BubbleTeaAddOnTypes = [
"Pearl",
"Lychee jelly",
"Red bean",
"Brown sugar",
"Taro",
] as const;
export const ListOfBubbleTeaAddOns = co.list(
z.literal([...BubbleTeaAddOnTypes]),
);
// ---cut---
// schema.ts
export const BubbleTeaOrder = co.map({
baseTea: z.literal(["Black", "Oolong", "Jasmine", "Thai"]),
addOns: ListOfBubbleTeaAddOns,
deliveryDate: z.date(),
withMilk: z.boolean(),
instructions: z.optional(z.string()),
});
```
#### 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.
```ts twoslash
// ---cut---
// schema.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/schemas/covalues).
## 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.
```ts twoslash
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),
});
// ---cut---
// schema.ts
export const JazzAccountRoot = co.map({
organizations: co.list(Organization),
});
export const JazzAccount = co
.account({
root: JazzAccountRoot,
profile: co.profile({}),
})
.withMigration((account) => {
if (account.root === undefined) {
// 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.root = JazzAccountRoot.create({ 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/groups/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/groups/sharing#invites).
When the user accepts the invite, add the `Organization` to the user's `organizations` list.
```tsx twoslash
const Project = z.object({
name: z.string(),
});
const Organization = co.map({
name: z.string(),
projects: co.list(Project),
});
const JazzAccountRoot = co.map({
organizations: co.list(Organization),
});
const JazzAccount = co.account({
root: JazzAccountRoot,
profile: co.profile({}),
});
// ---cut---
export function AcceptInvitePage() {
const { me } = useAccount(JazzAccount, {
resolve: { root: { organizations: { $each: { $onError: null } } } },
});
const onAccept = (organizationId: string) => {
if (me) {
Organization.load(organizationId).then((organization) => {
if (organization) {
// avoid duplicates
const ids = me.root.organizations.map(
(organization) => organization?.id,
);
if (ids.includes(organizationId)) return;
me.root.organizations.push(organization);
}
});
}
};
useAcceptInvite({
invitedObjectSchema: Organization,
onAccept,
});
return
Accepting invite...
;
}
```
## Further reading
- [Allowing users to request an invite to join a Group](/docs/groups/sharing#requesting-invites)
- [Groups as permission scopes](/docs/groups/intro#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 twoslash
const me = await createJazzTestAccount();
const Task = co.map({
title: z.string(),
status: z.literal(["todo", "in-progress", "completed"]),
});
type Task = co.loaded;
const task = Task.create({ title: "New task", status: "todo" }, { owner: me });
// ---cut---
function getAuditLog(task: Task) {
const changes = [];
// Collect edits for all fields
const fields = Object.keys(task);
for (const field of fields) {
const editField = field as keyof typeof task._edits;
if (!task._edits[editField]) continue;
for (const edit of task._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) => {
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 twoslash
const me = await createJazzTestAccount();
const Project = co.map({
name: z.string(),
status: z.literal(["todo", "in-progress", "completed"]),
});
type Project = co.loaded;
const project = Project.create({ name: "New project", status: "todo" }, { owner: me });
const myProjects = [project];
// ---cut---
function getRecentActivity(projects: Project[], since: Date) {
const activity = [];
for (const project of projects) {
// Get all fields that might have edits
const fields = Object.keys(project);
// Check each field for edit history
for (const field of fields) {
const editField = field as keyof typeof project._edits;
// Skip if no edits exist for this field
if (!project._edits[editField]) continue;
for (const edit of project._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 twoslash
const me = await createJazzTestAccount();
const Task = co.map({
title: z.string(),
status: z.literal(["todo", "in-progress", "completed"]),
});
type Task = co.loaded;
const task = Task.create({ title: "New task", status: "todo" }, { owner: me });
// ---cut---
function getLastUpdated(task: Task) {
// Find the most recent edit across all fields
let lastEdit: any = null;
for (const field of Object.keys(task)) {
const editField = field as keyof typeof task._edits;
// Skip if no edits exist for this field
if (!task._edits[editField]) continue;
const fieldEdit = task._edits[editField];
if (fieldEdit && (!lastEdit || fieldEdit.madeAt > lastEdit.madeAt)) {
lastEdit = fieldEdit;
}
}
if (!lastEdit) 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 twoslash
const me = await createJazzTestAccount();
const Task = co.map({
title: z.string(),
status: z.literal(["todo", "in-progress", "completed"]),
});
type Task = co.loaded;
const task = Task.create({ title: "New task", status: "todo" }, { owner: me });
task.status = "completed";
task.status = "in-progress";
task.status = "completed";
// ---cut---
// Find when a task was completed
function findCompletionTime(task: Task): Date | null {
if (!task._edits.status) 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 = task._edits.status.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 {
if (!task._edits.status) return null;
// Reverse and find (stops at first match)
const latestCompletionEdit = task._edits.status.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 edits = task._edits[field as keyof typeof task._edits];
if (!edits) return null;
const matchingEdit = edits.all.find(edit => edit.value === value);
return matchingEdit?.by || null;
}
const account = findWhoChanged(task, "status", "completed");
console.log(account?.profile?.name);
// Alice
```
## Further Reading
- [History](/docs/using-covalues/history) - Complete reference for the history API
- [Subscription & Loading](/docs/using-covalues/subscription-and-loading) - Ensure CoValues are loaded before accessing history
### Resources
#### 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
## 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
## Resources
- [Documentation](https://jazz.tools/docs): Detailed documentation about Jazz
- [Examples](https://jazz.tools/examples): Code examples and tutorials
## Music Example
### /vercel/path0/examples/music-player/src/1_schema.ts
```ts
import { co, 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()),
/**
* You can use getters for recusrive relations
*/
get sourceTrack() {
return MusicTrack.optional();
},
});
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;
/** 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: z.optional(MusicTrack),
activePlaylist: Playlist,
exampleDataLoaded: z.optional(z.boolean()),
});
export type MusicaAccountRoot = co.loaded;
export const MusicaAccount = co
.account({
/** the default user profile with a name */
profile: co.profile(),
root: MusicaAccountRoot,
})
.withMigration((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.root === undefined) {
const tracks = co.list(MusicTrack).create([]);
const rootPlaylist = Playlist.create({
tracks,
title: "",
});
account.root = MusicaAccountRoot.create({
rootPlaylist,
playlists: co.list(Playlist).create([]),
activeTrack: undefined,
activePlaylist: rootPlaylist,
exampleDataLoaded: false,
});
}
});
export type MusicaAccount = co.loaded;
/** Walkthrough: Continue with ./2_main.tsx */
```
### /vercel/path0/examples/music-player/src/2_main.tsx
```ts
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 "./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 { useUploadExampleData } from "./lib/useUploadExampleData";
/**
* 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 Main() {
const mediaPlayer = useMediaPlayer();
useUploadExampleData();
const router = createHashRouter([
{
path: "/",
element: ,
},
{
path: "/playlist/:playlistId",
element: ,
},
{
path: "/invite/*",
element: ,
},
]);
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(
,
);
```
### /vercel/path0/examples/music-player/src/3_HomePage.tsx
```ts
import { useToast } from "@/hooks/use-toast";
import {
createInviteLink,
useAccount,
useCoState,
useIsAuthenticated,
} from "jazz-tools/react";
import { useParams } from "react-router";
import { MusicaAccount, Playlist } from "./1_schema";
import { uploadMusicTracks } from "./4_actions";
import { MediaPlayer } from "./5_useMediaPlayer";
import { FileUploadButton } from "./components/FileUploadButton";
import { MusicTrackRow } from "./components/MusicTrackRow";
import { PlaylistTitleInput } from "./components/PlaylistTitleInput";
import { SidePanel } from "./components/SidePanel";
import { Button } from "./components/ui/button";
import { SidebarInset, SidebarTrigger } from "./components/ui/sidebar";
import { usePlayState } from "./lib/audio/usePlayState";
export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
/**
* `me` represents the current user account, which will determine
* access rights to CoValues. We get it from the top-level provider ``.
*/
const { me } = useAccount(MusicaAccount, {
resolve: { root: { rootPlaylist: true, playlists: true } },
});
const playState = usePlayState();
const isPlaying = playState.value === "play";
const { toast } = useToast();
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 = params.playlistId ?? me?.root._refs.rootPlaylist.id;
const playlist = useCoState(Playlist, playlistId, {
resolve: { tracks: true },
});
const isRootPlaylist = !params.playlistId;
const isPlaylistOwner = playlist?._owner.myRole() === "admin";
const isActivePlaylist = playlistId === me?.root.activePlaylist?.id;
const handlePlaylistShareClick = async () => {
if (!isPlaylistOwner) return;
const inviteLink = createInviteLink(playlist, "reader");
await navigator.clipboard.writeText(inviteLink);
toast({
title: "Invite link copied into the clipboard",
});
};
const isAuthenticated = useIsAuthenticated();
return (
);
}
```
### /vercel/path0/examples/music-player/src/4_actions.ts
```ts
import { getAudioFileData } from "@/lib/audio/getAudioFileData";
import { FileStream, Group, co } from "jazz-tools";
import {
MusicTrack,
MusicTrackWaveform,
MusicaAccount,
Playlist,
} from "./1_schema";
/**
* Walkthrough: Actions
*
* With Jazz is very simple to update the state, you
* just mutate the values and we take care of triggering
* the updates and sync and persist the values you change.
*
* We have grouped the complex updates here in an actions file
* just to keep them separated from the components.
*
* Jazz is very unopinionated in this sense and you can adopt the
* pattern that best fits your app.
*/
export async function uploadMusicTracks(
files: Iterable,
isExampleTrack: boolean = false,
) {
const { root } = await MusicaAccount.getMe().ensureLoaded({
resolve: {
root: {
rootPlaylist: {
tracks: true,
},
},
},
});
for (const file of files) {
// The ownership object defines the user that owns the created coValues
// We are creating a group for each CoValue in order to be able to share them via Playlist
const group = Group.create();
const data = await getAudioFileData(file);
// We transform the file blob into a FileStream
// making it a collaborative value that is encrypted, easy
// to share across devices and users and available offline!
const fileStream = await FileStream.createFromBlob(file, group);
const musicTrack = MusicTrack.create(
{
file: fileStream,
duration: data.duration,
waveform: MusicTrackWaveform.create({ data: data.waveform }, group),
title: file.name,
isExampleTrack,
},
group,
);
// The newly created musicTrack can be associated to the
// user track list using a simple push call
root.rootPlaylist.tracks.push(musicTrack);
}
}
export async function createNewPlaylist() {
const { root } = await MusicaAccount.getMe().ensureLoaded({
resolve: {
root: {
playlists: true,
},
},
});
// Since playlists are meant to be shared we associate them
// to a group which will contain the keys required to get
// access to the "owned" values
const playlistGroup = Group.create();
const playlist = Playlist.create(
{
title: "New Playlist",
tracks: co.list(MusicTrack).create([], playlistGroup),
},
playlistGroup,
);
// Again, we associate the new playlist to the
// user by pushing it into the playlists CoList
root.playlists.push(playlist);
return playlist;
}
export async function addTrackToPlaylist(
playlist: Playlist,
track: MusicTrack,
) {
const alreadyAdded = playlist.tracks?.some(
(t) => t?.id === track.id || t?._refs.sourceTrack?.id === track.id,
);
if (alreadyAdded) return;
// Check if the track has been created after the Group inheritance was introduced
if (track._owner._type === "Group" && playlist._owner._type === "Group") {
/**
* Extending the track with the Playlist group in order to make the music track
* visible to the Playlist user
*/
const trackGroup = track._owner;
trackGroup.addMember(playlist._owner);
playlist.tracks?.push(track);
return;
}
}
export async function removeTrackFromPlaylist(
playlist: Playlist,
track: MusicTrack,
) {
const notAdded = !playlist.tracks?.some(
(t) => t?.id === track.id || t?._refs.sourceTrack?.id === track.id,
);
if (notAdded) return;
if (track._owner._type === "Group" && playlist._owner._type === "Group") {
const trackGroup = track._owner;
await trackGroup.removeMember(playlist._owner);
const index =
playlist.tracks?.findIndex(
(t) => t?.id === track.id || t?._refs.sourceTrack?.id === track.id,
) ?? -1;
if (index > -1) {
playlist.tracks?.splice(index, 1);
}
return;
}
}
export async function updatePlaylistTitle(playlist: Playlist, title: string) {
playlist.title = title;
}
export async function updateMusicTrackTitle(track: MusicTrack, title: string) {
track.title = title;
}
export async function updateActivePlaylist(playlist?: Playlist) {
const { root } = await MusicaAccount.getMe().ensureLoaded({
resolve: {
root: {
activePlaylist: true,
rootPlaylist: true,
},
},
});
root.activePlaylist = playlist ?? root.rootPlaylist;
}
export async function updateActiveTrack(track: MusicTrack) {
const { root } = await MusicaAccount.getMe().ensureLoaded({
resolve: {
root: {},
},
});
root.activeTrack = track;
}
export async function onAnonymousAccountDiscarded(
anonymousAccount: MusicaAccount,
) {
const { root: anonymousAccountRoot } = await anonymousAccount.ensureLoaded({
resolve: {
root: {
rootPlaylist: {
tracks: {
$each: true,
},
},
},
},
});
const me = await MusicaAccount.getMe().ensureLoaded({
resolve: {
root: {
rootPlaylist: {
tracks: true,
},
},
},
});
for (const track of anonymousAccountRoot.rootPlaylist.tracks) {
if (track.isExampleTrack) continue;
const trackGroup = track._owner.castAs(Group);
trackGroup.addMember(me, "admin");
me.root.rootPlaylist.tracks.push(track);
}
}
export async function deletePlaylist(playlistId: string) {
const { root } = await MusicaAccount.getMe().ensureLoaded({
resolve: {
root: {
playlists: true,
},
},
});
const index = root.playlists.findIndex((p) => p?.id === playlistId);
if (index > -1) {
root.playlists.splice(index, 1);
}
}
```
### /vercel/path0/examples/music-player/src/5_useMediaPlayer.ts
```ts
import { MusicTrack, MusicaAccount, Playlist } from "@/1_schema";
import { usePlayMedia } from "@/lib/audio/usePlayMedia";
import { usePlayState } from "@/lib/audio/usePlayState";
import { FileStream } from "jazz-tools";
import { useAccount } from "jazz-tools/react";
import { useRef, useState } from "react";
import { updateActivePlaylist, updateActiveTrack } from "./4_actions";
import { getNextTrack, getPrevTrack } from "./lib/getters";
export function useMediaPlayer() {
const { me } = useAccount(MusicaAccount, {
resolve: { root: true },
});
const playState = usePlayState();
const playMedia = usePlayMedia();
const [loading, setLoading] = useState(null);
const activeTrackId = me?.root._refs.activeTrack?.id;
// Reference used to avoid out-of-order track loads
const lastLoadedTrackId = useRef(null);
async function loadTrack(track: MusicTrack) {
lastLoadedTrackId.current = track.id;
setLoading(track.id);
const file = await FileStream.loadAsBlob(track._refs.file!.id); // TODO: see if we can avoid !
if (!file) {
setLoading(null);
return;
}
// Check if another track has been loaded during
// the file download
if (lastLoadedTrackId.current !== track.id) {
return;
}
updateActiveTrack(track);
await playMedia(file);
setLoading(null);
}
async function playNextTrack() {
const track = await getNextTrack();
if (track) {
updateActiveTrack(track);
await loadTrack(track);
}
}
async function playPrevTrack() {
const track = await getPrevTrack();
if (track) {
await loadTrack(track);
}
}
async function setActiveTrack(track: MusicTrack, playlist?: Playlist) {
if (activeTrackId === track.id && lastLoadedTrackId.current !== null) {
playState.toggle();
return;
}
updateActivePlaylist(playlist);
await loadTrack(track);
if (playState.value === "pause") {
playState.toggle();
}
}
return {
activeTrackId,
setActiveTrack,
playNextTrack,
playPrevTrack,
loading,
};
}
export type MediaPlayer = ReturnType;
```
### /vercel/path0/examples/music-player/src/6_InvitePage.tsx
```ts
import { useAcceptInvite, useIsAuthenticated } from "jazz-tools/react";
import { useCallback } from "react";
import { useNavigate } from "react-router-dom";
import { MusicaAccount, Playlist } from "./1_schema";
export function InvitePage() {
const navigate = useNavigate();
const isAuthenticated = useIsAuthenticated();
useAcceptInvite({
invitedObjectSchema: Playlist,
onAccept: useCallback(
async (playlistId: string) => {
const playlist = await Playlist.load(playlistId, {});
const me = await MusicaAccount.getMe().ensureLoaded({
resolve: {
root: {
playlists: true,
},
},
});
if (
playlist &&
!me.root.playlists.some((item) => playlist.id === item?.id)
) {
me.root.playlists.push(playlist);
}
navigate("/playlist/" + playlistId);
},
[navigate],
),
});
return isAuthenticated ? (
Go to all tracksPlaylistsAdd a new playlist
{me?.root.playlists.map((playlist) => (
handlePlaylistClick(playlist.id)}
isActive={playlist.id === playlistId}
>