# Jazz
## Documentation
### Getting started
#### Introduction
# Learn some Jazz
Welcome to the Jazz documentation!
**Note:** We just released [Jazz 0.14.0](/docs/upgrade/0-14-0) with a bunch of breaking changes and are still cleaning the docs up - see the [upgrade guide](/docs/upgrade/0-14-0) for details.
## 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
```
Or set up Jazz yourself, using the following instructions for your framework of choice:
- [React](/docs/react/project-setup)
- [Next.js](/docs/react/project-setup#nextjs)
- [React Native](/docs/react-native/project-setup)
- [React Native Expo](/docs/react-native-expo/project-setup)
- [Vue](/docs/vue/project-setup)
- [Svelte](/docs/svelte/project-setup)
{/*
Or you can follow this [React step-by-step guide](/docs/react/guide) where we walk you through building an issue tracker app.
*/}
## Example apps
You can also find [example apps](/examples) with code most similar to what you want to build. These apps
make use of different features such as auth, file upload, and more.
## Sync and storage
Sync and persist your data by setting up a [sync and storage infrastructure](/docs/sync-and-storage) using Jazz Cloud, or do it yourself.
## Collaborative values
Learn how to structure your data using [collaborative values](/docs/schemas/covalues).
## LLM Docs
Get better results with AI by [importing the Jazz docs](/docs/ai-tools) into your context window.
## Get support
If you have any questions or need assistance, please don't hesitate to reach out to us on [Discord](https://discord.gg/utDMjHYg42).
We would 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.
### Project setup
#### Installation
### react-native-expo Implementation
# React Native (Expo) Installation and Setup
Jazz supports Expo through the dedicated `jazz-expo` package, 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-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 @craftzdog/react-native-buffer
# Jazz dependencies
npm i -S jazz-tools jazz-expo jazz-react-native-media-images
```
**Note**: 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`, `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).
## 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 Chat Demo](https://github.com/garden-co/jazz/tree/main/examples/chat-rn-expo-clerk) 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 @craftzdog/react-native-buffer @op-engineering/op-sqlite react-native-mmkv
# Jazz dependencies
npm i -S jazz-tools jazz-react-native jazz-react-native-media-images
```
**Note**: 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("Buffer", () => Buffer); // polyfill Buffer
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
# 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-react jazz-tools
```
## 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
### Client-side Only (Easiest)
The simplest approach for Next.js is client-side only integration:
```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
"use client" // Mark as client component
export function JazzWrapper({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
Remember to mark any component that uses Jazz hooks with `"use client"`:
```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: Profile.tsx
// ---cut---
// Profile.tsx
"use client"; // [!code ++]
export function Profile() {
const { me } = useAccount(MyAppAccount, { resolve: { profile: true } });
return
Hello, {me?.profile.name}
;
}
```
### SSR Support (Experimental)
For server-side rendering, Jazz offers experimental approaches:
- Pure SSR
- Hybrid SSR + Client Hydration
#### Pure SSR
Use Jazz in server components by directly loading data with `CoValue.load()`.
{/*
```tsx twoslash
// @errors: 18047
// @filename: schema.ts
export class MyItem extends CoMap {
title = co.string;
}
export class MyCollection extends CoList.Of(co.ref(MyItem)) {}
// @filename: PublicData.tsx
const collectionID = "co_z123" as ID;
// ---cut---
// Server Component (no "use client" directive)
export default async function PublicData() {
// Load data directly in the server component
const items = await MyCollection.load(collectionID);
if (!items) {
return
Loading...
;
}
return (
{items.map(item => (
item ?
{item.title}
: null
))}
);
}
```
*/}
This works well for public data accessible to the server account.
#### Hybrid SSR + Client Hydration
For more complex cases, you can pre-render on the server and hydrate on the client:
1. Create a shared rendering component.
{/*
```tsx twoslash
// @filename: schema.ts
export class MyItem extends CoMap {
title = co.string;
}
// @filename: ItemList.tsx
// ---cut---
// ItemList.tsx - works in both server and client contexts
export function ItemList({ items }: { items: MyItem[] }) {
return (
{items.map(item => (
{item.title}
))}
);
}
```
*/}
2. Create a client hydration component.
{/*
```tsx twoslash
// @filename: schema.ts
export class MyItem extends CoMap {
title = co.string;
}
export class MyCollection extends CoList.Of(co.ref(MyItem)) {}
// @filename: ItemList.tsx
export function ItemList({ items }: { items: MyItem[] }) {
return (
{items.map(item => (
{item.title}
))}
);
}
// @filename: ItemListHydrator.tsx
// ItemListHydrator.tsx
const myCollectionID = "co_z123" as ID;
// ---cut---
"use client"
export function ItemListHydrator({ initialItems }: { initialItems: MyItem[] }) {
// Hydrate with real-time data once client loads
const myCollection = useCoState(MyCollection, myCollectionID);
// Filter out nulls for type safety
const items = Array.from(myCollection?.values() || []).filter(
(item): item is MyItem => !!item
);
// Use server data until client data is available
const displayItems = items || initialItems;
return ;
}
```
*/}
3. Create a server component that pre-loads data.
{/*
```tsx twoslash
// @filename: schema.ts
export class MyItem extends CoMap {
title = co.string;
}
export class MyCollection extends CoList.Of(co.ref(MyItem)) {}
// @filename: ItemList.tsx
export function ItemList({ items }: { items: MyItem[] }) {
return (
{items.map(item => (
{item.title}
))}
);
}
// @filename: ItemListHydrator.tsx
// ItemListHydrator.tsx
const myCollectionID = "co_z123" as ID;
// ---cut---
"use client"
export function ItemListHydrator({ initialItems }: { initialItems: MyItem[] }) {
// Hydrate with real-time data once client loads
const myCollection = useCoState(MyCollection, myCollectionID);
// Filter out nulls for type safety
const items = Array.from(myCollection?.values() || []).filter(
(item): item is MyItem => !!item
);
// Use server data until client data is available
const displayItems = items || initialItems;
return ;
}
// @filename: ServerItemPage.tsx
const myCollectionID = "co_z123" as ID;
// ---cut---
// ServerItemPage.tsx
export default async function ServerItemPage() {
// Pre-load data on the server
const initialItems = await MyCollection.load(myCollectionID);
// Filter out nulls for type safety
const items = Array.from(initialItems?.values() || []).filter(
(item): item is MyItem => !!item
);
// Pass to client hydrator
return ;
}
```
*/}
This approach gives you the best of both worlds: fast initial loading with server rendering, plus real-time updates on the client.
## 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)
## 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
class MyWorkerAccount extends Account {}
// ---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
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:
1. Install Jazz dependencies
```sh
pnpm install jazz-tools jazz-svelte
```
2. 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);
// ...
}
```
3. Set up the Provider in your root layout
```svelte
```
4. 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.
---
### vue Implementation
# VueJS demo todo app guide
This guide provides step-by-step instructions for setting up and running a Jazz-powered Todo application using VueJS.
See the full example [here](https://github.com/garden-co/jazz/tree/main/examples/todo-vue).
---
## Setup
### Create a new app
Run the following command to create a new VueJS application:
```bash
❯ pnpm create vue@latest
✔ Project name: … vue-setup-guide
✔ Add TypeScript? … Yes
✔ Add JSX Support? … No
✔ Add Vue Router for Single Page Application development? … Yes
✔ Add Pinia for state management? … No
✔ Add Vitest for Unit Testing? … No
✔ Add an End-to-End Testing Solution? › No
✔ Add ESLint for code quality? › Yes
✔ Add Prettier for code formatting? … Yes
```
### Install dependencies
Run the following command to install Jazz libraries:
```bash
pnpm install jazz-tools jazz-browser jazz-vue
```
### Implement `schema.ts`
Define the schema for your application.
Example schema inside `src/schema.ts` for a todo app:
```typescript
export class ToDoItem extends CoMap {
name = coField.string;
completed = coField.boolean;
}
export class ToDoList extends CoList.Of(coField.ref(ToDoItem)) {}
export class Folder extends CoMap {
name = coField.string;
items = coField.ref(ToDoList);
}
export class FolderList extends CoList.Of(coField.ref(Folder)) {}
export class ToDoAccountRoot extends CoMap {
folders = coField.ref(FolderList);
}
export class ToDoAccount extends Account {
profile = coField.ref(Profile);
root = coField.ref(ToDoAccountRoot);
migrate() {
if (!this._refs.root) {
const group = Group.create({ owner: this });
const firstFolder = Folder.create(
{
name: "Default",
items: ToDoList.create([], { owner: group }),
},
{ owner: group },
);
this.root = ToDoAccountRoot.create(
{
folders: FolderList.create([firstFolder], {
owner: this,
}),
},
{ owner: this },
);
}
}
}
```
### Refactor `main.ts`
Update the `src/main.ts` file to integrate Jazz:
```typescript
declare module "jazz-vue" {
interface Register {
Account: ToDoAccount;
}
}
const RootComponent = defineComponent({
name: "RootComponent",
setup() {
return () => [
h(
JazzProvider,
{
AccountSchema: ToDoAccount,
auth: authMethod.value,
peer: "wss://cloud.jazz.tools/?key=vue-todo-example-jazz@garden.co",
},
{
default: () => h(App),
},
),
];
},
});
const app = createApp(RootComponent);
app.use(router);
app.mount("#app");
```
### Set up `router/index.ts`:
Create a basic Vue router configuration. For example:
```typescript
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/",
name: "Home",
component: HomeView,
},
],
});
export default router;
```
### Implement `App.vue`
Update the `App.vue` file to include logout functionality:
```typescript
Todo App
{{ me.profile?.name }}
```
## Subscribing to a CoValue
Subscribe to a CoValue inside `src/views/HomeView.vue`:
```typescript
{@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, Vue)
- `-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
- `vue-demo-auth` - Vue with Demo 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 14.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.
For now, you can get your account credentials from the `jazz-logged-in-secret` local storage key from within your Jazz app.
[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]
Alternatively, you can embed the Inspector directly into your app, so you don't need to open a separate window.
Install the package.
```sh
npm install jazz-inspector
```
Render the component within your `JazzProvider`.
```tsx
// [!code ++]
```
```sh
npm install jazz-inspector-element
```
Render the component.
```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/src/routes/%2Blayout.svelte) for a full example.
### Upgrade guides
#### 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.
**Note: This is a huge release that we're still cleaning up and documenting.**
We're still in the process of:
- updating all our docs
- double-checking all our framework bindings
- completing all the details of this upgrade guide
**Note: React Native is currently broken based on an [underlying Zod v4 issue](https://github.com/colinhacks/zod/issues/4148).**
If you see something broken, please let us know on [Discord](https://discord.gg/utDMjHYg42) and check back in a couple hours.
Thanks for your patience!
## 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 twoslash
// 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 twoslash
// ---cut---
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 twoslash
// ---cut---
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 twoslash filename="schema.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 twoslash filename="app.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 twoslash
{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.
When using `useAccount` you should now pass the `Account` schema directly:
```tsx twoslash
// @filename: schema.ts
export const MyAccount = co.account({
profile: co.profile(),
root: co.map({})
});
// @filename: app.tsx
// ---cut---
function MyComponent() {
const { me } = useAccount(MyAccount, {
resolve: {
profile: true,
},
});
return
{me?.profile.name}
;
}
```
When using `AccountCoState` you should now pass the `Account` schema directly:
```svelte twoslash filename="app.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 twoslash
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
TODO
## Minor breaking changes
### `_refs` and `_edits` are now potentially null
The type of `_refs` and `_edits` is now nullable.
```ts twoslash
// ---cut---
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 twoslash
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,
});
// ---cut---
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([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.null(); // For null
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
You can use the `withHelpers` method on CoValue schemas to add helper functions to the schema itself.
These typically take a parameter of a loaded CoValue of the schema.
```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(),
}).withHelpers((Self) => ({
fullName(person: Loaded) {
return `${person.firstName} ${person.lastName}`;
},
ageAsOf(person: Loaded, date: Date) {
return differenceInYears(date, person.dateOfBirth);
}
}));
const person = Person.create({
firstName: "John",
lastName: "Doe",
dateOfBirth: new Date("1990-01-01"),
});
const fullName = Person.fullName(person);
const age = Person.ageAsOf(person, new Date());
```
#### 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: Loaded }> {};
class ContactPreview extends React.Component<{ contact: 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.addMember("everyone", "reader");
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),
});
```
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
}
```
### 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
```
### 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
```
## 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 can add helper methods to your CoMap schema to make it more useful:
```ts twoslash
const me = await createJazzTestAccount();
// ---cut---
const Project = co.map({
name: z.string(),
startDate: z.date(),
endDate: z.optional(z.date()),
}).withHelpers((Self) => ({
isActive(project: Loaded) {
const now = new Date();
return now >= project.startDate && (!project.endDate || now <= project.endDate);
},
formatDuration(project: Loaded, 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(Project.isActive(project)); // false
console.log(Project.formatDuration(project, "short")); // "3 days"
```
#### 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());
const ListOfTasks = co.list(Task);
```
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
```
### 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: 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()),
});
// 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 [jazz-richtext-prosemirror](#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), 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. This plugin, [`jazz-richtext-prosemirror`](https://www.npmjs.com/package/jazz-richtext-prosemirror), 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 jazz-richtext-prosemirror \
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, though you'll need to import from the appropriate package (`jazz-expo` or `jazz-react-native`).
Jazz offers several tools to work with images in React Native:
- [`createImage()`](#creating-images) - function to create an `ImageDefinition` from a base64 image data URI
- [`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
For examples of use, see our example apps:
- [React Native Chat](https://github.com/gardencmp/jazz/tree/main/examples/chat-rn) (Framework-less implementation)
- [React Native Expo Chat](https://github.com/gardencmp/jazz/tree/main/examples/chat-rn-expo) (Expo implementation)
- [React Native Expo Clerk Chat](https://github.com/gardencmp/jazz/tree/main/examples/chat-rn-expo-clerk) (Expo implementation with Clerk)
## Creating Images
The easiest way to create and use images in your Jazz application is with the `createImage()` 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 createImage(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 `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:
```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 createImage(base64Uri, options);
```
## Displaying Images with `ProgressiveImg`
For a complete progressive loading experience, use the `ProgressiveImg` component:
```tsx
function GalleryView({ image }) {
return (
{({ src }) => (
)}
);
}
const styles = StyleSheet.create({
galleryImage: {
width: '100%',
height: 200,
borderRadius: 8,
}
});
```
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
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
} = 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 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. The functionality described here is identical regardless of which implementation you're using, though you'll need to import from the appropriate package (`jazz-expo` or `jazz-react-native`).
Jazz offers several tools to work with images in React Native:
- [`createImage()`](#creating-images) - function to create an `ImageDefinition` from a base64 image data URI
- [`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
For examples of use, see our example apps:
- [React Native Chat](https://github.com/gardencmp/jazz/tree/main/examples/chat-rn) (Framework-less implementation)
- [React Native Expo Chat](https://github.com/gardencmp/jazz/tree/main/examples/chat-rn-expo) (Expo implementation)
- [React Native Expo Clerk Chat](https://github.com/gardencmp/jazz/tree/main/examples/chat-rn-expo-clerk) (Expo implementation with Clerk)
## Creating Images
The easiest way to create and use images in your Jazz application is with the `createImage()` 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 createImage(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 `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:
```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 createImage(base64Uri, options);
```
## Displaying Images with `ProgressiveImg`
For a complete progressive loading experience, use the `ProgressiveImg` component:
```tsx
function GalleryView({ image }) {
return (
{({ src }) => (
)}
);
}
const styles = StyleSheet.create({
galleryImage: {
width: '100%',
height: 200,
borderRadius: 8,
}
});
```
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
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
} = 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 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.addMember("everyone", "reader");
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: 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: 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
```
#### 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: 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 Loaded Type
Jazz provides the `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 `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 = 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 = 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 = 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 = 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 `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 Loaded type**: Add compile-time type safety for components that require specific resolution patterns
### 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" as ID;
// ---cut---
const group = Group.create();
const bob = await 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" as ID;
const group = Group.create();
// ---cut---
const bob = await Account.load(bobsID as ID);
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 `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 = 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 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");
```
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 [pet example](https://github.com/garden-co/jazz/tree/main/examples/pets)
and 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/pets/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
},
});
```
### 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: 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: Account,
status: z.literal(["pending", "approved", "rejected"]),
});
const RequestsList = co.list(JoinRequest);
// ---cut-before---
function createRequestsToJoin() {
const requestsGroup = Group.create();
requestsGroup.addMember("everyone", "writeOnly");
return RequestsList.create([], requestsGroup);
}
async function sendJoinRequest(
requestsList: 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 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: 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;
}
}
```
#### Group inheritance
# Group Inheritance
Groups can inherit members from other groups using the `extend` method.
When a group extends another group, members of the parent group will become automatically part of the child group.
## Basic Usage
Here's how to extend a group:
```ts twoslash
// ---cut---
const playlistGroup = Group.create();
const trackGroup = Group.create();
// This way track becomes visible to the members of playlist
trackGroup.extend(playlistGroup);
```
When you extend a group:
- Members of the parent group get access to the child group
- Their roles are inherited (with some exceptions, see [below](#role-inheritance-rules))
- Removing a member from the parent group also removes their access to child groups
## Inheriting members but overriding their role
In some cases you might want to inherit all members from a parent group but override/flatten their roles to the same specific role in the child group. You can do so by passing an "override role" as a second argument to `extend`:
```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.extend(organizationGroup, "reader");
```
The "override role" works in both directions:
```ts twoslash
const bob = await createJazzTestAccount();
const alice = await createJazzTestAccount();
// ---cut---
const parentGroup = Group.create();
parentGroup.addMember(bob, "reader");
parentGroup.addMember(alice, "admin");
const childGroup = Group.create();
childGroup.extend(parentGroup, "writer");
// Bob and Alice are now writers in the child group
```
## Multiple Levels of Inheritance
Groups can be extended multiple levels deep:
```ts twoslash
// ---cut---
const grandParentGroup = Group.create();
const parentGroup = Group.create();
const childGroup = Group.create();
childGroup.extend(parentGroup);
parentGroup.extend(grandParentGroup);
```
Members of the grandparent group will get access to all descendant groups based on their roles.
## Permission Changes
When you remove a member from a parent group, they automatically lose access to all child groups. We handle key rotation automatically to ensure security.
```ts twoslash
const bob = await createJazzTestAccount();
const parentGroup = Group.create();
// ---cut---
// Remove member from parent
await parentGroup.removeMember(bob);
// Bob loses access to both parent and child groups
```
## Role Inheritance Rules
If the account is already a member of the child group, it will get the more permissive role:
```ts twoslash
const bob = await createJazzTestAccount();
// ---cut---
const parentGroup = Group.create();
parentGroup.addMember(bob, "reader");
const childGroup = Group.create();
parentGroup.addMember(bob, "writer");
childGroup.extend(parentGroup);
// Bob stays a writer because his role is higher
// than the inherited reader role.
```
When extending groups, only admin, writer and reader roles are inherited:
```ts twoslash
const bob = await createJazzTestAccount();
// ---cut---
const parentGroup = Group.create();
parentGroup.addMember(bob, "writeOnly");
const childGroup = Group.create();
childGroup.extend(parentGroup);
// Bob does not become a member of the child group
```
To extend a group:
1. The current account must be an admin in the child group
2. The current account must be a member of the parent group
```ts twoslash
const Company = co.map({
name: z.string(),
});
const company = Company.create({ name: "Garden Computing" });
// ---cut---
const companyGroup = company._owner.castAs(Group)
const teamGroup = Group.create();
// Works only if I'm a member of companyGroup
teamGroup.extend(companyGroup);
```
## Revoking a group extension
You can revoke a group extension by using the `revokeExtend` method:
```ts twoslash
// ---cut---
const parentGroup = Group.create();
const childGroup = Group.create();
childGroup.extend(parentGroup);
// Revoke the extension
await childGroup.revokeExtend(parentGroup);
```
## Getting all parent groups
You can get all the parent groups of a group by calling the `getParentGroups` method:
```ts twoslash
// ---cut---
const childGroup = Group.create();
const parentGroup = Group.create();
childGroup.extend(parentGroup);
console.log(childGroup.getParentGroups()); // [parentGroup]
```
## 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.extend(companyGroup); // Inherits company-wide access
teamGroup.addMember(teamLead, "admin");
teamGroup.addMember(developer, "writer");
// Project group with specific permissions
const projectGroup = Group.create();
projectGroup.extend(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
- [**Jazz Cloud Integration**](./jazz-cloud): Authentication service provided through Jazz Cloud
- [**Self-Hosting**](./self-hosting): Self-hosted authentication service
#### 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 `useAccountOrGuest` and `useIsAuthenticated`.
```tsx twoslash
// ---cut---
function AuthStateIndicator() {
const { me } = useAccountOrGuest();
const isAuthenticated = useIsAuthenticated();
// Check if guest mode is enabled in JazzProvider
const isGuest = me._type !== "Account"
// Anonymous authentication: has an account but not fully authenticated
const isAnonymous = me._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
class MusicTrack extends CoMap {
title = coField.string;
duration = coField.number;
isExampleTrack = coField.optional.boolean;
}
class ListOfTracks extends CoList.Of(coField.ref(MusicTrack)) {}
class Playlist extends CoMap {
title = coField.string;
tracks = coField.ref(ListOfTracks);
}
class MusicaAccountRoot extends CoMap {
rootPlaylist = coField.ref(Playlist);
}
class MusicaAccount extends Account {
root = coField.ref(MusicaAccountRoot);
}
// ---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 [JazzProvider](/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), which you can see in [this example](https://github.com/garden-co/jazz/tree/main/examples/password-manager).
## 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: Loaded | 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: Loaded | 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: Loaded | 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 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: 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: 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: Loaded | 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 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: 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: Loaded | 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 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: Loaded) {
const errors: string[] = [];
if (!draft.name) {
errors.push("Plese enter a name.");
}
return { errors };
},
hasChanges(draft?: 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: Loaded) {
const errors: string[] = [];
if (!draft.name) {
errors.push("Plese enter a name.");
}
return { errors };
},
hasChanges(draft?: 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)
## 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(): z.ZodOptional {
return z.optional(MusicTrack);
},
});
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-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 { PlayerControls } from "./components/PlayerControls";
import "./index.css";
import { MusicaAccount } from "@/1_schema";
import { apiKey } from "@/apiKey.ts";
import { JazzProvider } from "jazz-react";
import { onAnonymousAccountDiscarded } from "./4_actions";
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-react";
import { useNavigate, useParams } from "react-router";
import { MusicaAccount, Playlist } from "./1_schema";
import { createNewPlaylist, uploadMusicTracks } from "./4_actions";
import { MediaPlayer } from "./5_useMediaPlayer";
import { AuthButton } from "./components/AuthButton";
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 { 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 navigate = useNavigate();
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);
}
async function handleCreatePlaylist() {
const playlist = await createNewPlaylist();
navigate(`/playlist/${playlist.id}`);
}
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.extend(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.revokeExtend(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 { useAccount } from "jazz-react";
import { FileStream } from "jazz-tools";
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-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 ? (