# Jazz (react-native-expo) ## Getting started ### Overview # Learn some Jazz **Jazz is a new kind of database** that's **distributed** across your frontend, containers, serverless functions and its own storage cloud. It syncs structured data, files and LLM streams instantly, and looks like local reactive JSON state. It also provides auth, orgs & teams, real-time multiplayer, edit histories, permissions, E2E encryption and offline-support out of the box. --- ## Quickstart **Want to learn the basics?** Check out our [quickstart guide](/docs/quickstart) for a step-by-step guide to building a simple app with Jazz. **Just want to get started?** You can use [create-jazz-app](/docs/tooling-and-resources/create-jazz-app) to create a new Jazz project from one of our starter templates or example apps: ```sh npx create-jazz-app@latest --api-key you@example.com ``` **Using an LLM?** [Add our llms.txt](/react-native-expo/llms-full.txt) to your context window! **Info:** Requires at least Node.js v20\. See our [Troubleshooting Guide](/docs/troubleshooting) for quick fixes. ## How it works 1. **Define your data** with CoValues schemas 2. **Connect to storage infrastructure** (Jazz Cloud or self-hosted) 3. **Create and edit CoValues** like normal objects 4. **Get automatic sync and persistence** across all devices and users Your UI updates instantly on every change, everywhere. It's like having reactive local state that happens to be shared with the world. ## Ready to see Jazz in action? Have a look at our [example apps](/examples) for inspiration and to see what's possible with Jazz. From real-time chat and collaborative editors to file sharing and social features — these are just the beginning of what you can build. ## Core concepts Learn how to structure your data using [collaborative values](/docs/core-concepts/covalues/overview) — the building blocks that make Jazz apps work. ## Sync and storage Sync and persist your data by setting up [sync and storage infrastructure](/docs/core-concepts/sync-and-storage) using Jazz Cloud, or host it yourself. ## Going deeper Get better results with AI by [importing the Jazz docs](/docs/tooling-and-resources/ai-tools) into your context window. If you have any questions or need assistance, please don't hesitate to reach out to us on [Discord](https://discord.gg/utDMjHYg42). We'd love to help you get started. ### Quickstart # Get started with Jazz in 10 minutes This quickstart guide will take you from an empty project to a working app with a simple data model and components to create and display your data. ## Create your App **Note: Requires Node.js 20+** ## Install Jazz The `jazz-tools` package includes everything you're going to need to build your first Jazz app. ```sh npm install jazz-tools ``` ## Get your free API key Sign up for a free API key at [dashboard.jazz.tools](https://dashboard.jazz.tools) for higher limits or production use, or use your email address as a temporary key to get started quickly. ```bash VITE_JAZZ_API_KEY="you@example.com" # or your API key ``` ## Define your schema Jazz uses Zod for more simple data types (like strings, numbers, booleans), and its own schemas to create collaborative data structures known as CoValues. CoValues are automatically persisted across your devices and the cloud and synced in real-time. Here we're defining a schema made up of both Zod types and CoValues. Adding a `root` to the user's account gives us a container that can be used to keep a track of all the data a user might need to use the app. ```ts import { co, z } from "jazz-tools"; export const Band = co.map({ name: z.string(), // Zod primitive type }); export const Festival = co.list(Band); export const JazzFestAccountRoot = co.map({ myFestival: Festival, }); export const JazzFestAccount = co .account({ root: JazzFestAccountRoot, profile: co.profile(), }) .withMigration((account) => { if (!account.$jazz.has("root")) { account.$jazz.set("root", { myFestival: [], }); } }); ``` ```tsx import { JazzBrowserContextManager } from 'jazz-tools/browser'; import { JazzFestAccount } from './schema'; const apiKey = import.meta.env.VITE_JAZZ_API_KEY; const contextManager = new JazzBrowserContextManager(); await contextManager.createContext({ sync: { peer: `wss://cloud.jazz.tools?key=${apiKey}` }, }); function getCurrentAccount() { const context = contextManager.getCurrentValue(); if (!context || !("me" in context)) { throw new Error(""); } return context.me; } ``` ## Start your app Moment of truth — time to start your app and see if it works. ```bash npm run dev ``` ### Not loading? If you're not seeing the welcome page: **Info: Still stuck?** Ask for help on [Discord](https://discord.gg/utDMjHYg42)! ## Create data ```tsx const me = getCurrentAccount(); const account = await JazzFestAccount.load(me.$jazz.id); if (!account.$isLoaded) throw new Error("Account is not loaded"); account.migrate(); const myAccount = await account.$jazz.ensureLoaded({ resolve: { root: { myFestival: true } }, }); const form = document.createElement('form'); const input = Object.assign(document.createElement('input'), { type: 'text', name: 'band', placeholder: 'Band name' }); const button = Object.assign(document.createElement('button'), { name: 'band', innerText: 'Add', onclick: async (e: Event) => { e.preventDefault(); // Prevent navigation if (!myAccount.$isLoaded) return; myAccount.root.myFestival.$jazz.push({ name: input.value }); input.value = ''; } }); form.append(input, button); ``` ## Display your data Now we've got a way to create data, so let's add a component to display it. ```tsx const bandList = document.createElement('ul'); const unsubscribe = myAccount.root.myFestival.$jazz.subscribe((festival) => { if (!festival.$isLoaded) throw new Error("Festival not loaded"); const bandElements = festival .map((band) => { if (!band.$isLoaded) return; const bandElement = document.createElement("li"); bandElement.innerText = band.name; return bandElement; }) .filter((band) => band !== undefined); bandList.replaceChildren(...bandElements); }); ``` ## Put it all together You've built all your components, time to put them together. ```tsx const app = document.querySelector('#app')!; app.append(form, bandList); ``` You should now be able to add a band to your festival, and see it appear in the list! **Congratulations! 🎉** You've built your first Jazz app! You've begun to scratch the surface of what's possible with Jazz. Behind the scenes, your local-first JazzFest app is **already** securely syncing your data to the cloud in real-time, ready for you to build more and more powerful features. ## Next steps * [Add authentication](/docs/key-features/authentication/quickstart) to your app so that you can log in and view your data wherever you are! * Dive deeper into the collaborative data structures we call [CoValues](/docs/core-concepts/covalues/overview) * Learn how to share and [collaborate on data](/docs/permissions-and-sharing/overview) using groups and permissions * Complete the [server-side quickstart](/docs/server-side/quickstart) to learn more about Jazz on the server ### Installation # Providers * **Data Synchronization**: Manages connections to peers and the Jazz cloud * **Local Storage**: Persists data locally between app sessions * **Schema Types**: Provides APIs for the [AccountSchema](/docs/core-concepts/schemas/accounts-and-migrations) * **Authentication**: Connects your authentication system to Jazz ## Setting up the Provider The provider accepts several configuration options: ```tsx import { JazzExpoProvider } from "jazz-tools/expo"; import { MyAppAccount } from "./schema"; export function MyJazzProvider({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` **Info: Tip** Sign up for a free API key at [dashboard.jazz.tools](https://dashboard.jazz.tools) for higher limits or production use, or use your email address as a temporary key to get started quickly. ```bash VITE_JAZZ_API_KEY="you@example.com" # or your API key ``` ## Provider Options ### Sync Options The `sync` property configures how your application connects to the Jazz network: ```ts import { type SyncConfig } from "jazz-tools"; export const syncConfig: SyncConfig = { // Connection to Jazz Cloud or your own sync server peer: `wss://cloud.jazz.tools/?key=${apiKey}`, // When to sync: "always" (default), "never", or "signedUp" when: "always", }; ``` **Warning: iOS Credential Persistence** When using `sync: 'never'` or `sync: 'signedUp'`, like all other data, the user's account exists only on their device, and is deleted if the user uninstalls your app. On iOS though, login credentials are saved to the Keychain, and are not deleted when the app is uninstalled. If a user reinstalls your app, Jazz will try to re-use these credentials to sign in to an account that no longer exists, which will cause errors. To avoid this, consider using `sync: 'always'` for your iOS users, or let them know they'll need to remove their credentials from Keychain before reinstalling. See [Authentication States](/docs/key-features/authentication/authentication-states#controlling-sync-for-different-authentication-states) for more details on how the `when` property affects synchronization based on authentication state. ### Account Schema The `AccountSchema` property defines your application's account structure: ```tsx import { JazzExpoProvider } from "jazz-tools/expo"; import { MyAppAccount } from "./schema"; export function MyJazzProvider({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` ### Additional Options The provider accepts these additional options: * `kvStore` * `ExpoSecureStoreAdapter` (default) * `AccountSchema` * `Account` (default) * `CryptoProvider` * `PureJSCrypto` (default) - Pure JavaScript crypto provider * `RNQuickCrypto` \- C++ accelerated crypto provider * `RNCrypto` \- Native crypto provider ## Authentication Jazz for Expo includes built-in local persistence using SQLite. Following Expo's best practices, the Expo implementation uses: * **Database Storage**: `expo-sqlite` \- Expo's official SQLite module * **Key-Value Storage**: `expo-secure-store` \- Expo's secure storage system Local persistence is enabled by default with no additional configuration required. Your data will automatically persist across app restarts. ## RNCrypto For accelerated crypto operations, you can use the `RNCrypto` crypto provider. It is the most performant crypto provider available for React Native and Expo. To use it, install the following package: ```bash pnpm add cojson-core-rn ``` **Pay Attention:** The version of `cojson-core-rn` must be the same as the version of `jazz-tools`. ```json "dependencies": { "cojson-core-rn": "x.x.x", # same version as jazz-tools "jazz-tools": "x.x.x" # same version as cojson-core-rn } ``` Then add the following to your provider: ```tsx import { JazzExpoProvider } from "jazz-tools/expo"; import { RNCrypto } from "jazz-tools/react-native-core/crypto/RNCrypto"; function MyJazzProvider({ children }: { children: ReactNode }) { return ( {children} ); } ``` ## Quick Crypto For accelerated crypto operations, you can use the `RNQuickCrypto` crypto provider. To use it, install the following Packages: ```bash pnpm add react-native-quick-crypto@1.0.0-beta.18 react-native-nitro-modules react-native-fast-encoder ``` Then add the following to your provider: ```tsx import { JazzExpoProvider } from "jazz-tools/expo"; import { RNQuickCrypto } from "jazz-tools/expo/crypto"; function MyJazzProvider({ children }: { children: ReactNode }) { return ( {children} ); } ``` For configuration, use the RNQC Expo config plugin: ```json { "expo": { "plugins": [ [ "react-native-quick-crypto", { "sodiumEnabled": true } ] ] } } ``` ## 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. --- # React Native (Expo) Installation and Setup Jazz supports Expo through the dedicated `jazz-tools/expo` entry, which is specifically designed for Expo applications. If you're building for React Native without Expo, please refer to the [React Native](/docs/react-native/project-setup) guide instead. Jazz requires an [Expo development build](https://docs.expo.dev/develop/development-builds/introduction/) using [Expo Prebuild](https://docs.expo.dev/workflow/prebuild/) for native code. It is **not compatible** with Expo Go. Jazz also supports the [New Architecture](https://docs.expo.dev/guides/new-architecture/). Tested with: ```json "expo": "~53.0.0", "react-native": "0.79.2", "react": "18.3.1" ``` ## Installation ### Create a new project (Skip this step if you already have one) ```bash npx create-expo-app my-jazz-app cd my-jazz-app npx expo prebuild ``` ### Install dependencies ```bash # Expo dependencies npx expo install expo-linking expo-secure-store expo-sqlite expo-file-system @react-native-community/netinfo expo-image-manipulator # React Native polyfills npm install @azure/core-asynciterator-polyfill react-native-url-polyfill readable-stream react-native-get-random-values react-native-fast-encoder # Jazz dependencies npm install jazz-tools ``` **Info: Note** Requires at least Node.js v20\. See our [Troubleshooting Guide](/docs/troubleshooting) for quick fixes. #### Fix incompatible dependencies If you encounter incompatible dependencies, you can try to fix them with the following command: ```bash npx expo install --fix ``` ### Add polyfills Jazz provides a quick way for you to apply the polyfills in your project. Import them in your root `_layout.tsx` component: **File name: app/\_layout.tsx** ```tsx // [!code ++:1] import "jazz-tools/expo/polyfills"; import { DarkTheme, DefaultTheme, ThemeProvider, } from "@react-navigation/native"; // ... ``` ## 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/key-features/authentication/overview) guide and see the [Expo Clerk Demo](https://github.com/garden-co/jazz/tree/main/examples/clerk-expo) for a complete example. ## Next Steps Now that you've set up your Expo project for Jazz, you'll need to: 1. [Set up the Jazz Provider](/docs/project-setup/providers) \- Configure how your app connects to Jazz 2. [Add authentication](/docs/key-features/authentication/overview) (optional) - Enable users to access data across devices 3. Define your schema - See the [schema docs](/docs/core-concepts/covalues/overview) for more information 4. Run 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 you installed them all and are importing them correctly * **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 ``` ### Troubleshooting # Setup troubleshooting A few reported setup hiccups and how to fix them. --- ## Node.js version requirements Jazz requires **Node.js v20 or later** due to native module dependencies. Check your version: ```sh node -v ``` If you’re on Node 18 or earlier, upgrade via nvm: ```sh nvm install 20 nvm use 20 ``` --- ### Required TypeScript Configuration In order to build successfully with TypeScript, you must ensure that you have the following options configured (either in your `tsconfig.json` or using the command line): * `skipLibCheck` must be `true` * `exactOptionalPropertyTypes` must be `false` --- ## npx jazz-run: command not found If, when running: ```sh npx jazz-run sync ``` you encounter: ```sh sh: jazz-run: command not found ``` This is often due to an npx cache quirk. (For most apps using Jazz) 1. Clear your npx cache: ```sh npx clear-npx-cache ``` 1. Rerun the command: ```sh npx jazz-run sync ``` --- ### Node 18 workaround (rebuilding the native module) If you can’t upgrade to Node 20+, you can rebuild the native `better-sqlite3` module for your architecture. 1. Install `jazz-run` locally in your project: ```sh pnpm add -D jazz-run ``` 1. Find the installed version of better-sqlite3 inside node\_modules. It should look like this: ```sh ./node_modules/.pnpm/better-sqlite3{version}/node_modules/better-sqlite3 ``` Replace `{version}` with your installed version and run: ```sh # Navigate to the installed module and rebuild pushd ./node_modules/.pnpm/better-sqlite3{version}/node_modules/better-sqlite3 && pnpm install && popd ``` If you get ModuleNotFoundError: No module named 'distutils': Linux: ```sh pip install --upgrade setuptools ``` macOS: ```sh brew install python-setuptools ``` _Workaround originally shared by @aheissenberger on Jun 24, 2025._ --- ### Still having trouble? If none of the above fixes work: Make sure dependencies installed without errors (`pnpm install`). Double-check your `node -v` output matches the required version. Open an issue on GitHub with: * Your OS and version * Node.js version * Steps you ran and full error output We're always happy to help! If you're stuck, reachout via [Discord](https://discord.gg/utDMjHYg42) ## Upgrade guides ### 0.19.0 - Explicit loading states ### 0.18.0 - New `$jazz` field in CoValues ### 0.17.0 - New image APIs ### 0.16.0 - Cleaner separation between Zod and CoValue schemas ### 0.15.0 - Everything inside `jazz-tools` ### 0.14.0 - Zod-based schemas ## Core Concepts ### Overview # Defining schemas: CoValues **CoValues ("Collaborative Values") are the core abstraction of Jazz.** They're your bread-and-butter datastructures that you use to represent everything in your app. As their name suggests, CoValues are inherently collaborative, meaning **multiple users and devices can edit them at the same time.** **Think of CoValues as "super-fast Git for lots of tiny data."** * CoValues keep their full edit histories, from which they derive their "current state". * The fact that this happens in an eventually-consistent way makes them [CRDTs](https://en.wikipedia.org/wiki/Conflict-free%5Freplicated%5Fdata%5Ftype). * Having the full history also means that you often don't need explicit timestamps and author info - you get this for free as part of a CoValue's [edit metadata](/docs/key-features/history). CoValues model JSON with CoMaps and CoLists, but also offer CoFeeds for simple per-user value feeds, and let you represent binary data with FileStreams. ## Start your app with a schema Fundamentally, CoValues are as dynamic and flexible as JSON, but in Jazz you use them by defining fixed schemas to describe the shape of data in your app. This helps correctness and development speed, but is particularly important... * when you evolve your app and need migrations * when different clients and server workers collaborate on CoValues and need to make compatible changes Thinking about the shape of your data is also a great first step to model your app. Even before you know the details of how your app will work, you'll probably know which kinds of objects it will deal with, and how they relate to each other. In Jazz, you define schemas using `co` for CoValues and `z` (from [Zod](https://zod.dev/)) for their primitive fields. **File name: schema.ts** ```ts import { co, z } from "jazz-tools"; export const TodoProject = co.map({ title: z.string(), tasks: ListOfTasks, }); ``` This gives us schema info that is available for type inference _and_ at runtime. Check out the inferred type of `project` in the example below, as well as the input `.create()` expects. ```ts import { Group } from "jazz-tools"; import { TodoProject, ListOfTasks } from "./schema"; const project = TodoProject.create( { title: "New Project", tasks: ListOfTasks.create([], Group.create()), }, Group.create(), ); ``` When creating CoValues that contain other CoValues, you can pass in a plain JSON object. Jazz will automatically create the CoValues for you. ```ts const group = Group.create().makePublic(); const publicProject = TodoProject.create( { title: "New Project", tasks: [], // Permissions are inherited, so the tasks list will also be public }, group, ); ``` **Info:** To learn more about how permissions work when creating nested CoValues with plain JSON objects, refer to [Ownership on implicit CoValue creation](/docs/permissions-and-sharing/cascading-permissions#ownership-on-implicit-covalue-creation). ## Types of CoValues ### `CoMap` (declaration) CoMaps are the most commonly used type of CoValue. They are the equivalent of JSON objects (Collaborative editing follows a last-write-wins strategy per-key). You can either declare struct-like CoMaps: ```ts export const Task = co.map({ title: z.string(), completed: z.boolean(), }); ``` Or record-like CoMaps (key-value pairs, where keys are always `string`): ```ts export const ColourToHex = co.record(z.string(), z.string()); export const ColorToFruit = co.record(z.string(), Fruit); ``` See the corresponding sections for [creating](/docs/core-concepts/covalues/comaps#creating-comaps),[subscribing/loading](/docs/core-concepts/subscription-and-loading),[reading from](/docs/core-concepts/covalues/comaps#reading-from-comaps) and[updating](/docs/core-concepts/covalues/comaps#updating-comaps) CoMaps. ### `CoList` (declaration) CoLists are ordered lists and are the equivalent of JSON arrays. (They support concurrent insertions and deletions, maintaining a consistent order.) You define them by specifying the type of the items they contain: ```ts export const ListOfColors = co.list(z.string()); export const ListOfTasks = co.list(Task); ``` See the corresponding sections for [creating](/docs/core-concepts/covalues/colists#creating-colists),[subscribing/loading](/docs/core-concepts/subscription-and-loading),[reading from](/docs/core-concepts/covalues/colists#reading-from-colists) and[updating](/docs/core-concepts/covalues/colists#updating-colists) CoLists. ### `CoFeed` (declaration) CoFeeds are a special CoValue type that represent a feed of values for a set of users/sessions (Each session of a user gets its own append-only feed). They allow easy access of the latest or all items belonging to a user or their sessions. This makes them particularly useful for user presence, reactions, notifications, etc. You define them by specifying the type of feed item: ```ts export const FeedOfTasks = co.feed(Task); ``` See the corresponding sections for [creating](/docs/core-concepts/covalues/overview#creating-cofeeds),[subscribing/loading](/docs/core-concepts/subscription-and-loading),[reading from](/docs/core-concepts/covalues/cofeeds#reading-from-cofeeds) and[writing to](/docs/core-concepts/covalues/cofeeds#writing-to-cofeeds) CoFeeds. ### `FileStream` (declaration) FileStreams are a special type of CoValue that represent binary data. (They are created by a single user and offer no internal collaboration.) They allow you to upload and reference files. You typically don't need to declare or extend them yourself, you simply refer to the built-in `co.fileStream()` from another CoValue: ```ts export const Document = co.map({ title: z.string(), file: co.fileStream(), }); ``` See the corresponding sections for [creating](/docs/core-concepts/covalues/filestreams#creating-filestreams),[subscribing/loading](/docs/core-concepts/subscription-and-loading),[reading from](/docs/core-concepts/covalues/filestreams#reading-from-filestreams) and[writing to](/docs/core-concepts/covalues/filestreams#writing-to-filestreams) FileStreams. **Note: For images, we have a special, higher-level `co.image()` helper, see [ImageDefinition](/docs/core-concepts/covalues/imagedef).** ### Unions of CoMaps (declaration) You can declare unions of CoMaps that have discriminating fields, using `co.discriminatedUnion()`. ```ts export const ButtonWidget = co.map({ type: z.literal("button"), label: z.string(), }); export const SliderWidget = co.map({ type: z.literal("slider"), min: z.number(), max: z.number(), }); export const WidgetUnion = co.discriminatedUnion("type", [ ButtonWidget, SliderWidget, ]); ``` See the corresponding sections for [creating](/docs/core-concepts/schemas/schemaunions#creating-schema-unions),[subscribing/loading](/docs/core-concepts/subscription-and-loading) and[narrowing](/docs/core-concepts/schemas/schemaunions#narrowing-unions) schema unions. ## CoValue field/item types Now that we've seen the different types of CoValues, let's see more precisely how we declare the fields or items they contain. ### Primitive fields You can declare primitive field types using `z` (re-exported in `jazz-tools` from [Zod](https://zod.dev/)). Here's a quick overview of the primitive types you can use: ```ts z.string(); // For simple strings z.number(); // For numbers z.boolean(); // For booleans z.date(); // For dates z.literal(["waiting", "ready"]); // For enums ``` Finally, for more complex JSON data, that you _don't want to be collaborative internally_ (but only ever update as a whole), you can use more complex Zod types. For example, you can use `z.object()` to represent an internally immutable position: ```ts const Sprite = co.map({ // assigned as a whole position: z.object({ x: z.number(), y: z.number() }), }); ``` Or you could use a `z.tuple()`: ```ts const SpriteWithTuple = co.map({ // assigned as a whole position: z.tuple([z.number(), z.number()]), }); ``` ### References to other CoValues To represent complex structured data with Jazz, you form trees or graphs of CoValues that reference each other. Internally, this is represented by storing the IDs of the referenced CoValues in the corresponding fields, but Jazz abstracts this away, making it look like nested CoValues you can get or assign/insert. The important caveat here is that **a referenced CoValue might or might not be loaded yet,** but we'll see what exactly that means in [Subscribing and Deep Loading](/docs/core-concepts/subscription-and-loading). In Schemas, you declare references by just using the schema of the referenced CoValue: ```ts const Person = co.map({ name: z.string(), }); const ListOfPeople = co.list(Person); const Company = co.map({ members: ListOfPeople, }); ``` #### Optional References You can make schema fields optional using either `z.optional()` or `co.optional()`, depending on the type of value: * Use `z.optional()` for primitive Zod values like `z.string()`, `z.number()`, or `z.boolean()` * Use `co.optional()` for CoValues like `co.map()`, `co.list()`, or `co.record()` You can make references optional with `co.optional()`: ```ts const PersonWithOptionalProperties = co.map({ age: z.optional(z.number()), // primitive pet: co.optional(Pet), // CoValue }); ``` #### Recursive References You can wrap references in getters. This allows you to defer evaluation until the property is accessed. This technique is particularly useful for defining circular references, including recursive (self-referencing) schemas, or mutually recursive schemas. ```ts const SelfReferencingPerson = co.map({ name: z.string(), get bestFriend() { return Person; }, }); ``` You can use the same technique for mutually recursive references: ```ts const MutuallyRecursivePerson = co.map({ name: z.string(), get friends() { return ListOfFriends; }, }); const ListOfFriends = co.list(Person); ``` If you try to reference `ListOfPeople` in `Person` without using a getter, you'll run into a `ReferenceError` because of the [temporal dead zone](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let#temporal%5Fdead%5Fzone%5Ftdz). ### Helper methods If you find yourself repeating the same logic to access computed CoValues properties, you can define helper functions to encapsulate it for better reusability: ```ts const Person = co.map({ firstName: z.string(), lastName: z.string(), dateOfBirth: z.date(), }); type Person = co.loaded; export function getPersonFullName(person: Person) { return `${person.firstName} ${person.lastName}`; } function differenceInYears(date1: Date, date2: Date) { const diffTime = Math.abs(date1.getTime() - date2.getTime()); return Math.ceil(diffTime / (1000 * 60 * 60 * 24 * 365.25)); } export function getPersonAgeAsOf(person: Person, date: Date) { return differenceInYears(date, person.dateOfBirth); } const person = Person.create({ firstName: "John", lastName: "Doe", dateOfBirth: new Date("1990-01-01"), }); const fullName = getPersonFullName(person); const age = getPersonAgeAsOf(person, new Date()); ``` Similarly, you can encapsulate logic needed to update CoValues: ```ts export function updatePersonName(person: Person, fullName: string) { const [firstName, lastName] = fullName.split(" "); person.$jazz.set("firstName", firstName); person.$jazz.set("lastName", lastName); } console.log(person.firstName, person.lastName); // John Doe updatePersonName(person, "Jane Doe"); console.log(person.firstName, person.lastName); // Jane Doe ``` ### CoMaps # CoMaps CoMaps are key-value objects that work like JavaScript objects. You can access properties with dot notation and define typed fields that provide TypeScript safety. They're ideal for structured data that needs type validation. ## Creating CoMaps CoMaps are typically defined with `co.map()` and specifying primitive fields using `z` (see [Defining schemas: CoValues](/docs/core-concepts/covalues/overview) for more details on primitive fields): ```ts import { co, z } from "jazz-tools"; const Project = co.map({ name: z.string(), startDate: z.date(), status: z.literal(["planning", "active", "completed"]), coordinator: co.optional(Member), }); export type Project = co.loaded; export type ProjectInitShape = co.input; // type accepted by `Project.create` ``` You can create either struct-like CoMaps with fixed fields (as above) or record-like CoMaps for key-value pairs: ```ts const Inventory = co.record(z.string(), z.number()); ``` To instantiate a CoMap: ```ts const project = Project.create({ name: "Spring Planting", startDate: new Date("2025-03-15"), status: "planning", }); const inventory = Inventory.create({ tomatoes: 48, basil: 12, }); ``` ### Ownership When creating CoMaps, you can specify ownership to control access: ```ts // Create with default owner (current user) const privateProject = Project.create({ name: "My Herb Garden", startDate: new Date("2025-04-01"), status: "planning", }); // Create with shared ownership const gardenGroup = Group.create(); gardenGroup.addMember(memberAccount, "writer"); const communityProject = Project.create( { name: "Community Vegetable Plot", startDate: new Date("2025-03-20"), status: "planning", }, { owner: gardenGroup }, ); ``` See [Groups as permission scopes](/docs/permissions-and-sharing/overview) for more information on how to use groups to control access to CoMaps. ## Reading from CoMaps CoMaps can be accessed using familiar JavaScript object notation: ```ts console.log(project.name); // "Spring Planting" console.log(project.status); // "planning" ``` ### Handling Optional Fields Optional fields require checks before access: ```ts if (project.coordinator) { console.log(project.coordinator.name); // Safe access } ``` ### Recursive references You can wrap references in getters. This allows you to defer evaluation until the property is accessed. This technique is particularly useful for defining circular references, including recursive (self-referencing) schemas, or mutually recursive schemas. ```ts import { co, z } from "jazz-tools"; const Project = co.map({ name: z.string(), startDate: z.date(), status: z.literal(["planning", "active", "completed"]), coordinator: co.optional(Member), get subProject() { return Project.optional(); }, }); export type Project = co.loaded; ``` When the recursive references involve more complex types, it is sometimes required to specify the getter return type: ```ts const ProjectWithTypedGetter = co.map({ name: z.string(), startDate: z.date(), status: z.literal(["planning", "active", "completed"]), coordinator: co.optional(Member), // [!code ++:3] get subProjects(): co.Optional> { return co.optional(co.list(Project)); }, }); export type Project = co.loaded; ``` ### Partial For convenience Jazz provies a dedicated API for making all the properties of a CoMap optional: ```ts const Project = co.map({ name: z.string(), startDate: z.date(), status: z.literal(["planning", "active", "completed"]), }); const ProjectDraft = Project.partial(); // The fields are all optional now const project = ProjectDraft.create({}); ``` ### Pick You can also pick specific fields from a CoMap: ```ts const Project = co.map({ name: z.string(), startDate: z.date(), status: z.literal(["planning", "active", "completed"]), }); const ProjectStep1 = Project.pick({ name: true, startDate: true, }); // We don't provide the status field const project = ProjectStep1.create({ name: "My project", startDate: new Date("2025-04-01"), }); ``` ### Working with Record CoMaps For record-type CoMaps, you can access values using bracket notation: ```ts const inventory = Inventory.create({ tomatoes: 48, peppers: 24, basil: 12, }); console.log(inventory["tomatoes"]); // 48 ``` ## Updating CoMaps To update a CoMap's properties, use the `$jazz.set` method: ```ts project.$jazz.set("name", "Spring Vegetable Garden"); // Update name project.$jazz.set("startDate", new Date("2025-03-20")); // Update date ``` **Info:** The `$jazz` namespace is available on all CoValues, and provides access to methods to modify and load CoValues, as well as access common properties like `id` and `owner`. When updating references to other CoValues, you can provide both the new CoValue or a JSON object from which the new CoValue will be created. ```ts const Dog = co.map({ name: co.plainText(), }); const Person = co.map({ name: co.plainText(), dog: Dog, }); const person = Person.create({ name: "John", dog: { name: "Rex" }, }); // Update the dog field using a CoValue person.$jazz.set("dog", Dog.create({ name: co.plainText().create("Fido") })); // Or use a plain JSON object person.$jazz.set("dog", { name: "Fido" }); ``` When providing a JSON object, Jazz will automatically create the CoValues for you. To learn more about how permissions work in this case, refer to[Ownership on implicit CoValue creation](/docs/permissions-and-sharing/cascading-permissions#ownership-on-implicit-covalue-creation). ### Type Safety CoMaps are fully typed in TypeScript, giving you autocomplete and error checking: ```ts project.$jazz.set("name", "Spring Vegetable Planting"); // ✓ Valid string // [!code --] project.$jazz.set("startDate", "2025-03-15"); // ✗ Type error: expected Date // [!code --] // Argument of type 'string' is not assignable to parameter of type 'Date' ``` ### Soft Deletion Implementing a soft deletion pattern by using a `deleted` flag allows you to maintain data for potential recovery and auditing. ```ts const Project = co.map({ name: z.string(), // [!code ++] deleted: z.optional(z.boolean()), }); ``` When an object needs to be "deleted", instead of removing it from the system, the deleted flag is set to true. This gives us a property to omit it in the future. ### Deleting Properties You can delete properties from CoMaps: ```ts inventory.$jazz.delete("basil"); // Remove a key-value pair // For optional fields in struct-like CoMaps project.$jazz.set("coordinator", undefined); // Remove the reference ``` ## Running migrations on CoMaps Migrations are functions that run when a CoMap is loaded, allowing you to update existing data to match new schema versions. Use them when you need to modify the structure of CoMaps that already exist in your app. Unlike [Account migrations](/docs/core-concepts/schemas/accounts-and-migrations#when-migrations-run), CoMap migrations are not run when a CoMap is created. **Note:** Migrations are run synchronously and cannot be run asynchronously. Here's an example of a migration that adds the `priority` field to the `Task` CoMap: ```ts const Task = co .map({ done: z.boolean(), text: co.plainText(), version: z.literal([1, 2]), priority: z.enum(["low", "medium", "high"]), // new field }) .withMigration((task) => { if (task.version === 1) { task.$jazz.set("priority", "medium"); // Upgrade the version so the migration won't run again task.$jazz.set("version", 2); } }); ``` ### Migration best practices Design your schema changes to be compatible with existing data: * **Add, don't change:** Only add new fields; avoid renaming or changing types of existing fields * **Make new fields optional:** This prevents errors when loading older data * **Use version fields:** Track schema versions to run migrations only when needed ### Migration & reader permissions Migrations need write access to modify CoMaps. If some users only have read permissions, they can't run migrations on those CoMaps. **Forward-compatible schemas** (where new fields are optional) handle this gracefully - users can still use the app even if migrations haven't run. **Non-compatible changes** require handling both schema versions in your app code using discriminated unions. When you can't guarantee all users can run migrations, handle multiple schema versions explicitly: ```ts const TaskV1 = co.map({ version: z.literal(1), done: z.boolean(), text: z.string(), }); const TaskV2 = co .map({ // We need to be more strict about the version to make the // discriminated union work version: z.literal(2), done: z.boolean(), text: z.string(), priority: z.enum(["low", "medium", "high"]), }) .withMigration((task) => { if (task.version === 1) { task.$jazz.set("version", 2); task.$jazz.set("priority", "medium"); } }); // Export the discriminated union; because some users might // not be able to run the migration export const Task = co.discriminatedUnion("version", [TaskV1, TaskV2]); export type Task = co.loaded; ``` ## Best Practices ### Structuring Data * Use struct-like CoMaps for entities with fixed, known properties * Use record-like CoMaps for dynamic key-value collections * Group related properties into nested CoMaps for better organization ### Common Patterns #### Helper methods You should define helper methods of CoValue schemas separately, in standalone functions: ```ts import { co, z } from "jazz-tools"; const Project = co.map({ name: z.string(), startDate: z.date(), endDate: z.optional(z.date()), }); type Project = co.loaded; export function isProjectActive(project: Project) { const now = new Date(); return ( now >= project.startDate && (!project.endDate || now <= project.endDate) ); } export function formatProjectDuration( project: Project, format: "short" | "full", ) { const start = project.startDate.toLocaleDateString(); if (!project.endDate) { return format === "full" ? `Started on ${start}, ongoing` : `From ${start}`; } const end = project.endDate.toLocaleDateString(); return format === "full" ? `From ${start} to ${end}` : `${(project.endDate.getTime() - project.startDate.getTime()) / 86400000} days`; } const project = Project.create({ name: "My project", startDate: new Date("2025-04-01"), endDate: new Date("2025-04-04"), }); console.log(isProjectActive(project)); // false console.log(formatProjectDuration(project, "short")); // "3 days" ``` #### Uniqueness CoMaps are typically created with a CoValue ID that acts as an opaque UUID, by which you can then load them. However, there are situations where it is preferable to load CoMaps using a custom identifier: * The CoMaps have user-generated identifiers, such as a slug * The CoMaps have identifiers referring to equivalent data in an external system * The CoMaps have human-readable & application-specific identifiers * If an application has CoValues used by every user, referring to it by a unique _well-known_ name (eg, `"my-global-comap"`) can be more convenient than using a CoValue ID Consider a scenario where one wants to identify a CoMap using some unique identifier that isn't the Jazz CoValue ID: ```ts // This will not work as `learning-jazz` is not a CoValue ID const myTask = await Task.load("learning-jazz"); ``` To make it possible to use human-readable identifiers Jazz lets you to define a `unique` property on CoMaps. Then the CoValue ID is deterministically derived from the `unique` property and the owner of the CoMap. ```ts // Given the project owner, myTask will have always the same id Task.create( { text: "Let's learn some Jazz!", }, { unique: "learning-jazz", owner: project.$jazz.owner, // Different owner, different id }, ); ``` Now you can use `CoMap.loadUnique` to easily load the CoMap using the human-readable identifier: ```ts const learnJazzTask = await Task.loadUnique( "learning-jazz", project.$jazz.owner.$jazz.id, ); ``` It's also possible to combine the create+load operation using `CoMap.upsertUnique`: ```ts await Task.upsertUnique({ value: { text: "Let's learn some Jazz!", }, unique: "learning-jazz", owner: project.$jazz.owner, }); ``` **Caveats:** * The `unique` parameter acts as an _immutable_ identifier - i.e. the same `unique` parameter in the same `Group` will always refer to the same CoValue. * To make dynamic renaming possible, you can create an indirection where a stable CoMap identified by a specific value of `unique` is simply a pointer to another CoMap with a normal, dynamic CoValue ID. This pointer can then be updated as desired by users with the corresponding permissions. * This way of introducing identifiers allows for very fast lookup of individual CoMaps by identifier, but it doesn't let you enumerate all the CoMaps identified this way within a `Group`. If you also need enumeration, consider using a global `co.record()` that maps from identifier to a CoMap, which you then do lookups in (this requires at least a shallow load of the entire `co.record()`, but this should be fast for up to 10s of 1000s of entries) #### Creating Set-like Collections You can use CoRecords as a way to create set-like collections, by keying the CoRecord on the item's CoValue ID. You can then use static `Object` methods to iterate over the CoRecord, effectively allowing you to treat it as a set. ```ts const Chat = co.map({ messages: co.list(Message), participants: co.record(z.string(), MyAppUser), }); const chat = await Chat.load(chatId, { resolve: { participants: true, }, }); let participantList: string[]; // Note that I don't need to load the map deeply to read and set keys if (chat.$isLoaded) { chat.participants.$jazz.set(me.$jazz.id, me); participantList = Object.keys(chat.participants); } ``` You can choose a loading strategy for the CoRecord. Use $each when you need all item properties to be immediately available. In general, it is enough to shallowly load a CoRecord to access its keys, and then load the values of those keys as needed (for example, by passing the keys as strings to a child component). ```ts const { participants } = await chat.$jazz.ensureLoaded({ resolve: { participants: { $each: { profile: { avatar: true, }, }, }, }, }); const avatarList = Object.values(participants).map( (user) => user.profile.avatar, ); ``` ### CoLists # CoLists CoLists are ordered collections that work like JavaScript arrays. They provide indexed access, iteration methods, and length properties, making them perfect for managing sequences of items. ## Creating CoLists CoLists are defined by specifying the type of items they contain: ```ts import { co, z } from "jazz-tools"; const ListOfResources = co.list(z.string()); export type ListOfResources = co.loaded; const ListOfTasks = co.list(Task); export type ListOfTasks = co.loaded; export type ListOfTasksInitShape = co.input; // type accepted by `ListOfTasks.create` ``` To create a `CoList`: ```ts // Create an empty list const resources = co.list(z.string()).create([]); // Create a list with initial items const tasks = co.list(Task).create([ { title: "Prepare soil beds", status: "in-progress" }, { title: "Order compost", status: "todo" }, ]); ``` ### Ownership Like other CoValues, you can specify ownership when creating CoLists. ```ts // Create with shared ownership const teamGroup = Group.create(); teamGroup.addMember(colleagueAccount, "writer"); const teamList = co.list(Task).create([], { owner: teamGroup }); ``` See [Groups as permission scopes](/docs/permissions-and-sharing/overview) for more information on how to use groups to control access to CoLists. ## Reading from CoLists CoLists support standard array access patterns: ```ts // Access by index const firstTask = tasks[0]; console.log(firstTask.title); // "Prepare soil beds" // Get list length console.log(tasks.length); // 2 // Iteration tasks.forEach((task) => { console.log(task.title); // "Prepare soil beds" // "Order compost" }); // Array methods const todoTasks = tasks.filter((task) => task.status === "todo"); console.log(todoTasks.length); // 1 ``` ## Updating CoLists Methods to update a CoList's items are grouped inside the `$jazz` namespace: ```ts // Add items resources.$jazz.push("Tomatoes"); // Add to end resources.$jazz.unshift("Lettuce"); // Add to beginning tasks.$jazz.push({ // Add complex items title: "Install irrigation", // (Jazz will create status: "todo", // the CoValue for you!) }); // Replace items resources.$jazz.set(0, "Cucumber"); // Replace by index // Modify nested items tasks[0].$jazz.set("status", "complete"); // Update properties of references ``` ### Soft Deletion You can do a soft deletion by using a deleted flag, then creating a helper method that explicitly filters out items where the deleted property is true. ```ts const Task = co.map({ title: z.string(), status: z.literal(["todo", "in-progress", "complete"]), deleted: z.optional(z.boolean()), // [!code ++] }); type Task = typeof Task; const ListOfTasks = co.list(Task); type ListOfTasks = typeof ListOfTasks; export function getCurrentTasks(list: co.loaded) { return list.filter((task): task is co.loaded => !task.deleted); } async function main() { const myTaskList = ListOfTasks.create([]); myTaskList.$jazz.push({ title: "Tomatoes", status: "todo", deleted: false, }); myTaskList.$jazz.push({ title: "Cucumbers", status: "todo", deleted: true, }); myTaskList.$jazz.push({ title: "Carrots", status: "todo", }); const activeTasks = getCurrentTasks(myTaskList); console.log(activeTasks.map((task) => task.title)); // Output: ["Tomatoes", "Carrots"] } ``` There are several benefits to soft deletions: * **recoverablity** \- Nothing is truly deleted, so recovery is possible in the future * **data integrity** \- Relationships can be maintained between current and deleted values * **auditable** \- The data can still be accessed, good for audit trails and checking compliance ### Deleting Items Jazz provides two methods to retain or remove items from a CoList: ```ts // Remove items resources.$jazz.remove(2); // By index console.log(resources); // ["Cucumber", "Peppers"] resources.$jazz.remove((item) => item === "Cucumber"); // Or by predicate console.log(resources); // ["Tomatoes", "Peppers"] // Keep only items matching the predicate resources.$jazz.retain((item) => item !== "Cucumber"); console.log(resources); // ["Tomatoes", "Peppers"] ``` You can also remove specific items by index with `splice`, or remove the first or last item with `pop` or `shift`: ```ts // Remove 2 items starting at index 1 resources.$jazz.splice(1, 2); console.log(resources); // ["Tomatoes"] // Remove a single item at index 0 resources.$jazz.splice(0, 1); console.log(resources); // ["Cucumber", "Peppers"] // Remove items const lastItem = resources.$jazz.pop(); // Remove and return last item resources.$jazz.shift(); // Remove first item ``` ### Array Methods `CoList`s support the standard JavaScript array methods you already know. Methods that mutate the array are grouped inside the `$jazz` namespace. ```ts // Add multiple items at once resources.$jazz.push("Tomatoes", "Basil", "Peppers"); // Find items const basil = resources.find((r) => r === "Basil"); // Filter (returns regular array, not a CoList) const tItems = resources.filter((r) => r.startsWith("T")); console.log(tItems); // ["Tomatoes"] ``` ### Type Safety CoLists maintain type safety for their items: ```ts // TypeScript catches type errors resources.$jazz.push("Carrots"); // ✓ Valid string // [!code --] resources.$jazz.push(42); // ✗ Type error: expected string // [!code --] // Argument of type 'number' is not assignable to parameter of type 'string' // For lists of references tasks.forEach((task) => { console.log(task.title); // TypeScript knows task has title }); ``` ## Best Practices ### Common Patterns #### List Rendering CoLists work well with UI rendering libraries: ```ts import { co, z } from "jazz-tools"; const ListOfTasks = co.list(Task); // React example function TaskList({ tasks }: { tasks: co.loaded }) { return (
    {tasks.map((task) => task.$isLoaded ? (
  • {task.title} - {task.status}
  • ) : null, )}
); } ``` #### Managing Relations CoLists can be used to create one-to-many relationships: ```ts import { co, z } from "jazz-tools"; const Task = co.map({ title: z.string(), status: z.literal(["todo", "in-progress", "complete"]), get project(): co.Optional { return co.optional(Project); }, }); const ListOfTasks = co.list(Task); const Project = co.map({ name: z.string(), get tasks(): co.List { return ListOfTasks; }, }); const project = Project.create({ name: "Garden Project", tasks: ListOfTasks.create([]), }); const task = Task.create({ title: "Plant seedlings", status: "todo", project: project, // Add a reference to the project }); // Add a task to a garden project project.tasks.$jazz.push(task); // Access the project from the task console.log(task.project); // { name: "Garden Project", tasks: [task] } ``` #### Set-like Collections CoLists, like JavaScript arrays, allow you to insert the same item multiple times. In some cases, you might want to have a collection of unique items (similar to a set). To achieve this, you can use a CoRecord with entries keyed on a unique identifier (for example, the CoValue ID). You can read [more about this pattern here](/docs/core-concepts/covalues/comaps#creating-set-like-collections). ### CoFeeds # CoFeeds CoFeeds are append-only data structures that track entries from different user sessions and accounts. Unlike other CoValues where everyone edits the same data, CoFeeds maintain separate streams for each session. Each account can have multiple sessions (different browser tabs, devices, or app instances), making CoFeeds ideal for building features like activity logs, presence indicators, and notification systems. The following examples demonstrate a practical use of CoFeeds: * [Multi-cursors](https://github.com/garden-co/jazz/tree/main/examples/multi-cursors) \- track user presence on a canvas with multiple cursors and out of bounds indicators * [Reactions](https://github.com/garden-co/jazz/tree/main/examples/reactions) \- store per-user emoji reaction using a CoFeed ## Creating CoFeeds CoFeeds are defined by specifying the type of items they'll contain, similar to how you define CoLists: ```ts // Define a schema for feed items const Activity = co.map({ timestamp: z.date(), action: z.literal(["watering", "planting", "harvesting", "maintenance"]), notes: z.optional(z.string()), }); export type Activity = co.loaded; // Define a feed of garden activities const ActivityFeed = co.feed(Activity); // Create a feed instance const activityFeed = ActivityFeed.create([]); ``` ### Ownership Like other CoValues, you can specify ownership when creating CoFeeds. ```ts const teamGroup = Group.create(); teamGroup.addMember(colleagueAccount, "writer"); const teamFeed = ActivityFeed.create([], { owner: teamGroup }); ``` See [Groups as permission scopes](/docs/permissions-and-sharing/overview) for more information on how to use groups to control access to CoFeeds. ## Reading from CoFeeds Since CoFeeds are made of entries from users over multiple sessions, you can access entries in different ways - from a specific user's session or from their account as a whole. ### Per-Session Access To retrieve entries from a session: ```ts // Get the feed for a specific session const sessionFeed = activityFeed.perSession[sessionId]; // Latest entry from a session if (sessionFeed?.value.$isLoaded) { console.log(sessionFeed.value.action); // "watering" } ``` For convenience, you can also access the latest entry from the current session with `inCurrentSession`: ```ts // Get the feed for the current session const currentSessionFeed = activityFeed.inCurrentSession; // Latest entry from the current session if (currentSessionFeed?.value.$isLoaded) { console.log(currentSessionFeed.value.action); // "harvesting" } ``` ### Per-Account Access To retrieve entries from a specific account (with entries from all sessions combined) use `perAccount`: ```ts // Get the feed for a specific session const accountFeed = activityFeed.perAccount[accountId]; // Latest entry from an account if (accountFeed?.value.$isLoaded) { console.log(accountFeed.value.action); // "watering" } ``` For convenience, you can also access the latest entry from the current account with `byMe`: ```ts // Get the feed for the current account const myLatestEntry = activityFeed.byMe; // Latest entry from the current account if (myLatestEntry?.value.$isLoaded) { console.log(myLatestEntry.value.action); // "harvesting" } ``` ### Feed Entries #### All Entries To retrieve all entries from a CoFeed: ```ts // Get the feeds for a specific account and session const accountFeed = activityFeed.perAccount[accountId]; const sessionFeed = activityFeed.perSession[sessionId]; // Iterate over all entries from the account for (const entry of accountFeed.all) { if (entry.value.$isLoaded) { console.log(entry.value); } } // Iterate over all entries from the session for (const entry of sessionFeed.all) { if (entry.value.$isLoaded) { console.log(entry.value); } } ``` #### Latest Entry To retrieve the latest entry from a CoFeed, ie. the last update: ```ts // Get the latest entry from the current account const latestEntry = activityFeed.byMe; if (latestEntry?.value.$isLoaded) { console.log(`My last action was ${latestEntry?.value?.action}`); // "My last action was harvesting" } // Get the latest entry from each account const latestEntriesByAccount = Object.values(activityFeed.perAccount).map( (entry) => ({ accountName: entry.by?.profile.$isLoaded ? entry.by.profile.name : "Unknown", value: entry.value, }), ); ``` ## Writing to CoFeeds CoFeeds are append-only; you can add new items, but not modify existing ones. This creates a chronological record of events or activities. ### Adding Items ```ts // Log a new activity activityFeed.$jazz.push( Activity.create({ timestamp: new Date(), action: "watering", notes: "Extra water for new seedlings", }), ); ``` Each item is automatically associated with the current user's session. You don't need to specify which session the item belongs to - Jazz handles this automatically. ### Understanding Session Context Each entry is automatically added to the current session's feed. When a user has multiple open sessions (like both a mobile app and web browser), each session creates its own separate entries: ```ts // On mobile device: fromMobileFeed.$jazz.push( Activity.create({ timestamp: new Date(), action: "harvesting", notes: "Vegetable patch", }), ); // On web browser (same user): fromBrowserFeed.$jazz.push( Activity.create({ timestamp: new Date(), action: "planting", notes: "Flower bed", }), ); // These are separate entries in the same feed, from the same account ``` ## Metadata CoFeeds support metadata, which is useful for tracking information about the feed itself. ### By The `by` property is the account that made the entry. ```ts Me // Get the feed for the current account const myLatestEntry = activityFeed.byMe; // Latest entry from the current account if (myLatestEntry?.value.$isLoaded) { console.log(myLatestEntry.value.action); // "harvesting" } ``` ### MadeAt The `madeAt` property is a timestamp of when the entry was added to the feed. ```ts const accountFeed = activityFeed.perAccount[accountId]; // Get the timestamp of the last update console.log(accountFeed?.madeAt); // Get the timestamp of each entry for (const entry of accountFeed.all) { console.log(entry.madeAt); } ``` ## Best Practices ### When to Use CoFeeds * **Use CoFeeds when**: * You need to track per-user/per-session data * Time-based information matters (activity logs, presence) * **Consider alternatives when**: * Data needs to be collaboratively edited (use CoMaps or CoLists) * You need structured relationships (use CoMaps/CoLists with references) ### CoTexts # CoTexts Jazz provides two CoValue types for collaborative text editing, collectively referred to as "CoText" values: * **`co.plainText()`** for simple text editing without formatting * **`co.richText()`** for rich text with HTML-based formatting (extends `co.plainText()`) Both types enable real-time collaborative editing of text content while maintaining consistency across multiple users. **Note:** If you're looking for a quick way to add rich text editing to your app, check out [our prosemirror plugin](#using-rich-text-with-prosemirror). ```ts const note = co.plainText().create("Meeting notes"); // Update the text note.$jazz.applyDiff("Meeting notes for Tuesday"); console.log(note.toString()); // "Meeting notes for Tuesday" ``` For a full example of CoTexts in action, see [our Richtext example app](https://github.com/garden-co/jazz/tree/main/examples/richtext-prosemirror), which shows plain text and rich text editing. ## `co.plainText()` vs `z.string()` While `z.string()` is perfect for simple text fields, `co.plainText()` is the right choice when you need: * Frequent text edits that aren't just replacing the whole field * Fine-grained control over text edits (inserting, deleting at specific positions) * Multiple users editing the same text simultaneously * Character-by-character collaboration * Efficient merging of concurrent changes Both support real-time updates, but `co.plainText()` provides specialized tools for collaborative editing scenarios. ## Creating CoText Values CoText values are typically used as fields in your schemas: ```ts const Profile = co.profile({ name: z.string(), bio: co.plainText(), // Plain text field description: co.richText(), // Rich text with formatting }); ``` Create a CoText value with a simple string: ```ts // Create plaintext with default ownership (current user) const meetingNotes = co.plainText().create("Meeting notes"); // Create rich text with HTML content const document = co .richText() .create("

Project overview

"); ``` ### Ownership Like other CoValues, you can specify ownership when creating CoTexts. ```ts // Create with shared ownership const teamGroup = Group.create(); teamGroup.addMember(colleagueAccount, "writer"); const teamNote = co.plainText().create("Team updates", { owner: teamGroup }); ``` See [Groups as permission scopes](/docs/permissions-and-sharing/overview) for more information on how to use groups to control access to CoText values. ## Reading Text CoText values work similarly to JavaScript strings: ```ts // Get the text content console.log(note.toString()); // "Meeting notes" console.log(`${note}`); // "Meeting notes" // Check the text length console.log(note.length); // 14 ``` ## Making Edits Insert and delete text with intuitive methods: ```ts // Insert text at a specific position note.insertBefore(8, "weekly "); // "Meeting weekly notes" // Insert after a position note.insertAfter(21, " for Monday"); // "Meeting weekly notes for Monday" // Delete a range of text note.deleteRange({ from: 8, to: 15 }); // "Meeting notes for Monday" // Apply a diff to update the entire text note.$jazz.applyDiff("Team meeting notes for Tuesday"); ``` ### Applying Diffs Use `applyDiff` to efficiently update text with minimal changes: ```ts // Original text: "Team status update" const minutes = co.plainText().create("Team status update"); // Replace the entire text with a new version minutes.$jazz.applyDiff("Weekly team status update for Project X"); // Make partial changes let text = minutes.toString(); text = text.replace("Weekly", "Monday"); minutes.$jazz.applyDiff(text); // Efficiently updates only what changed ``` Perfect for handling user input in form controls: ## Using Rich Text with ProseMirror Jazz provides a dedicated plugin for integrating `co.richText()` with the popular ProseMirror editor that enables bidirectional synchronization between your co.richText() instances and ProseMirror editors. ### ProseMirror Plugin Features * **Bidirectional Sync**: Changes in the editor automatically update the `co.richText()` and vice versa * **Real-time Collaboration**: Multiple users can edit the same document simultaneously * **HTML Conversion**: Automatically converts between HTML (used by `co.richText()`) and ProseMirror's document model ### Installation ```bash pnpm add prosemirror-view \ prosemirror-state \ prosemirror-schema-basic ``` ### Integration We don't currently have a React Native 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 function RichTextEditor() { const me = useAccount(JazzAccount, { resolve: { profile: { bio: true } } }); const editorRef = useRef(null); const viewRef = useRef(null); const bio = me.$isLoaded ? me.profile.bio : undefined; useEffect(() => { if (!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(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; } }; }, [bio?.$jazz.id]); if (!me.$isLoaded) return null; return (
); } ``` For use without a framework: ```ts function setupRichTextEditor( coRichText: CoRichText, container: HTMLDivElement, ) { // 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 doc = co.richText().create("

Initial content

"); const editorContainer = document.getElementById("editor") as HTMLDivElement; const cleanup = setupRichTextEditor(doc, 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/core-concepts/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: **File name: schema.ts** ```ts import { co, z } from "jazz-tools"; 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 // 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](#writing-to-filestreams): ```ts const fileStream = co.fileStream().create({ owner: myGroup }); ``` ### Ownership Like other CoValues, you can specify ownership when creating FileStreams. ```ts // Create a team group const teamGroup = Group.create(); teamGroup.addMember(colleagueAccount, "writer"); // Create a FileStream with shared ownership const teamFileStream = co.fileStream().create({ owner: teamGroup }); ``` See [Groups as permission scopes](/docs/permissions-and-sharing/overview) for more information on how to use groups to control access to 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 // 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 // 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 // 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 // Load directly as a Blob when you have an ID const blobFromID = await co.fileStream().loadAsBlob(fileStreamId); // By default, waits for complete uploads // For in-progress uploads: const partialBlob = await co.fileStream().loadAsBlob(fileStreamId, { allowUnfinished: true, }); ``` ### Checking Completion Status Check if a `FileStream` is fully synced: ```ts 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 // Create an empty FileStream const manualFileStream = co.fileStream().create({ owner: myGroup }); // Initialize with metadata manualFileStream.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 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}%`); } // Finalise the upload fileStream.end(); console.log("Upload complete!"); ``` ### Completing the Upload Once all chunks are pushed, mark the `FileStream` as complete: ```ts // Finalise 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 const fileStreamFromId = await co.fileStream().load(fileStreamId); if (fileStream.$isLoaded) { 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 const unsubscribe = co .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 // Wait for the FileStream to be fully synced await fileStream.$jazz.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. ### CoVectors # CoVectors CoVectors let you store and query high‑dimensional vectors directly in Jazz apps. They are ideal for semantic search, or personalization features that work offline, sync across devices, and remain end‑to‑end encrypted. The [Journal example](https://github.com/garden-co/jazz/tree/main/examples/vector-search) demonstrates semantic search using of CoVector. CoVectors are defined using `co.vector()`, and are often used as fields in a CoMap within a CoList (making it easy to perform vector search across list items). ```ts import { co, z } from "jazz-tools"; const Embedding = co.vector(384); // Define 384-dimensional embedding const Document = co.map({ content: z.string(), embedding: Embedding, }); export const DocumentsList = co.list(Document); ``` The number of dimensions matches the embedding model used in your app. Many small sentence transformers produce 384‑dim vectors; others use 512, 768, 1024 or more. ## Creating CoVectors You can create vectors in your Jazz application from an array of numbers, or Float32Array instance. ```ts // Generate embeddings (bring your own embeddings model) const vectorData = await createEmbedding("Text"); const newDocument = Document.create({ content: "Text", embedding: Embedding.create(vectorData), }); documents.$jazz.push(newDocument); ``` ### Ownership Like other CoValues, you can specify ownership when creating CoVectors. ```ts // Create with shared ownership const teamGroup = Group.create(); teamGroup.addMember(colleagueAccount, "writer"); const teamList = co.vector(384).create(vector, { owner: teamGroup }); ``` See [Groups as permission scopes](/docs/permissions-and-sharing/overview) for more information on how to use groups to control access to CoVectors. ### Immutability CoVectors cannot be changed after creation. Instead, create a new CoVector with the updated values and replace the previous one. ## Semantic Search Semantic search lets you find data based on meaning, not just keywords. In Jazz, you can easily sort results by how similar they are to your search query. ```ts // // 1) Load your documents const allDocuments = await DocumentsList.load(documentsListId, { resolve: { $each: { embedding: true }, }, }); // 2) Obtain vector for your search query const queryEmbedding = await createEmbedding("search query"); // 3) Sort documents by vector similarity const similarDocuments = documents.$isLoaded ? documents.map((value) => ({ value, similarity: value.embedding.$jazz.cosineSimilarity(queryEmbedding), // [!code ++] })) .sort((a, b) => b.similarity - a.similarity) .filter((result) => result.similarity > 0.5) : null; ``` Wrapping each item with its similarity score makes it easy to sort, filter, and display the most relevant results. This approach is widely used in vector search and recommendation systems, since it keeps both the data and its relevance together for further processing or display. ### Cosine Similarity To compare how similar two vectors are, we use their [cosine similarity](https://en.wikipedia.org/wiki/Cosine%5Fsimilarity). This returns a value between `-1` and `1`, describing how similar the vectors are: * `1` means the vectors are identical * `0` means the vectors are orthogonal (i.e. no similarity) * `-1` means the vectors are opposite direction (perfectly dissimilar). If you sort items by their cosine similarity, the ones which are most similar will appear at the top of the list. Jazz provides a built-in `$jazz.cosineSimilarity` method to calculate this for you. ## Embedding Models CoVectors handles storage and search, you provide the vectors. Generate embeddings with any model you prefer (Hugging Face, OpenAI, custom, etc). **Recommended:** Run models locally for privacy and offline support using [Transformers.js](https://huggingface.co/docs/transformers.js). Check our [Journal app example](https://github.com/garden-co/jazz/tree/main/examples/vector-search) to see how to do this. The following models offer a good balance between accuracy and performance: * [Xenova/all-MiniLM-L6-v2](https://huggingface.co/Xenova/all-MiniLM-L6-v2) — 384 dimensions, \~23 MB * [Xenova/paraphrase-multilingual-mpnet-base-v2](https://huggingface.co/Xenova/paraphrase-multilingual-mpnet-base-v2) — 768 dimensions, \~279 MB * [mixedbread-ai/mxbai-embed-large-v1](https://huggingface.co/mixedbread-ai/mxbai-embed-large-v1) — 1024 dimensions, \~337 MB * [Browse more models →](https://huggingface.co/models?pipeline%5Ftag=feature-extraction&library=transformers.js) Alternatively, you can generate embeddings using server-side or commercial APIs (such as OpenAI or Anthropic). ## Best Practices ### Changing embedding models **Always use the same embedding model for all vectors you intend to compare.**Mixing vectors from different models (or even different versions of the same model) will result in meaningless similarity scores, as the vector spaces are not compatible. If you need to switch models, consider storing the model identifier alongside each vector, and re-embedding your data as needed. ### ImageDefinitions # ImageDefinition `ImageDefinition` is a specialized CoValue designed specifically for managing images in Jazz applications. It extends beyond basic file storage by supporting a blurry placeholder, built-in resizing, and progressive loading patterns. Beyond `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 * [loadImage, loadImageBySize, highestResAvailable](#displaying-images) \- functions to load and display images * [Image](#displaying-images) \- Component to display an image ## Installation \[!framework=react-native-exp\] Jazz's images implementation is based on `expo-image-manipulator`. Check the [installation guide](/docs/react-native-expo/project-setup#install-dependencies) for more details. ## Creating Images The easiest way to create and use images in your Jazz application is with the `createImage()` function: ```ts import { createImage } from "jazz-tools/media"; import { launchImageLibrary } from "react-native-image-picker"; async function handleImagePicker() { // Use your favorite image picker library to get the image URI const result = await launchImageLibrary({ mediaType: "photo", quality: 1, }); if ( !result.didCancel && result.assets && result.assets.length > 0 && me.profile.$isLoaded ) { // Creates ImageDefinition with a blurry placeholder, limited to 1024px on the longest side, and multiple resolutions automatically. // See the options below for more details. const image = await createImage(result.assets[0].uri ?? "", { owner: me.$jazz.owner, maxSize: 1024, placeholder: "blur", progressive: true, }); // Store the image me.profile.$jazz.set("image", image); } } ``` The `createImage()` function: * Creates an `ImageDefinition` with the right properties * Optionally generates a small placeholder for immediate display * Creates multiple resolution variants of your image * Returns the created `ImageDefinition` ### Configuration Options ```ts declare function createImage( image: Blob | File | string, options?: { owner?: Group | Account; placeholder?: false | "blur"; maxSize?: number; progressive?: boolean; }, ): Promise>; ``` #### `image` The image to create an `ImageDefinition` from. This must be a `string` with the file path. #### `owner` The owner of the `ImageDefinition`. This is used to control access to the image. See [Groups as permission scopes](/docs/permissions-and-sharing/overview) for more information on how to use groups to control access to images. #### `placeholder` Disabled by default. This option allows you to automatically generate a low resolution preview for use while the image is loading. Currently, only `"blur"` is a supported. #### `maxSize` The image generation process includes a maximum size setting that controls the longest side of the image. A built-in resizing feature is applied based on this setting. #### `progressive` The progressive loading pattern is a technique that allows images to load incrementally, starting with a small version and gradually replacing it with a larger version as it becomes available. This is useful for improving the user experience by showing a placeholder while the image is loading. Passing `progressive: true` to `createImage()` will create internal smaller versions of the image for future uses. ### Create multiple resized copies To create multiple resized copies of an original image for better layout control, you can use the `createImage` function multiple times with different parameters for each desired size. Here’s an example of how you might implement this: ```ts import { co } from "jazz-tools"; import { createImage } from "jazz-tools/media"; // Jazz Schema const ProductImage = co.map({ image: co.image(), thumbnail: co.image(), }); const mainImage = await createImage(myBlob); const thumbnail = await createImage(myBlob, { maxSize: 100, }); // or, in case of migration, you can use the original stored image. const newThumb = await createImage(mainImage!.original!.toBlob()!, { maxSize: 100, }); const imageSet = ProductImage.create({ image: mainImage, thumbnail, }); ``` ### Creating images on the server We provide a `createImage` function to create images from server side using the same options as the browser version, using the package `jazz-tools/media/server`. Check the [server worker](/docs/server-side/setup) documentation to learn more. The resize features are based on the `sharp` library, then it is requested as peer dependency in order to use it. ```sh npm install sharp ``` ```ts import fs from "node:fs"; import { createImage } from "jazz-tools/media/server"; const image = fs.readFileSync(new URL("./image.jpg", import.meta.url)); await createImage(image, { // options }); ``` ## Displaying Images To use the stored ImageDefinition, there are two ways: declaratively, using the `Image` component, and imperatively, using the static methods. The Image component is the best way to let Jazz handle the image loading. ### `` component \[!framework=react,svelte,react-native,react-native-expo\] ```tsx import { Image } from "jazz-tools/expo"; import { StyleSheet } from "react-native"; function GalleryView({ image }: { image: co.loaded }) { return ( ); } const styles = StyleSheet.create({ galleryImage: { width: "100%", height: 200, borderRadius: 8, }, }); ``` The `Image` component handles: * Showing a placeholder while loading, if generated or specified * Automatically selecting the appropriate resolution, if generated with progressive loading * Progressive enhancement as higher resolutions become available, if generated with progressive loading * Determining the correct width/height attributes to avoid layout shifting * Cleaning up resources when unmounted The component's props are: ```ts export type ImageProps = Omit & { imageId: string; width?: number | "original"; height?: number | "original"; placeholder?: string; }; ``` #### Width and Height props \[!framework=react,svelte,react-native,react-native-expo\] The `width` and `height` props are used to control the best resolution to use but also the width and height attributes of the image tag. Let's say we have an image with a width of 1920px and a height of 1080px. ```tsx // Image with the highest resolution available // Image with width 1920 and height 1080 // Better to avoid, as may be rendered with 0 height // Keeps the aspect ratio (height: 338) // As above, aspect ratio is maintained, width is 1067 // Renders as a 600x600 square ``` If the image was generated with progressive loading, the `width` and `height` props will determine the best resolution to use. #### Placeholder You can choose to specify a custom placeholder to display as a fallback while an image is loading in case your image does not have a placeholder generated. A data URL or a URL for a static asset works well here. ### Imperative Usage \[!framework=react,svelte,react-native,react-native-expo\] Like other CoValues, `ImageDefinition` can be used to load the object. ```tsx const image = await ImageDefinition.load("123", { resolve: { original: true, }, }); if (image.$isLoaded) { console.log({ originalSize: image.originalSize, placeholderDataUrl: image.placeholderDataURL, original: image.original, // this FileStream may be not loaded yet }); } ``` `image.original` is a `FileStream` and its content can be read as described in the [FileStream](/docs/core-concepts/covalues/filestreams#reading-from-filestreams) documentation. Since FileStream objects are also CoValues, they must be loaded before use. To simplify loading, if you want to load the binary data saved as Original, you can use the `loadImage` function. ```tsx import { loadImage } from "jazz-tools/media"; const loadedImage = await loadImage(imageDefinitionOrId); if (loadedImage === null) { throw new Error("Image not found"); } const img = document.createElement("img"); img.width = loadedImage.width; img.height = loadedImage.height; img.src = URL.createObjectURL(loadedImage.image.toBlob()!); img.onload = () => URL.revokeObjectURL(img.src); ``` If the image was generated with progressive loading, and you want to access the best-fit resolution, use `loadImageBySize`. It will load the image of the best resolution that fits the wanted width and height. ```tsx import { loadImageBySize } from "jazz-tools/media"; const imageLoadedBySize = await loadImageBySize(imageDefinitionOrId, 600, 600); // 600x600 if (imageLoadedBySize) { console.log({ width: imageLoadedBySize.width, height: imageLoadedBySize.height, image: imageLoadedBySize.image, }); } ``` If want to dynamically listen to the _loaded_ resolution that best fits the wanted width and height, you can use the `subscribe` and the `highestResAvailable` function. ```tsx import { highestResAvailable } from "jazz-tools/media"; const progressiveImage = await ImageDefinition.load(imageId); if (!progressiveImage.$isLoaded) { throw new Error("Image not loaded"); } const img = document.createElement("img"); img.width = 600; img.height = 600; // start with the placeholder if (progressiveImage.placeholderDataURL) { img.src = progressiveImage.placeholderDataURL; } // then listen to the image changes progressiveImage.$jazz.subscribe({}, (image) => { const bestImage = highestResAvailable(image, 600, 600); if (bestImage) { // bestImage is again a FileStream const blob = bestImage.image.toBlob(); if (blob) { const url = URL.createObjectURL(blob); img.src = url; img.onload = () => URL.revokeObjectURL(url); } } }); ``` ## Custom image manipulation implementations To manipulate images (like placeholders, resizing, etc.), `createImage()` uses different implementations depending on the environment. On Expo, image manipulation is done using the `expo-image-manipulator` library. If you want to use a custom implementation, you can use the `createImageFactory` function in order create your own `createImage` function and use your preferred image manipulation library. ```tsx import { createImageFactory } from "jazz-tools/media"; const customCreateImage = createImageFactory({ createFileStreamFromSource: async (source, owner) => { // ... }, getImageSize: async (image) => { // ... }, getPlaceholderBase64: async (image) => { // ... }, resize: async (image, width, height) => { // ... }, }); ``` ## Best Practices * **Set image sizes** when possible to avoid layout shifts * **Use placeholders** (like LQIP - Low Quality Image Placeholders) for instant rendering * **Prioritize loading** the resolution appropriate for the current viewport * **Consider device pixel ratio** (window.devicePixelRatio) for high-DPI displays * **Always call URL.revokeObjectURL** after the image loads to prevent memory leaks ### Connecting CoValues # Connecting CoValues with direct linking CoValues can form relationships with each other by **linking directly to other CoValues**. This creates a powerful connection where one CoValue can point to the unique identity of another. Instead of embedding all the details of one CoValue directly within another, you use its Jazz-Tools schema as the field type. This allows multiple CoValues to point to the same piece of data effortlessly. ```ts import { co, z, Loaded, Group, Account } from "jazz-tools"; export const Location = co.map({ city: z.string(), country: z.string(), }); export type Location = co.loaded; // co.ref can be used within CoMap fields to point to other CoValues const Actor = co.map({ name: z.string, imageURL: z.string, birthplace: Location, // Links directly to the Location CoMap above. }); export type Actor = co.loaded; // actual actor data is stored in the separate Actor CoValue const Movie = co.map({ title: z.string, director: z.string, cast: co.list(Actor), // ordered, mutable }); export type Movie = co.loaded; // A User CoMap can maintain a CoFeed of co.ref(Movie) to track their favorite movies const User = co.map({ username: z.string, favoriteMovies: co.feed(Movie), // append-only }); export type User = co.loaded; ``` ### Understanding CoList and CoFeed * CoList is a collaborative list where each item is a reference to a CoValue * CoFeed contains an append-only list of references to CoValues. This direct linking approach offers a single source of truth. When you update a referenced CoValue, all other CoValues that point to it are automatically updated, ensuring data consistency across your application. By connecting CoValues through these direct references, you can build robust and collaborative applications where data is consistent, efficient to manage, and relationships are clearly defined. The ability to link different CoValue types to the same underlying data is fundamental to building complex applications with Jazz. ### 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/core-concepts/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 import { co, z } from "jazz-tools"; 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 // ...somewhere in jazz-tools itself... const Account = co.account({ root: co.map({}), profile: co.profile(), }); ``` If you want to keep the default `co.profile()` schema, but customise your account's private `root`, you can use `co.profile()` without options. 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({...})`. A `co.profile({...})` is a [type of CoMap](/docs/core-concepts/covalues/comaps), so you can add fields in the same way: ```ts export const MyAppProfile = co.profile({ name: z.string(), // compatible with default Profile schema avatar: co.optional(co.image()), }); export const MyAppAccountWithProfile = co.account({ root: MyAppRoot, profile: MyAppProfile, }); ``` **Info:** When using custom profile schemas, you need to take care of initializing the `profile` field in a migration, and set up the correct permissions for it. See [Adding/changing fields to root and profile](#addingchanging-fields-to-root-and-profile). ## Resolving CoValues starting at `profile` or `root` To use per-user data in your app, you typically use `useAccount` with your custom Account schema and specify which references to resolve using a resolve query (see [Subscribing & deep loading](/docs/core-concepts/subscription-and-loading)). Jazz will deduplicate loads, so you can safely use `useAccount` multiple times throughout your app without any performance overhead to ensure each component has exactly the data it needs. ```tsx import { View, Text } from "react-native"; export default function DashboardPage() { const me = useAccount(MyAppAccount, { resolve: { profile: true, root: { myChats: { $each: true } } }, }); if (!me.$isLoaded) return Loading...; return ( Logged in as {me.profile.name} {me.root.myChats.map((chat) => ( ))} ); } ``` ## 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 export const MyAppAccountWithMigration = co .account({ root: MyAppRoot, profile: MyAppProfile, }) .withMigration((account, creationProps?: { name: string }) => { // we use has to check if the root has ever been set if (!account.$jazz.has("root")) { account.$jazz.set("root", { myChats: [], }); } if (!account.$jazz.has("profile")) { const profileGroup = Group.create(); // Unlike the root, we want the profile to be publicly readable. profileGroup.makePublic(); account.$jazz.set( "profile", MyAppProfile.create( { name: creationProps?.name ?? "New user", }, 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 `$jazz.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 const MyAppRoot = co.map({ myChats: co.list(Chat), myBookmarks: co.optional(co.list(Bookmark)), // [!code ++:1] }); export const MyAppAccount = co .account({ root: MyAppRoot, profile: MyAppProfile, }) .withMigration(async (account) => { if (!account.$jazz.has("root")) { account.$jazz.set("root", { myChats: [], }); } // We need to load the root field to check for the myBookmarks field const { root } = await account.$jazz.ensureLoaded({ resolve: { root: true }, }); if (!root.$jazz.has("myBookmarks")) { // [!code ++:3] root.$jazz.set( "myBookmarks", co.list(Bookmark).create([], Group.create()), ); } }); ``` ### Guidance on building robust schemas Once you've published a schema, you should only ever add fields to it. This is because you have no way of ensuring that a new schema is distributed to all clients, especially if you're building a local-first app. You should plan to be able to handle data from users using any former schema version that you have published for your app. ### Schema Unions # Schema Unions Schema unions allow you to create types that can be one of several different schemas, similar to TypeScript union types. They use a discriminator field to determine which specific schema an instance represents at runtime, enabling type-safe polymorphism in your Jazz applications. The following operations are not available in schema unions: * `$jazz.ensureLoaded` — use the union schema's `load` method, or narrow the type first * `$jazz.subscribe` — use the union schema's `subscribe` method * `$jazz.set` — use `$jazz.applyDiff` ## Creating schema unions Schema unions are defined with `co.discriminatedUnion()` by providing an array of schemas and a discriminator field. The discriminator field must be a `z.literal()`. ```ts export const ButtonWidget = co.map({ type: z.literal("button"), label: z.string(), }); export const SliderWidget = co.map({ type: z.literal("slider"), min: z.number(), max: z.number(), }); export const WidgetUnion = co.discriminatedUnion("type", [ ButtonWidget, SliderWidget, ]); ``` To instantiate a schema union, just use the `create` method of one of the member schemas: ```ts const dashboard = Dashboard.create({ widgets: [ ButtonWidget.create({ type: "button", label: "Click me" }), SliderWidget.create({ type: "slider", min: 0, max: 100 }), ], }); ``` You can also use plain JSON objects, and let Jazz infer the concrete type from the discriminator field: ```ts const dashboardFromJSON = Dashboard.create({ widgets: [ { type: "button", label: "Click me" }, { type: "slider", min: 0, max: 100 }, ], }); ``` ## Narrowing unions When working with schema unions, you can access any property that is common to all members of the union. To access properties specific to a particular union member, you need to narrow the type. You can do this using a [TypeScript type guard](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) on the discriminator field: ```ts dashboard.widgets.forEach((widget) => { if (widget.type === "button") { console.log(`Button: ${widget.label}`); } else if (widget.type === "slider") { console.log(`Slider: ${widget.min} to ${widget.max}`); } }); ``` ## Loading schema unions You can load an instance of a schema union using its ID, without having to know its concrete type: ```ts const widget = await WidgetUnion.load(widgetId); // Subscribe to updates const unsubscribe = WidgetUnion.subscribe(widgetId, {}, (widget) => { console.log("Widget updated:", widget); }); ``` ## Nested schema unions You can create complex hierarchies by nesting discriminated unions within other unions: ```ts // Define error types const BadRequestError = co.map({ status: z.literal("failed"), code: z.literal(400), message: z.string(), }); const UnauthorizedError = co.map({ status: z.literal("failed"), code: z.literal(401), message: z.string(), }); const InternalServerError = co.map({ status: z.literal("failed"), code: z.literal(500), message: z.string(), }); // Create a union of error types const ErrorResponse = co.discriminatedUnion("code", [ BadRequestError, UnauthorizedError, InternalServerError, ]); // Define success type const SuccessResponse = co.map({ status: z.literal("success"), data: z.string(), }); // Create a top-level union that includes the error union const ApiResponse = co.discriminatedUnion("status", [ SuccessResponse, ErrorResponse, ]); function handleResponse(response: co.loaded) { if (response.status === "success") { console.log("Success:", response.data); } else { // This is an error - narrow further by error code if (response.code === 400) { console.log("Bad request:", response.message); } else if (response.code === 401) { console.log("Unauthorized:", response.message); } else if (response.code === 500) { console.log("Server error:", response.message); } } } ``` ## Limitations with schema unions Schema unions have some limitations that you should be aware of. They are due to TypeScript behaviour with type unions: when the type members of the union have methods with generic parameters, TypeScript will not allow calling those methods on the union type. This affects some of the methods on the `$jazz` namespace. Note that these methods may still work at runtime, but their use is not recommended as you will lose type safety. ### `$jazz.ensureLoaded` and `$jazz.subscribe` require type narrowing The `$jazz.ensureLoaded` and `$jazz.subscribe` methods are not supported directly on a schema union unless you first narrow the type using the discriminator. ### Updating union fields You can't use `$jazz.set` to modify a schema union's fields (even if the field is present in all the union members). Use `$jazz.applyDiff` instead. ### Codecs # Codecs You can use Zod `z.codec()` schemas to store arbitrary data types such as class instances within CoValues by defining custom encoders. This allows you to directly use these data types within CoValues without having to do an extra manual conversion step. ## Using Zod codecs To use a Zod `z.codec()` with Jazz, your encoder must encode the data into a JSON-compatible format. This is means that the `Input` type shall map to the JSON-compatible type, and `Output` will map to your custom type. ```ts class Greeter { constructor(public name: string) {} greet() { console.log(`Hello, ${this.name}!`); } } const schema = co.map({ greeter: z.codec(z.string(), z.z.instanceof(Greeter), { encode: (value) => value.name, decode: (value) => new Greeter(value), }), }); const porter = schema.create({ greeter: new Greeter("Alice"), }); porter.greeter.greet(); ``` **Info:** Schemas that are not directly supported by Jazz such as `z.instanceof` are not re-exported by Jazz under the `z` object. The full Zod API is exported under `z.z` if you need to use any of these schemas as part of a codec. ### Subscriptions & Deep Loading ### Sync and storage # Sync and storage: Jazz Cloud or self-hosted For sync and storage, you can either use Jazz Cloud for zero-config magic, or run your own sync server. ## Using Jazz Cloud Sign up for a free API key at [dashboard.jazz.tools](https://dashboard.jazz.tools) for higher limits or production use, or use your email address as a temporary key to get started quickly. ```bash VITE_JAZZ_API_KEY="you@example.com" # or your API key ``` Replace the API key in the Jazz provider sync server URL with your API key: ```tsx export function MyExpoApp({ children }: { children: React.ReactNode }) { // Get a free API Key at dashboard.jazz.tools, or use your email as a temporary key. const apiKey = "you@example.com"; return ( {children} ); } ``` Jazz Cloud will * sync CoValues in real-time between users and devices * safely persist CoValues on redundant storage nodes with additional backups * make use of geographically distributed cache nodes for low latency ### Free public alpha * Jazz Cloud is free during the public alpha, with no strict usage limits * We plan to keep a free tier, so you'll always be able to get started with zero setup * See [Jazz Cloud pricing](/cloud#pricing) for more details ## Self-hosting your sync server You can run your own sync server using: ```sh npx jazz-run sync ``` And then use `ws://localhost:4200` as the sync server URL. You can also run this simple sync server behind a proxy that supports WebSockets, for example to provide TLS. In this case, provide the WebSocket endpoint your proxy exposes as the sync server URL. **Info:** Requires at least Node.js v20\. See our [Troubleshooting Guide](/docs/troubleshooting) for quick fixes. ### Command line options: * `--host` / `-h` \- the host to run the sync server on. Defaults to 127.0.0.1. * `--port` / `-p` \- the port to run the sync server on. Defaults to 4200. * `--in-memory` \- keep CoValues in-memory only and do sync only, no persistence. Persistence is enabled by default. * `--db` \- the path to the file where to store the data (SQLite). Defaults to `sync-db/storage.db`. ### Source code The implementation of this simple sync server is available open-source [on GitHub](https://github.com/garden-co/jazz/blob/main/packages/jazz-run/src/startSyncServer.ts). ## Key Features ### 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](/docs/key-features/authentication/passkey), [passphrases](/docs/key-features/authentication/passphrase), or third-party authentications, such as [Clerk](/docs/key-features/authentication/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](/docs/key-features/authentication/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](/docs/key-features/authentication/authentication-states#migrating-data-from-anonymous-to-authenticated-account). ## Available Authentication Methods Jazz provides several ways to authenticate users: * [**Passkeys**](/docs/key-features/authentication/passkey): Secure, biometric authentication using WebAuthn * [**Passphrases**](/docs/key-features/authentication/passphrase): Bitcoin-style word phrases that users store * [**Clerk Integration**](/docs/key-features/authentication/clerk): Third-party authentication service with OAuth support * [**Better Auth**](/docs/key-features/authentication/better-auth): Self-hosted authentication service **Note**: For serverless authentication methods (passkey, passphrase), Jazz stores your account's credentials in your browser's local storage. This avoids needing to reauthenticate on every page load, but means you must take extra care to avoid [XSS attacks](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/XSS). In particular, you should take care to [sanitise user input](https://github.com/cure53/DOMPurify), set [appropriate CSP headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP), and avoid third-party JavaScript wherever possible. ### Quickstart # Add Authentication to your App This guide will show you how you can access your data on multiple devices by signing in to your app. **Info:** If you haven't gone through the [front-end Quickstart](/docs/quickstart), you might find this guide a bit confusing, as it continues from there. If you're looking for a quick reference, you might find [this page](/docs/key-features/authentication/overview) or our [Passkey Auth example app](https://github.com/gardencmp/jazz/tree/main/starters/react-passkey-auth) more helpful! ## Add passkey authentication ```ts const signUpForm = document.createElement("form"); const nameInput = Object.assign(document.createElement("input"), { placeholder: "Name", required: true, }); const signInButton = Object.assign(document.createElement("button"), { type: "button", innerText: "Sign In", onclick: async (evt: MouseEvent) => { evt.preventDefault(); await auth.logIn(); window.location.href = "/"; }, }); const signUpButton = Object.assign(document.createElement("button"), { type: "submit", innerText: "Sign Up", onclick: async (evt: MouseEvent) => { evt.preventDefault(); await auth.signUp(nameInput.value); window.location.href = "/"; }, }); ``` ## Give it a go! ... what, already?! Yes! Run your app and try creating a passkey and logging in! ```bash npm run dev ``` ### Not working? * Are you running your app in a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure%5FContexts) (either HTTPS or localhost)? **Info: Still stuck?** Ask for help on [Discord](https://discord.gg/utDMjHYg42)! ## Add a recovery method Passkeys are very convenient for your users because they offer a secure alternative to traditional authentication methods and they're normally synchronised across devices automatically by the user's browser or operating system. However, they're not available everywhere, and in case the user loses or deletes their passkey by mistake, they won't be able to access their account. So, let's add a secondary login method using a passphrase. You can integrate [as many different authentication methods as you like](https://github.com/garden-co/jazz/tree/main/examples/multiauth) in your app. ### Create an `Auth` component ```ts import { BrowserPasskeyAuth, JazzBrowserContextManager, } from "jazz-tools/browser"; import { JazzFestAccount } from "./schema"; export const AuthComponent = ( contextManager: JazzBrowserContextManager ) => { const ctx = contextManager.getCurrentValue(); if (!ctx) throw new Error("Context is not available"); const crypto = ctx.node.crypto; const authenticate = ctx.authenticate; const authSecretStorage = contextManager.getAuthSecretStorage(); const appName = "JazzFest"; const auth = new BrowserPasskeyAuth( crypto, authenticate, authSecretStorage, appName, ); const signUpForm = document.createElement("form"); const nameInput = Object.assign(document.createElement("input"), { placeholder: "Name", required: true, }); const signInButton = Object.assign(document.createElement("button"), { type: "button", innerText: "Sign In", onclick: async (evt: MouseEvent) => { evt.preventDefault(); await auth.logIn(); window.location.href = "/"; }, }); const signUpButton = Object.assign(document.createElement("button"), { type: "submit", innerText: "Sign Up", onclick: async (evt: MouseEvent) => { evt.preventDefault(); await auth.signUp(nameInput.value); window.location.href = "/"; }, }); signUpForm.append(nameInput, signInButton, signUpButton); if (!authSecretStorage.isAuthenticated) { return signUpForm; } return null; }; ``` ### Use your new component ```ts import { AuthComponent } from './AuthComponent'; // Your existing main.ts const authComponent = AuthComponent(contextManager); if (authComponent) { app.insertBefore(authComponent, app.firstChild); } ``` ### Show recovery key Jazz allows you to generate a passphrase from a wordlist which can be used to log in to an account. This passphrase will work regardless of how the account was originally created (passkey, Clerk, BetterAuth, etc.). Each account will always have the same recovery key. You can get started with a wordlist [from here](https://github.com/bitcoinjs/bip39/tree/master/src/wordlists). For example, you could save the `english.json` file in your project and format it as a JavaScript export. **File name: wordlist.ts** ```ts export const wordlist = [ "abandon", // ... many more words "zoo" ]; ``` We'll import this, and add a textarea into our auth component which will show the recovery key for the current user's account. ```ts // [!code ++:2] import { PassphraseAuth } from "jazz-tools"; import { wordlist } from "./wordlist"; import { BrowserPasskeyAuth, JazzBrowserContextManager, } from "jazz-tools/browser"; import { JazzFestAccount } from "./schema"; export const AuthComponent = ( contextManager: JazzBrowserContextManager ) => { const ctx = contextManager.getCurrentValue(); if (!ctx) throw new Error("Context is not available"); const crypto = ctx.node.crypto; const authenticate = ctx.authenticate; const register = ctx.register; const authSecretStorage = contextManager.getAuthSecretStorage(); const appName = "JazzFest"; const auth = new BrowserPasskeyAuth(crypto, authenticate, authSecretStorage, appName); // [!code ++:1] const passphraseAuth = new PassphraseAuth(crypto, authenticate, register, authSecretStorage, wordlist) const signUpForm = document.createElement("form"); const nameInput = Object.assign(document.createElement("input"), { placeholder: "Name", required: true, }); const signInButton = Object.assign(document.createElement("button"), { type: "button", innerText: "Sign In", onclick: async (evt: MouseEvent) => { evt.preventDefault(); await auth.logIn(); window.location.href = "/"; }, }); const signUpButton = Object.assign(document.createElement("button"), { type: "submit", innerText: "Sign Up", onclick: async (evt: MouseEvent) => { evt.preventDefault(); await auth.signUp(nameInput.value); window.location.href = "/"; }, }); // [!code ++:4] const passphraseDisplay = Object.assign(document.createElement("textarea"), { rows: 5, }); // [!code ++:3] passphraseAuth.getCurrentAccountPassphrase().then((passphrase) => { passphraseDisplay.value = passphrase; }); // [!code ++:1] signUpForm.append(nameInput, signInButton, signUpButton, passphraseDisplay); if (!authSecretStorage.isAuthenticated) { return signUpForm; } return null; }; ``` **Warning: Security Warning** This 'recovery key' is a method of authenticating into an account, and if compromised, it _cannot_ be changed! You should impress on your users the importance of keeping this key secret. ### Allow users to log in with the recovery key Now you're displaying a recovery key to users, so we'll allow users to login using a saved recovery key by extending the Auth component a little further. ```ts // [!code ++:2] import { PassphraseAuth } from "jazz-tools"; import { wordlist } from "./wordlist"; import { BrowserPasskeyAuth, JazzBrowserContextManager, } from "jazz-tools/browser"; import { JazzFestAccount } from "./schema"; export const AuthComponent = ( contextManager: JazzBrowserContextManager ) => { const ctx = contextManager.getCurrentValue(); if (!ctx) throw new Error("Context is not available"); const crypto = ctx.node.crypto; const authenticate = ctx.authenticate; const register = ctx.register; const authSecretStorage = contextManager.getAuthSecretStorage(); const appName = "JazzFest"; const auth = new BrowserPasskeyAuth(crypto, authenticate, authSecretStorage, appName); const passphraseAuth = new PassphraseAuth(crypto, authenticate, register, authSecretStorage, wordlist) const signUpForm = document.createElement("form"); const nameInput = Object.assign(document.createElement("input"), { placeholder: "Name", required: true, }); const signInButton = Object.assign(document.createElement("button"), { type: "button", innerText: "Sign In", onclick: async (evt: MouseEvent) => { evt.preventDefault(); await auth.logIn(); window.location.href = "/"; }, }); const signUpButton = Object.assign(document.createElement("button"), { type: "submit", innerText: "Sign Up", onclick: async (evt: MouseEvent) => { evt.preventDefault(); await auth.signUp(nameInput.value); window.location.href = "/"; }, }); const passphraseDisplay = Object.assign(document.createElement("textarea"), { rows: 5, }); passphraseAuth.getCurrentAccountPassphrase().then((passphrase) => { passphraseDisplay.value = passphrase; }); // [!code ++:10] const signInWithPassPhraseButton = Object.assign(document.createElement("button"), { type: "button", innerText: "Sign In With Passphrase", onclick: async (evt: MouseEvent) => { evt.preventDefault(); await passphraseAuth.logIn(passphraseDisplay.value); window.location.href = "/"; }, }); signUpForm.append(nameInput, signInButton, signUpButton, passphraseDisplay, signInWithPassPhraseButton); if (!authSecretStorage.isAuthenticated) { return signUpForm; } return null; }; ``` **Info: Tip** Although we're presenting this as a 'recovery key' here, this key could also be used as the primary method of authenticating users into your app. You could even completely remove passkey support if you wanted. **Congratulations! 🎉** You've added authentication to your app, allowing your users to log in from multiple devices, and you've added a recovery method, allowing users to make sure they never lose access to their account. ## Next steps * Check out how to [use other types of authentication](/docs/key-features/authentication/overview#available-authentication-methods) * Learn more about [sharing and collaboration](/docs/permissions-and-sharing/quickstart) * Find out how to [use server workers](/docs/server-side/quickstart) to build more complex applications ### 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 `useAgent` and `useIsAuthenticated`. ```tsx import { useAgent, useIsAuthenticated } from "jazz-tools/expo"; function AuthStateIndicator() { const agent = useAgent(); const isAuthenticated = useIsAuthenticated(); // Check if guest mode is enabled in JazzExpoProvider const isGuest = agent.$type$ !== "Account"; // Anonymous authentication: has an account but not fully authenticated const isAnonymous = agent.$type$ === "Account" && !isAuthenticated; return ( {isGuest && Guest Mode} {isAnonymous && Anonymous Account} {isAuthenticated && Authenticated} ); } ``` ## 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 export async function onAnonymousAccountDiscarded( anonymousAccount: MusicaAccount, ) { const { root: anonymousAccountRoot } = await anonymousAccount.$jazz.ensureLoaded({ resolve: { root: { rootPlaylist: { tracks: { $each: true, }, }, }, }, }); const me = await MusicaAccount.getMe().$jazz.ensureLoaded({ resolve: { root: { rootPlaylist: { tracks: true, }, }, }, }); for (const track of anonymousAccountRoot.rootPlaylist.tracks) { if (track.isExampleTrack) continue; const trackGroup = track.$jazz.owner; trackGroup.addMember(me, "admin"); me.root.rootPlaylist.tracks.$jazz.push(track); } } ``` To see how this works, try uploading a song in the [music player demo](https://music.demo.jazz.tools/) and then log in with an existing account. ## Provider Configuration for Authentication You can configure how authentication states work in your app with the [JazzReactProvider](/docs/project-setup/providers/). The provider offers several options that impact authentication behavior: * `guestMode`: Enable/disable Guest Mode * `onAnonymousAccountDiscarded`: Handle data migration when switching accounts * `sync.when`: Control when data synchronization happens * `defaultProfileName`: Set default name for new user profiles For detailed information on all provider options, see [Provider Configuration options](/docs/project-setup/providers/#additional-options). ## Controlling sync for different authentication states You can control network sync with [Providers](/docs/project-setup/providers/) based on authentication state: * `when: "always"`: Sync is enabled for both Anonymous Authentication and Authenticated Account * `when: "signedUp"`: Sync is enabled when the user is authenticated * `when: "never"`: Sync is disabled, content stays local ```tsx ``` ### 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 ``` **Warning: iOS Credential Persistence** When using `sync: 'never'` or `sync: 'signedUp'`, like all other data, the user's account exists only on their device, and is deleted if the user uninstalls your app. On iOS though, login credentials are saved to the Keychain, and are not deleted when the app is uninstalled. If a user reinstalls your app, Jazz will try to re-use these credentials to sign in to an account that no longer exists, which will cause errors. To avoid this, consider using `sync: 'always'` for your iOS users, or let them know they'll need to remove their credentials from Keychain before reinstalling. ### Configuring Guest Mode Access \[!framework=react,react-native,react-native-expo,svelte\] You can configure Guest Mode access with the `guestMode` prop for [Providers](/docs/project-setup/providers/). ```tsx ``` 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%5FAuthentication%5FAPI) 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: ```ts import { JazzBrowserContextManager, BrowserPasskeyAuth } from 'jazz-tools/browser'; const apiKey = import.meta.env.VITE_JAZZ_API_KEY; const contextManager = new JazzBrowserContextManager(); await contextManager.createContext({ sync: { peer: `wss://cloud.jazz.tools?key=${apiKey}` }, }); const ctx = contextManager.getCurrentValue(); if (!ctx) throw new Error("Context is not available"); const crypto = ctx.node.crypto; const authenticate = ctx.authenticate; const authSecretStorage = contextManager.getAuthSecretStorage(); const appName = "My Jazz App" const auth = new BrowserPasskeyAuth( crypto, authenticate, authSecretStorage, appName, ); // To register a new account, use auth.signUp(name: string) // To log in to an existing account with a passkey, use auth.signIn() ``` ## 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](/docs/key-features/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%5FAuthentication%5FAPI) ### 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. If you do decide to create your own wordlist, it's recommended to use at least 2048 unique words (or some higher power of two). ## 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 You can implement passphrase authentication in your application quickly and easily: ```tsx import { wordlist } from "./wordlist"; export function AuthModal({ open, onOpenChange }: AuthModalProps) { const [loginPassphrase, setLoginPassphrase] = useState(""); const auth = usePassphraseAuth({ // Must be inside the JazzProvider! 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 passphrase ); } ``` The `sendInboxMessage` API returns a Promise that waits for the message to be handled by a Worker. A message is considered to be handled when the Promise returned by `inbox.subscribe` resolves. The value returned will be the id of the CoValue returned in the `inbox.subscribe` resolved promise. ## Deployment considerations Multi-region deployments are not supported when using the Inbox API. If you need to split the workload across multiple regions, you can use the [HTTP API](/docs/server-side/communicating-with-workers/http-requests) instead. ### Server-side rendering # Add Server-Side Rendering to your App This guide will take your simple client-side app to the next level by showing you how to create a server-rendered page to publish your data to the world. **Info:** If you haven't gone through the [front-end Quickstart](/docs/quickstart), you might find this guide a bit confusing. If you're looking for a quick reference, you might find [this page](/docs/project-setup#ssr-integration) more helpful! ## Creating an agent For Jazz to access data on the server, we need to create an SSR agent, which is effectively a read-only user which can access public data stored in Jazz. We can create this user using the `createSSRJazzAgent` function. In this example, we'll create a new file and export the agent, which allows us to import and use the same agent in multiple pages. ```ts import { createSSRJazzAgent } from "jazz-tools/ssr"; export const jazzSSR = createSSRJazzAgent({ peer: "wss://cloud.jazz.tools/", }); ``` ## Telling Jazz to use the SSR agent Normally, Jazz expects a logged in user (or an anonymous user) to be accessing data. We can use the `enableSSR` setting to tell Jazz that this may not be the case, and the data on the page may be being accessed by an agent. ```tsx "use client"; import { JazzReactProvider } from "jazz-tools/react"; import { JazzFestAccount } from "./schema"; const apiKey = process.env.NEXT_PUBLIC_JAZZ_API_KEY; export function JazzWrapper({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` ## Making your data public By default, when you create data in Jazz, it's private and only accessible to the account that created it. However, the SSR agent is credential-less and unauthenticated, so it can only read data which has been made public. Although Jazz allows you to define [complex, role-based permissions](/docs/permissions-and-sharing/overview), here, we'll focus on making the CoValues public. **File name: app/schema.ts** ```ts import { co, z } from "jazz-tools"; export const Band = co .map({ name: z.string(), // Zod primitive type }) // [!code ++:3] .withMigration((band) => { band.$jazz.owner.makePublic(); }); export const Festival = co.list(Band); export const JazzFestAccountRoot = co.map({ myFestival: Festival, }); export const JazzFestAccount = co .account({ root: JazzFestAccountRoot, profile: co.profile(), }) .withMigration(async (account) => { if (!account.$jazz.has("root")) { account.$jazz.set("root", { myFestival: [], }); // [!code ++:8] if (account.root.$isLoaded) { const { myFestival } = await account.root.$jazz.ensureLoaded({ resolve: { myFestival: true, }, }); myFestival.$jazz.owner.makePublic(); } } }); ``` ## Creating a server-rendered page Now let's set up a page which will be read by the agent we created earlier, and rendered fully on the server. ```tsx import { jazzSSR } from "@/app/jazzSSR"; import { Festival } from "@/app/schema"; export default async function ServerSidePage(props: { params: { festivalId: string }; }) { const { festivalId } = await props.params; const festival = await Festival.load(festivalId, { loadAs: jazzSSR, resolve: { $each: { $onError: "catch", }, }, }); return (

🎪 Server-rendered Festival {festivalId}

    {festival.$isLoaded && festival.map((band) => { if (!band.$isLoaded) return null; return
  • 🎶 {band.name}
  • ; })}
); } ``` ## Linking to your server-rendered page The last step is to link to your server-rendered page from your `Festival` component so that you can find it easily! ```tsx "use client"; import { useAccount } from "jazz-tools/react"; // [!code ++:1] import Link from "next/link"; import { JazzFestAccount } from "@/app/schema"; export function Festival() { const me = useAccount(JazzFestAccount, { resolve: { root: { myFestival: { $each: { $onError: "catch" } } } }, }); if (!me.$isLoaded) return null; return ( <>
    {me.root.myFestival.map((band) => { if (!band.$isLoaded) return null; return
  • {band.name}
  • ; })}
{/* [!code ++:3] */} Go to my Server-Rendered Festival Page! ); } ``` ## Start your app Let's fire up your app and see if it works! ```bash npm run dev ``` If everything's going according to plan, your app will load with the home page. You can click the link to your server-rendered page to see your data - fully rendered on the server! **Congratulations! 🎉** You've now set up server-side rendering in your React app. You can use this same pattern to render any page on the server. ### Not working? * Did you add `enableSSR` to the provider? * Did you add `loadAs: jazzSSR` to `Festival.load`? * Did you add the migrations to make the data public? **Info: Still stuck?** Ask for help on [Discord](https://discord.gg/utDMjHYg42)! ## Next steps * Learn more about how to [manage complex permissions](/docs/permissions-and-sharing/overview) using groups and roles * Dive deeper into the collaborative data structures we call [CoValues](/docs/core-concepts/covalues/overview) * Learn more about migrations in the [accounts and migrations docs](/docs/core-concepts/schemas/accounts-and-migrations) ## Project setup ### Providers ## Tooling & Resources ### create-jazz-app # create-jazz-app Jazz comes with a CLI tool that helps you quickly scaffold new Jazz applications. There are two main ways to get started: 1. **Starter templates** \- Pre-configured setups to start you off with your preferred framework 2. **Example apps** \- Extend one of our [example applications](https://jazz.tools/examples) to build your project ## Quick Start with Starter Templates Create a new Jazz app from a starter template in seconds: ```bash npx create-jazz-app@latest --api-key YOUR_API_KEY ``` **Info: Tip** Sign up for a free API key at [dashboard.jazz.tools](https://dashboard.jazz.tools) for higher limits or production use, or use your email address as a temporary key to get started quickly. ```bash VITE_JAZZ_API_KEY="you@example.com" # or your API key ``` This launches an interactive CLI that guides you through selecting: * Pre-configured frameworks and authentication methods (See [Available Starters](#available-starters)) * Package manager * Project name * Jazz Cloud API key (optional) - Provides seamless sync and storage for your app ## Command Line Options If you know what you want, you can specify options directly from the command line: ```bash # Basic usage with project name npx create-jazz-app@latest my-app --framework react --api-key YOUR_API_KEY # Specify a starter template npx create-jazz-app@latest my-app --starter react-passkey-auth --api-key YOUR_API_KEY # Specify example app npx create-jazz-app@latest my-app --example chat --api-key YOUR_API_KEY ``` ### Available Options * `directory` \- Directory to create the project in (defaults to project name) * `-f, --framework` \- Framework to use (React, React Native, Svelte) * `-s, --starter` \- Starter template to use * `-e, --example` \- Example project to use * `-p, --package-manager` \- Package manager to use (npm, yarn, pnpm, bun, deno) * `-k, --api-key` \- Jazz Cloud API key (during our [free public alpha](/docs/core-concepts/sync-and-storage#free-public-alpha), you can use your email as the API key) * `-h, --help` \- Display help information ## Start From an Example App Want to start from one of [our example apps](https://jazz.tools/examples)? Our example apps include specific examples of features and use cases. They demonstrate real-world patterns for building with Jazz. Use one as your starting point: ```bash npx create-jazz-app@latest --example chat ``` ## Available Starters Starter templates are minimal setups that include the basic configuration needed to get started with Jazz. They're perfect when you want a clean slate to build on. Choose from these ready-to-use starter templates: * `react-passkey-auth` \- React with Passkey authentication (easiest to start with) * `react-clerk-auth` \- React with Clerk authentication * `svelte-passkey-auth` \- Svelte with Passkey authentication * `rn-clerk-auth` \- React Native with Clerk authentication Run `npx create-jazz-app --help` to see the latest list of available starters. ## What Happens Behind the Scenes When you run `create-jazz-app`, we'll: 1. Ask for your preferences (or use your command line arguments) 2. Clone the appropriate starter template 3. Update dependencies to their latest versions 4. Install all required packages 5. Set up your project and show next steps ## Requirements * Node.js 20.0.0 or later * Your preferred package manager (npm, yarn, pnpm, bun, or deno) ### Inspector # Jazz Inspector [Jazz Inspector](https://inspector.jazz.tools) is a tool to visually inspect a Jazz account or other CoValues. To pass your account credentials, go to your Jazz app, copy the full JSON from the `jazz-logged-in-secret` local storage key, and paste it into the Inspector's Account ID field. Alternatively, you can pass the Account ID and Account Secret separately. ### AI tools (llms.txt) # Using AI to build Jazz apps AI tools, particularly large language models (LLMs), can accelerate your development with Jazz. Searching docs, responding to questions and even helping you write code are all things that LLMs are starting to get good at. However, Jazz is a rapidly evolving framework, so sometimes AI might get things a little wrong. To help the LLMs, we provide the Jazz documentation in a txt file that is optimized for use with AI tools, like Cursor. [llms-full.txt](/react-native-expo/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. ![ChatGPT prompt with llms-full.txt attached](/chatgpt-with-llms-full-txt.jpg) ### Cursor 1. Go to Settings > Cursor Settings > Features > Docs 2. Click "Add new doc" 3. Enter the following URL: ``` https://jazz.tools/llms-full.txt ``` ## llms.txt convention We follow the llms.txt [proposed standard](https://llmstxt.org/) for providing documentation to AI tools at inference time that helps them understand the context of the code you're writing. ## Limitations and considerations AI is amazing, but it's not perfect. What works well this week could break next week (or be twice as good). We're keen to keep up with changes in tooling to help support you building the best apps, but if you need help from humans (or you have issues getting set up), please let us know on [Discord](https://discord.gg/utDMjHYg42). ### FAQs # Frequently Asked Questions ## How established is Jazz? Jazz is backed by fantastic angel and institutional investors with experience and know-how in devtools and has been in development since 2020. ## Will Jazz be around long-term? We're committed to Jazz being around for a long time! We understand that when you choose Jazz for your projects, you're investing time and making a significant architectural choice, and we take that responsibility seriously. That's why we've designed Jazz with longevity in mind from the start: * The open source nature of our sync server means you'll always be able to run your own infrastructure * Your data remains accessible even if our cloud services change * We're designing the protocol as an open specification This approach creates a foundation that can continue regardless of any single company's involvement. The local-first architecture means your apps will always work, even offline, and your data remains yours. ## How secure is my data? Jazz encrypts all your data by default using modern cryptographic standards. Every transaction is cryptographically signed, and data is encrypted using industry-standard algorithms including BLAKE3 hashing, Ed25519 signatures, and XSalsa20 stream ciphers. Key features of Jazz's security: * **Privacy by default**: Your data is encrypted even on Jazz Cloud servers * **Automatic key rotation**: When members are removed from Groups, encryption keys rotate automatically * **Verifiable authenticity**: Every change is cryptographically signed * **Zero-trust architecture**: Only people you explicitly grant access can read your data For technical details, see our [encryption documentation](/docs/reference/encryption). ## Does Jazz use Non-standard cryptography? Jazz uses BLAKE3, XSalsa20, and Ed25519, which are all widely published and publicly reviewed standard cryptographic algorithms. Although we're not lawyers, and so can't give legal advice, we believe that Jazz does not use 'Non-standard cryptography' as defined in the [BIS requirements](https://www.ecfr.gov/current/title-15/subtitle-B/chapter-VII/subchapter-C/part-772#p-772.1%28Non-standard%20cryptography%29) and therefore the requirements for publishing Jazz apps in the Apple App Store. ### Encryption # Encryption Jazz uses proven cryptographic primitives in a novel, but simple protocol to implement auditable permissions while allowing real-time collaboration and offline editing. ## How encryption works Jazz uses proven cryptographic primitives in a novel, but simple protocol to implement auditable permissions while allowing real-time collaboration and offline editing. ### Write permissions: Signing with your keys When you create or modify CoValues, Jazz cryptographically signs every transaction: * All transactions are signed with your account's signing keypair * This proves the transaction came from you * Whether transactions are valid depends on your permissions in the Group that owns the CoValue * Groups have internal logic ensuring only admins can change roles or create invites * You can add yourself to a Group only with a specific role via invites ### Read permissions: Symmetric encryption Groups use a shared "read key" for encrypting data: * Admins reveal this symmetric encryption key to accounts with "reader" role or higher * All transactions in CoValues owned by that Group are encrypted with the current read key * When someone is removed from a Group, the read key rotates and gets revealed to all remaining members * CoValues start using the new read key for future transactions This means removed members can't read new data, but existing data they already had access to remains readable to them. ## Key rotation and security Jazz automatically handles key management: * **Member removal triggers rotation**: When you remove someone from a Group, Jazz generates a new read key * **Seamless transition**: New transactions use the new key immediately * **No data loss**: Existing members get the new key automatically ## Streaming encryption Jazz encrypts data efficiently for real-time collaboration: * **Incremental hashing**: CoValue sessions use [BLAKE3](https://github.com/BLAKE3-team/BLAKE3) for append-only hashing * **Session signatures**: Each session is signed with [Ed25519](https://ed25519.cr.yp.to/) after each transaction * **Stream ciphers**: Data is encrypted using [XSalsa20](https://cr.yp.to/salsa20.html) stream cipher * **Integrity protection**: Hashing and signing ensure data hasn't been tampered with Although we're not lawyers, and so can't give legal advice, the encryption algorithms used in Jazz are widely published. As a result, we believe that Jazz does not use 'Non-standard cryptography' per the [BIS requirements](https://www.ecfr.gov/current/title-15/subtitle-B/chapter-VII/subchapter-C/part-772#p-772.1%28Non-standard%20cryptography%29) (and therefore the requirements for publishing Jazz apps in the Apple App Store). ## Content addressing CoValue IDs are the [BLAKE3](https://github.com/BLAKE3-team/BLAKE3) hash of their immutable "header" (containing CoValue type and owning group). This allows CoValues to be "content addressed" while remaining dynamic and changeable. ## What this means for you **Privacy by default**: Your data is always encrypted, even on Jazz Cloud servers. Only people you explicitly give access to can read your data. **Flexible permissions**: Use Groups to control exactly who can read, write, or admin your CoValues. **Automatic security**: Key rotation and encryption happen behind the scenes - you don't need to think about it. **Verifiable authenticity**: Every change is cryptographically signed, so you always know who made what changes. ## Further reading * [BLAKE3](https://github.com/BLAKE3-team/BLAKE3) \- append-only hashing * [Ed25519](https://ed25519.cr.yp.to/) \- signature scheme * [XSalsa20](https://cr.yp.to/salsa20.html) \- stream cipher for data encryption ### Implementation details The cryptographic primitives are implemented in the [cojson/src/crypto](https://github.com/garden-co/jazz/tree/main/packages/cojson/src/crypto) package. Key files to explore: * [permissions.ts](https://github.com/garden-co/jazz/blob/main/packages/cojson/src/permissions.ts) \- Permission logic * [permissions.test.ts](https://github.com/garden-co/jazz/blob/main/packages/cojson/src/tests/permissions.test.ts) \- Permission tests * [verifiedState.ts](https://github.com/garden-co/jazz/blob/main/packages/cojson/src/coValueCore/verifiedState.ts) \- State verification * [coValueCore.test.ts](https://github.com/garden-co/jazz/blob/main/packages/cojson/src/tests/coValueCore.test.ts) \- Core functionality tests ### Testing # Testing Jazz Apps As you develop your Jazz app, you might find yourself needing to test functionality relating to sync, identities, and offline behaviour. The `jazz-tools/testing` utilities provide helpers to enable you to do so. ## Core test helpers Jazz provides some key helpers that you can use to simplify writing complex tests for your app's functionality. ### `setupJazzTestSync` This should normally be the first thing you call in your test setup, for example in a `beforeEach` or `beforeAll` block. This function sets up an in-memory sync node for the test session, which is needed in case you want to test data synchronisation functionality. Test data is not persisted, and no clean-up is needed between test runs. ```ts import { co, z } from "jazz-tools"; import { beforeEach, describe, expect, test } from "vitest"; import { createJazzTestAccount, runWithoutActiveAccount, setActiveAccount, setupJazzTestSync, } from "jazz-tools/testing"; const MyAccountSchema = co.account({ profile: co.profile(), root: co.map({}), }); describe("My app's tests", () => { beforeEach(async () => { await setupJazzTestSync(); }); test("I can create a test account", async () => { // See below for details on createJazzTestAccount() const account1 = await createJazzTestAccount({ AccountSchema: MyAccountSchema, isCurrentActiveAccount: true, }); expect(account1).not.toBeUndefined(); // ... }); }); ``` ### `createJazzTestAccount` After you've created the initial account using `setupJazzTestSync`, you'll typically want to create user accounts for running your tests. You can use `createJazzTestAccount()` to create an account and link it to the sync node. By default, this account will become the currently active account (effectively the 'logged in' account). You can use it like this: ```ts const account = await createJazzTestAccount({ AccountSchema: MyAccountSchema, isCurrentActiveAccount: true, creationProps: {}, }); ``` #### `AccountSchema` This option allows you to provide a custom account schema to the utility to be used when creating the account. The account will be created based on the schema, and all attached migrations will run. #### `isCurrentActiveAccount` This option (disabled by default) allows you to quickly switch to the newly created account when it is created. ```ts const account1 = await createJazzTestAccount({ isCurrentActiveAccount: true, }); const group1 = co.group().create(); // Group is owned by account1; const account2 = await createJazzTestAccount(); const group2 = co.group().create(); // Group is still owned by account1; ``` #### `creationProps` This option allows you to specify `creationProps` for the account which are used during the account creation (and passed to the migration function on creation). ## Managing active Accounts During your tests, you may need to manage the currently active account after account creation, or you may want to simulate behaviour where there is no currently active account. ### `setActiveAccount` Use `setActiveAccount()` to switch between active accounts during a test run. You can use this to test your app with multiple accounts. ```ts const account1 = await createJazzTestAccount({ isCurrentActiveAccount: true, }); const account2 = await createJazzTestAccount(); const group1 = co.group().create(); // Group is owned by account1; group1.addMember(account2, "reader"); const myMap = MyMap.create( { text: "Created by account1", }, { owner: group1 }, ); const myMapId = myMap.$jazz.id; setActiveAccount(account2); // myMap is still loaded as account1, so we need to load again as account2 const myMapFromAccount2 = await MyMap.load(myMapId); if (myMapFromAccount2.$isLoaded) { expect(myMapFromAccount2.text).toBe("Created by account1"); expect(() => myMapFromAccount2.$jazz.set("text", "Updated by account2"), ).toThrow(); } ``` ### `runWithoutActiveAccount` If you need to test how a particular piece of code behaves when run without an active account. ```ts const account1 = await createJazzTestAccount({ isCurrentActiveAccount: true, }); runWithoutActiveAccount(() => { expect(() => co.group().create()).toThrow(); // can't create new group }); ``` ## Managing Context To test UI components, you may need to create a mock Jazz context. ### Simulating connection state changes You can use `MockConnectionStatus.setIsConnected(isConnected: boolean)` to simulate disconnected and connected states (depending on whether `isConnected` is set to `true` or `false`). ## Next Steps You're ready to start writing your own tests for your Jazz apps now. For further details and reference, you can check how we do our testing below. * [Unit test examples](https://github.com/garden-co/jazz/tree/main/packages/jazz-tools/src/tools/tests) * [End-to-end examples](https://github.com/garden-co/jazz/tree/main/tests/e2e/tests) ### Performance tips # Tips for maximising Jazz performance ## Use the best crypto implementation for your platform The fastest implementations are (in order): 1. [Node-API crypto](/docs/server-side/setup#node-api) (only available in some Node/Deno environments) or [RNCrypto](/docs/project-setup/providers#react-native-crypto) and [RNQuickCrypto](/docs/react-native/project-setup/providers#quick-crypto) on React Native 2. [WASM crypto](/docs/server-side/setup#wasm-on-edge-runtimes) 3. JavaScript fallback (slowest, but most compatible) Check whether your environment supports Node-API. Some edge runtimes may not enable WASM by default. ## Minimise group extensions Group extensions make it easy to cascade permissions and they’re fast enough for most cases. However, performance can slow down when many parent groups need to load in the dependency chain. To avoid this, create and reuse groups manually when their permissions stay the same for both CoValues over time. **Note**: Implicit CoValue creation extends groups automatically. Be careful about how you create nested CoValues if you are likely to build long dependency chains. ```ts const SubSubItem = co.map({ name: z.string(), }); const SubItem = co.map({ subSubItem: SubSubItem, }); const Item = co.map({ subItem: SubItem, }); // Implicit CoValue creation // Results in Group extension for subItem and subSubItem's owners. const item = Item.create({ subItem: { subSubItem: { name: "Example", }, }, }); // Explicit CoValue creation // Does not result in Group extension. const fasterItem = Item.create({ subItem: SubItem.create({ subSubItem: SubSubItem.create({ name: "Example", }), }), }); // Alternative const subSubItem = SubSubItem.create({ name: "Example" }); const subItem = SubItem.create({ subSubItem: subSubItem }); const fasterItem = Item.create({ subItem: subItem }); ``` ## Choose simple datatypes where possible CoValues will always be slightly slower to load than their primitive counterparts. For most cases, this is negligible. In data-heavy apps where lots of data has to be loaded at the same time, you can choose to trade off some of the flexibility of CoValues for speed by opting for primitive data types. ### `z.string()` vs CoTexts In case you use a CoText, Jazz will enable character-by-character collaboration possibilities for you. However, in many cases, users do not expect to be able to collaborate on the text itself, and are happy with replacing the whole string at once, especially shorter strings. In this case, you could use a `z.string()` for better performance. Examples: * names * URLs * phone numbers ### `z.object()/z.tuple()` vs CoMaps CoMaps allow granular updates to objects based on individual keys. If you expect your whole object to be updated at once, you could consider using the `z.object()` or `z.tuple()` type. Note that if you use these methods, you must replace the whole value if you choose to update it. Examples: * locations/co-ordinates * data coming from external sources * data which is rarely changed after it is created ```ts const Sprite = co.map({ position: z.object({ x: z.number(), y: z.number() }), }); const Location = co.map({ position: z.tuple([z.number(), z.number()]), }); const mySprite = Sprite.create({ position: { x: 10, y: 10 } }); mySprite.$jazz.set("position", { x: 20, y: 20 }); // You cannot update 'x' and 'y' independently, only replace the whole object const myLocation = Location.create({ position: [26.052, -80.209] }); myLocation.$jazz.set("position", [-33.868, -63.987]); // Note: you cannot replace a single array element, only replace the whole tuple ``` ### Forms # How to write forms with Jazz This guide shows you a simple and powerful way to implement forms for creating and updating CoValues. [See the full example here.](https://github.com/garden-co/jazz/tree/main/examples/form) ## Updating a CoValue To update a CoValue, we simply assign the new value directly as changes happen. These changes are synced to the server. ```tsx order.$jazz.set("name", e.target.value)} />; ``` It's that simple! ## Creating a CoValue When creating a CoValue, we can use a partial version that allows us to build up the data before submitting. ### Using a Partial CoValue Let's say we have a CoValue called `BubbleTeaOrder`. We can create a partial version,`PartialBubbleTeaOrder`, which has some fields made optional so we can build up the data incrementally. **File name: schema.ts** ```ts import { co, z } from "jazz-tools"; export const BubbleTeaOrder = co.map({ name: z.string(), }); export type BubbleTeaOrder = co.loaded; export const PartialBubbleTeaOrder = BubbleTeaOrder.partial(); export type PartialBubbleTeaOrder = co.loaded; ``` ## Writing the components in React Let's write the form component that will be used for both create and update. ```tsx import { co } from "jazz-tools"; import { BubbleTeaOrder, PartialBubbleTeaOrder } from "./schema"; export function OrderForm({ order, onSave, }: { order: BubbleTeaOrder | PartialBubbleTeaOrder; onSave?: (e: React.FormEvent) => void; }) { return (
e.preventDefault())}> {onSave && }
); } ``` ### Writing the edit form To make the edit form, simply pass the `BubbleTeaOrder`. Changes are automatically saved as you type. ```tsx export function EditOrder(props: { id: string }) { const order = useCoState(BubbleTeaOrder, props.id); if (!order.$isLoaded) return; return ; } ``` ### Writing the create form For the create form, we need to: 1. Create a partial order. 2. Edit the partial order. 3. Convert the partial order to a "real" order on submit. Here's how that looks like: ```tsx export function CreateOrder(props: { id: string }) { const orders = useAccount(JazzAccount, { resolve: { root: { orders: true } }, select: (account) => (account.$isLoaded ? account.root.orders : undefined), }); const newOrder = useCoState(PartialBubbleTeaOrder, props.id); if (!newOrder.$isLoaded || !orders) return; const handleSave = (e: React.FormEvent) => { e.preventDefault(); // Convert to real order and add to the list // Note: the name field is marked as required in the form, so we can assume that has been set in this case // In a more complex form, you would need to validate the partial value before storing it orders.$jazz.push(newOrder as BubbleTeaOrder); }; return ; } ``` ## Editing with a save button If you need a save button for editing (rather than automatic saving), you can use Jazz's branching feature. The example app shows how to create a private branch for editing that can be merged back when the user saves: ```tsx import { Group } from "jazz-tools"; import { useState, useMemo } from "react"; export function EditOrderWithSave(props: { id: string }) { // Create a new group for the branch, so that every time we open the edit page, // we create a new private branch const owner = useMemo(() => Group.create(), []); const order = useCoState(BubbleTeaOrder, props.id, { resolve: { addOns: { $each: true, $onError: "catch" }, instructions: true, }, unstable_branch: { name: "edit-order", owner, }, }); function handleSave(e: React.FormEvent) { e.preventDefault(); if (!order.$isLoaded) return; // Merge the branch back to the original order.$jazz.unstable_merge(); // Navigate away or show success message } function handleCancel() { // Navigate away without saving - the branch will be discarded } if (!order.$isLoaded) return; return ; } ``` This approach creates a private branch using `unstable_branch` with a unique owner group. The user can edit the branch without affecting the original data, and changes are only persisted when they click save via `unstable_merge()`. **Info:** **Important:** Version control is currently unstable and we may ship breaking changes in patch releases. ## Handling different types of data Forms can be more complex than just a single string field, so we've put together an example app that shows you how to handle single-select, multi-select, date, boolean inputs, and rich text. [See the full example here.](https://github.com/garden-co/jazz/tree/main/examples/form) ### Organization/Team # How to share data between users through Organizations This guide shows you how to share a set of CoValues between users. Different apps have different names for this concept, such as "teams" or "workspaces". We'll use the term Organization. [See the full example here.](https://github.com/garden-co/jazz/tree/main/examples/organization) ## Defining the schema for an Organization Create a CoMap shared by the users of the same organization to act as a root (or "main database") for the shared data within an organization. For this example, users within an `Organization` will be sharing `Project`s. **File name: schema.ts** ```ts export const Project = co.map({ name: z.string(), }); export const Organization = co.map({ name: z.string(), // shared data between users of each organization projects: co.list(Project), }); export const ListOfOrganizations = co.list(Organization); ``` Learn more about [defining schemas](/docs/core-concepts/covalues/overview). ## Adding a list of Organizations to the user's Account Let's add the list of `Organization`s to the user's Account `root` so they can access them. ```tsx export const JazzAccountRoot = co.map({ organizations: co.list(Organization), }); export const JazzAccount = co .account({ root: JazzAccountRoot, profile: co.profile(), }) .withMigration((account) => { if (!account.$jazz.has("root")) { // Using a Group as an owner allows you to give access to other users const organizationGroup = Group.create(); const organizations = co.list(Organization).create([ // Create the first Organization so users can start right away Organization.create( { name: "My organization", projects: co.list(Project).create([], organizationGroup), }, organizationGroup, ), ]); account.$jazz.set("root", { organizations }); } }); ``` This schema now allows users to create `Organization`s and add `Project`s to them. [See the schema for the example app here.](https://github.com/garden-co/jazz/blob/main/examples/organization/src/schema.ts) ## Adding members to an Organization Here are different ways to add members to an `Organization`. * Send users an invite link. * [The user requests to join.](/docs/permissions-and-sharing/sharing#requesting-invites) This guide and the example app show you the first method. ### Adding members through invite links Here's how you can generate an [invite link](/docs/permissions-and-sharing/sharing#invites). When the user accepts the invite, add the `Organization` to the user's `organizations` list. ```ts import { useAcceptInviteNative } from "jazz-tools/expo"; useAcceptInviteNative({ invitedObjectSchema: Organization, onAccept: async (organizationID) => { const organization = await Organization.load(organizationID); if (!organization.$isLoaded) throw new Error("Organization could not be loaded"); me.root.organizations.$jazz.push(organization); // navigate to the organization page }, }); ``` ## Further reading * [Allowing users to request an invite to join a Group](/docs/permissions-and-sharing/sharing#requesting-invites) * [Groups as permission scopes](/docs/permissions-and-sharing/overview#adding-group-members-by-id) ### History Patterns # History Patterns Jazz's automatic history tracking enables powerful patterns for building collaborative features. Here's how to implement common history-based functionality. ## Audit Logs Build a complete audit trail showing all changes to your data: ```ts function getAuditLog(task: Task) { const changes: { field: string; value: Task[keyof Task] | undefined; by: Account | null; at: Date; }[] = []; // Collect edits for all fields const fields = Object.keys(task); const edits = task.$jazz.getEdits(); for (const field of fields) { const editField = field as keyof typeof edits; if (!edits[editField]) continue; for (const edit of edits[editField].all) { changes.push({ field, value: edit.value, by: edit.by, at: edit.madeAt, }); } } // Sort by timestamp (newest first) return changes.sort((a, b) => b.at.getTime() - a.at.getTime()); } // Use it to show change history const auditLog = getAuditLog(task); auditLog.forEach((entry) => { if (!entry.by?.profile?.$isLoaded) return; const when = entry.at.toLocaleString(); const who = entry.by.profile.name; const what = entry.field; const value = entry.value; console.log(`${when} - ${who} changed ${what} to "${value}"`); // 22/05/2025, 12:00:00 - Alice changed title to "New task" }); ``` ## Activity Feeds Show recent activity across your application: ```ts function getRecentActivity(projects: Project[], since: Date) { const activity: { project: string; field: string; value: Task[keyof Task] | undefined; by: Account | null; at: Date; }[] = []; for (const project of projects) { // Get all fields that might have edits const fields = Object.keys(project); // Check each field for edit history const edits = project.$jazz.getEdits(); for (const field of fields) { const editField = field as keyof typeof edits; // Skip if no edits exist for this field if (!edits[editField]) continue; for (const edit of edits[editField].all) { // Only include edits made after the 'since' date if (edit.madeAt > since) { activity.push({ project: project.name, field, value: edit.value, by: edit.by, at: edit.madeAt, }); } } } } return activity.sort((a, b) => b.at.getTime() - a.at.getTime()); } // Show activity from the last hour const hourAgo = new Date(Date.now() - 60 * 60 * 1000); const recentActivity = getRecentActivity(myProjects, hourAgo); // [{ // project: "New project", // field: "name", // value: "New project", // by: Account, // at: Date // }] ``` ## Change Indicators Show when something was last updated: ```ts function getLastUpdated(task: Task) { // Find the most recent edit across all fields let lastEdit: CoMapEdit | null = null; const edits = task.$jazz.getEdits(); for (const field of Object.keys(task)) { const editField = field as keyof typeof edits; // Skip if no edits exist for this field if (!edits[editField]) continue; const fieldEdit = edits[editField]; if (fieldEdit && (!lastEdit || fieldEdit.madeAt > lastEdit.madeAt)) { lastEdit = fieldEdit; } } if (!lastEdit || !lastEdit.by?.profile?.$isLoaded) return null; return { updatedBy: lastEdit.by.profile.name, updatedAt: lastEdit.madeAt, message: `Last updated by ${lastEdit.by.profile.name} at ${lastEdit.madeAt.toLocaleString()}`, }; } const lastUpdated = getLastUpdated(task); console.log(lastUpdated?.message); // "Last updated by Alice at 22/05/2025, 12:00:00" ``` ## Finding Specific Changes Query history for specific events: ```ts // Find when a task was completed function findCompletionTime(task: Task): Date | null { const statusEdits = task.$jazz.getEdits().status; if (!statusEdits) return null; // find() returns the FIRST completion time // If status toggles (completed → in-progress → completed), // this gives you the earliest completion, not the latest const completionEdit = statusEdits.all.find( (edit) => edit.value === "completed", ); return completionEdit?.madeAt || null; } // To get the LATEST completion time instead reverse the array, then find: function findLatestCompletionTime(task: Task): Date | null { const statusEdits = task.$jazz.getEdits().status; if (!statusEdits) return null; // Reverse and find (stops at first match) const latestCompletionEdit = statusEdits.all .slice() // Create copy to avoid mutating original .reverse() .find((edit) => edit.value === "completed"); return latestCompletionEdit?.madeAt || null; } console.log(findCompletionTime(task)); // First completion console.log(findLatestCompletionTime(task)); // Most recent completion // Find who made a specific change function findWhoChanged(task: Task, field: string, value: any) { const taskEdits = task.$jazz.getEdits(); const fieldEdits = taskEdits[field as keyof typeof taskEdits]; if (!fieldEdits) return null; const matchingEdit = fieldEdits.all.find((edit) => edit.value === value); return matchingEdit?.by || null; } const account = findWhoChanged(task, "status", "completed"); if (account?.profile?.$isLoaded) { console.log(account.profile.name); } // Alice ``` ## Further Reading * [History](/docs/key-features/history) \- Complete reference for the history API * [Subscription & Loading](/docs/core-concepts/subscription-and-loading) \- Ensure CoValues are loaded before accessing history ## Resources - [Documentation](https://jazz.tools/docs): Detailed documentation about Jazz - [Examples](https://jazz.tools/examples): Code examples and tutorials ## chat-rn-expo Example ### App.tsx ```tsx import { JazzExpoProvider } from "jazz-tools/expo"; import React, { StrictMode } from "react"; import { apiKey } from "./apiKey"; import ChatScreen from "./chat"; import { RNCrypto } from "jazz-tools/react-native-core/crypto/RNCrypto"; export default function App() { return ( ); } ``` ### apiKey.ts ```ts export const apiKey = "chat-rn-expo-example-jazz@garden.co"; ``` ### chat.tsx ```tsx import * as Clipboard from "expo-clipboard"; import { Account, getLoadedOrUndefined, Group } from "jazz-tools"; import { useEffect, useRef, useState } from "react"; import React, { Button, FlatList, KeyboardAvoidingView, SafeAreaView, Text, TextInput, TouchableOpacity, View, Alert, StyleSheet, Animated, Easing, } from "react-native"; import { useAccount, useCoState, useLogOut, Image } from "jazz-tools/expo"; import { Chat, Message } from "./schema"; import { createImage } from "jazz-tools/media"; import { launchImageLibrary } from "react-native-image-picker"; export default function ChatScreen() { const me = useAccount(Account, { resolve: { profile: true } }); const logOut = useLogOut(); const [chatId, setChatId] = useState(); const [chatIdInput, setChatIdInput] = useState(); const loadedChat = useCoState(Chat, chatId, { resolve: { $each: { text: true } }, }); const [message, setMessage] = useState(""); const [imageUploading, setImageUploading] = useState(false); const spinAnim = useRef(new Animated.Value(0)).current; useEffect(() => { let loop: Animated.CompositeAnimation | null = null; if (imageUploading) { const flip = Animated.sequence([ Animated.timing(spinAnim, { toValue: 1, duration: 400, // quick flip easing: Easing.inOut(Easing.ease), useNativeDriver: true, }), Animated.delay(700), // pause before next flip ]); loop = Animated.loop(flip); loop.start(); } else { spinAnim.stopAnimation(); spinAnim.setValue(0); } return () => { loop?.stop(); }; }, [imageUploading]); const spin = spinAnim.interpolate({ inputRange: [0, 1], outputRange: ["0deg", "180deg"], }); function handleLogOut() { setChatId(undefined); logOut(); } const createChat = () => { const group = Group.create(); group.addMember("everyone", "writer"); const chat = Chat.create([], group); setChatId(chat.$jazz.id); }; const joinChat = () => { if (chatIdInput) { if (chatIdInput.startsWith("https://chat.jazz.tools/#/chat/")) { setChatId(chatIdInput.split("/").pop()); } else { setChatId(chatIdInput); } } else { Alert.alert("Error", "Chat ID cannot be empty."); } }; const sendMessage = () => { if (!loadedChat.$isLoaded) return; if (message.trim()) { loadedChat.$jazz.push( Message.create({ text: message }, { owner: loadedChat?.$jazz.owner }), ); setMessage(""); } }; const sendPhoto = async () => { setImageUploading(true); try { if (!loadedChat.$isLoaded || !me.$isLoaded) throw new Error("Chat or user not loaded"); const result = await launchImageLibrary({ mediaType: "photo", quality: 0.8, }); if (!result.didCancel && result.assets?.[0].uri) { const image = await createImage(result.assets[0].uri, { owner: loadedChat.$jazz.owner, placeholder: "blur", maxSize: 1024, }); const thisMessage = Message.create( { text: message ? message.trim() : "", image, }, loadedChat.$jazz.owner, ); loadedChat.$jazz.push(thisMessage); setMessage(""); } } catch (error) { console.error(error); } finally { setImageUploading(false); } }; const renderMessageItem = ({ item }: { item: Message }) => { const isMe = item.$jazz.getEdits()?.text?.by?.isMe; const lastEdit = item.$jazz.getEdits()?.text; const lastEditor = lastEdit?.by?.profile; const lastEditorName = getLoadedOrUndefined(lastEditor)?.name; return ( {!isMe ? ( {lastEditorName} ) : null} {item.image && ( )} {item.text.toString()} {new Date(item.$jazz.createdAt).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", })} ); }; return ( {!loadedChat.$isLoaded ? ( Username { if (me.$isLoaded) { me.profile.$jazz.set("name", value); } }} textAlignVertical="center" onSubmitEditing={sendMessage} testID="username-input" /> Start new chat Join existing chat { setChatIdInput(value); }} textAlignVertical="center" onSubmitEditing={() => { if (chatIdInput) { setChatId(chatIdInput); } }} testID="chat-id-input" /> Join chat ) : ( <>