# Jazz (react-native) ## 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/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 NEXT_PUBLIC_JAZZ_API_KEY="you@example.com" # or your API key ``` ## Define your schema Jazz uses Zod for more simple data types (like strings, numbers, booleans), and its own schemas to create collaborative data structures known as CoValues. CoValues are automatically persisted across your devices and the cloud and synced in real-time. Here we're defining a schema made up of both Zod types and CoValues. Adding a `root` to the user's account gives us a container that can be used to keep a track of all the data a user might need to use the app. The migration runs when the user logs in, and ensures the account is properly set up before we try to use it. ```ts import { co, z } from "jazz-tools"; export const Band = co.map({ name: z.string(), // Zod primitive type }); export const Festival = co.list(Band); export const JazzFestAccountRoot = co.map({ myFestival: Festival, }); export const JazzFestAccount = co .account({ root: JazzFestAccountRoot, profile: co.profile(), }) .withMigration((account) => { if (!account.$jazz.has("root")) { account.$jazz.set("root", { myFestival: [], }); } }); ``` ## Add the Jazz Provider Wrap your app with a provider so components can use Jazz. ```tsx import { JazzWrapper } from "@/app/components/JazzWrapper"; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( {children} ); } ``` ## 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 Let's create a simple form to add a new band to the festival. We'll use the `useAccount` hook to get the current account and tell Jazz to load the `myFestival` CoValue by passing a `resolve` query. ```tsx "use client"; import { useAccount } from "jazz-tools/react"; import { JazzFestAccount } from "@/app/schema"; import { useState } from "react"; export function NewBand() { const me = useAccount(JazzFestAccount, { resolve: { root: { myFestival: true } }, }); const [name, setName] = useState(""); const handleSave = () => { if (!me.$isLoaded) return; me.root.myFestival.$jazz.push({ name }); setName(""); }; return (
setName(e.target.value)} />
); } ``` ## Display your data Now we've got a way to create data, so let's add a component to display it. ```tsx "use client"; import { useAccount } from "jazz-tools/react"; import { JazzFestAccount } from "@/app/schema"; export function Festival() { const me = useAccount(JazzFestAccount, { resolve: { root: { myFestival: { $each: true } } }, }); if (!me.$isLoaded) return null; return ( ); } ``` ## Put it all together You've built all your components, time to put them together. ```tsx import { Festival } from "@/app/components/Festival"; import { NewBand } from "@/app/components/NewBand"; export default function Home() { return (

πŸŽͺ My Festival

); } ``` You should now be able to add a band to your festival, and see it appear in the list! **Congratulations! πŸŽ‰** You've built your first Jazz app! You've begun to scratch the surface of what's possible with Jazz. Behind the scenes, your local-first JazzFest app is **already** securely syncing your data to the cloud in real-time, ready for you to build more and more powerful features. ## 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 `` is the core component that connects your React Native application to Jazz. It handles: * **Data Synchronization**: Manages connections to peers and the Jazz cloud * **Local Storage**: Persists data locally between app sessions * **Schema Types**: Provides APIs for the [AccountSchema](/docs/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 { JazzReactNativeProvider } from "jazz-tools/react-native"; 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 NEXT_PUBLIC_JAZZ_API_KEY="you@example.com" # or your API key ``` ## Provider Options ### Sync Options The `sync` property configures how your application connects to the Jazz network: ```ts import { type SyncConfig } from "jazz-tools"; export const syncConfig: SyncConfig = { // Connection to Jazz Cloud or your own sync server peer: `wss://cloud.jazz.tools/?key=${apiKey}`, // When to sync: "always" (default), "never", or "signedUp" when: "always", }; ``` 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 { JazzReactNativeProvider } from "jazz-tools/react-native"; import { MyAppAccount } from "./schema"; export function MyJazzProvider({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` ### Additional Options The provider accepts these additional options: * `kvStore` * `MMKVStoreAdapter` (default) * `AccountSchema` * `Account` (default) * `CryptoProvider` * `PureJSCrypto` (default) - Pure JavaScript crypto provider * `RNQuickCrypto` \- C++ accelerated crypto provider ## Authentication The Provider works with various authentication methods, with PassphraseAuth being the easiest way to get started for development and testing. For authentication details, refer to our [Authentication Overview](/docs/key-features/authentication/overview) guide. The authentication hooks must always be used inside the Provider component. Implementing PassphraseAuth is straightforward: 1. Import the [wordlist](https://github.com/bitcoinjs/bip39/tree/a7ecbfe2e60d0214ce17163d610cad9f7b23140c/src/wordlists) for generating recovery phrases 2. Use the `usePassphraseAuth` hook to handle authentication 3. Create simple registration and sign-in screens ```tsx import { JazzReactNativeProvider, usePassphraseAuth, } from "jazz-tools/react-native"; import { englishWordlist } from "./wordlist"; function JazzAuthentication({ children }: { children: ReactNode }) { const auth = usePassphraseAuth({ wordlist: englishWordlist, }); // If the user is already signed in, render the App if (auth.state === "signedIn") { return children; } // Otherwise, show a sign-in screen return ; } function AuthenticatedProvider({ children }: { children: ReactNode }) { return ( {children} ); } ``` ## Local Persistence \[!framework=react-native,expo\] Jazz for React Native includes built-in local persistence using SQLite. This implementation uses: * **Database Storage**: `@op-engineering/op-sqlite` \- A high-performance SQLite implementation * **Key-Value Storage**: `react-native-mmkv` \- A fast key-value storage system Local persistence is enabled by default with no additional configuration required. Your data will automatically persist across app restarts. ## Quick Crypto \[!framework=react-native,react-native-expo\] 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 { JazzReactNativeProvider } from "jazz-tools/react-native"; import { RNQuickCrypto } from "jazz-tools/react-native/crypto"; function MyJazzProvider({ children }: { children: ReactNode }) { return ( {children} ); } ``` For configuration, add the following settings: ```ruby # ios/Podfile ENV['SODIUM_ENABLED'] = '1' ``` and ```groovy // android/gradle.properties 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 Installation and Setup This guide covers setting up Jazz for React Native applications from scratch. If you're using Expo, please refer to the [React Native - Expo](/docs/react-native-expo/project-setup) guide instead. If you just want to get started quickly, you can use our [React Native Chat Demo](https://github.com/garden-co/jazz/tree/main/examples/chat-rn) as a starting point. Jazz supports the [New Architecture](https://reactnative.dev/architecture/landing-page) for React Native. Tested with: ```json "react-native": "0.79.2", "react": "18.3.1" ``` ## Installation ### Create a new project (Skip this step if you already have one) ```bash npx @react-native-community/cli init myjazzapp cd myjazzapp ``` If you intend to build for iOS, you can accept the invitation to install CocoaPods. If you decline, or you get an error, [you can install it with pod-install](#install-cocoapods). ### Install dependencies ```bash # React Native dependencies npm install @react-native-community/netinfo @bam.tech/react-native-image-resizer # React Native polyfills npm i -S @azure/core-asynciterator-polyfill react-native-url-polyfill readable-stream react-native-get-random-values @op-engineering/op-sqlite react-native-mmkv # Jazz dependencies npm i -S jazz-tools ``` **Info: Note** * Hermes has added support for `atob` and `btoa` in React Native 0.74\. If you are using earlier versions, you may also need to polyfill `atob` and `btoa` in your `package.json`. Packages to try include `text-encoding` and `base-64`, and you can drop `@bacons/text-decoder`. ### Configure Metro #### Regular repositories If you are not working within a monorepo, create a new file `metro.config.js` in the root of your project with the following content: ```ts // metro.config.js const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); const config = { resolver: { sourceExts: ["mjs", "js", "json", "ts", "tsx", "cjs"], requireCycleIgnorePatterns: [/(^|\/|\\)node_modules($|\/|\\)/] } }; module.exports = mergeConfig(getDefaultConfig(__dirname), config); ``` #### Monorepos For monorepos, use the following `metro.config.js`: ```ts // metro.config.js const path = require("path"); const { makeMetroConfig } = require("@rnx-kit/metro-config"); const MetroSymlinksResolver = require("@rnx-kit/metro-resolver-symlinks"); // Define workspace root const projectRoot = __dirname; const workspaceRoot = path.resolve(projectRoot, "../.."); // Add packages paths const extraNodeModules = { modules: path.resolve(workspaceRoot, "node_modules"), }; const watchFolders = [ path.resolve(workspaceRoot, "node_modules"), path.resolve(workspaceRoot, "packages"), ]; const nodeModulesPaths = [ path.resolve(projectRoot, "node_modules"), path.resolve(workspaceRoot, "node_modules"), ]; module.exports = makeMetroConfig({ resolver: { resolveRequest: MetroSymlinksResolver(), extraNodeModules, nodeModulesPaths, sourceExts: ["mjs", "js", "json", "ts", "tsx", "cjs"], }, watchFolders, }); ``` ### Additional monorepo configuration (for pnpm) * Add `node-linker=hoisted` to the root `.npmrc` (create this file if it doesn't exist). * Add the following to the root `package.json`: ```json // package.json "pnpm": { "peerDependencyRules": { "ignoreMissing": [ "@babel/*", "typescript" ] } } ``` ### Add polyfills Create a file `polyfills.js` at the project root with the following content: ```ts // @noErrors: 7016 // polyfills.js import { polyfillGlobal } from 'react-native/Libraries/Utilities/PolyfillFunctions'; import { ReadableStream } from "readable-stream"; polyfillGlobal("ReadableStream", () => ReadableStream); // polyfill ReadableStream import "@azure/core-asynciterator-polyfill"; // polyfill Async Iterator import "@bacons/text-decoder/install"; // polyfill Text Decoder import 'react-native-get-random-values'; // polyfill getRandomValues ``` Update `index.js`: ```ts import { AppRegistry } from 'react-native'; import App from './App'; import { name as appName } from './app.json'; import './polyfills'; AppRegistry.registerComponent(appName, () => App); ``` Lastly, ensure that the `"main"` field in your `package.json` points to `index.js`: ```json // package.json { "main": "index.js", ... } ``` ## Authentication Jazz provides authentication to help users access their data across multiple devices. For details on implementing authentication, check our [Authentication Overview](/docs/key-features/authentication/overview) guide and see the [React Native Chat Demo](https://github.com/garden-co/jazz/tree/main/examples/chat-rn) for a complete example. ## Next Steps Now that you've set up your React Native project for Jazz, you'll need to: 1. [Set up the Jazz Provider](/docs/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 react-native run-ios npx react-native run-android ``` ## Verification Ready to see if everything's working? Let's fire up your app: ```sh npx react-native run-ios # or npx react-native run-android ``` If all goes well, your app should start up without any angry red error screens. Take a quick look at the Metro console too - no Jazz-related errors there means you're all set! If you see your app's UI come up smoothly, you've nailed the installation. If you run into any issues that aren't covered in the Common Issues section, [drop by our Discord for help](https://discord.gg/utDMjHYg42). ## Common Issues * **Metro bundler errors**: If you see errors about missing polyfills, ensure all polyfills are properly imported in your `polyfills.js` file. * **iOS build failures**: Make sure you've run `pod install` after adding the dependencies. * **Android build failures**: Ensure your Android SDK and NDK versions are compatible with the native modules. ### Install CocoaPods If you're compiling for iOS, you'll need to install CocoaPods for your project. If you need to install it, we recommend using [pod-install](https://www.npmjs.com/package/pod-install): ```bash npx pod-install ``` ### 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-specific example, but you need help you can [request one](https://github.com/garden-co/jazz/issues/new), or ask on [Discord](https://discord.gg/utDMjHYg42). For use 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. ```tsx import { useCoState } from "jazz-tools/react"; const { queryEmbedding } = useCreateEmbedding(); const foundDocuments = useCoState(DocumentsList, documentsListId, { resolve: { $each: { embedding: true }, }, select(documents) { if (!documents.$isLoaded) return; // If no query embedding, return all entries if (!queryEmbedding) return documents.map((value) => ({ value })); return documents .map((value) => ({ value, similarity: value.embedding.$jazz.cosineSimilarity(queryEmbedding), // [!code ++] })) .sort((a, b) => b.similarity - a.similarity) .filter((result) => result.similarity > 0.5); }, }); ``` 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\] Jazz's images implementation is based on `@bam.tech/react-native-image-resizer`. Check the [installation guide](/docs/react-native/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/react-native"; 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 React Native, image manipulation is done using the `@bam.tech/react-native-image-resizer` 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 # Subscriptions & Deep Loading Jazz's Collaborative Values (such as [CoMaps](/docs/core-concepts/covalues/comaps) or [CoLists](/docs/core-concepts/covalues/colists)) are reactive. You can subscribe to them to automatically receive updates whenever they change, either locally or remotely. You can also use subscriptions to load CoValues _deeply_ by resolving nested values. You can specify exactly how much data you want to resolve and handle loading states and errors. You can load and subscribe to CoValues in one of two ways: * **shallowly** β€” all of the primitive fields are available (such as strings, numbers, dates), but the references to other CoValues are not loaded * **deeply** β€” some or all of the referenced CoValues have been loaded **Info: Tip** Jazz automatically deduplicates loading. If you subscribe to the same CoValue multiple times in your app, Jazz will only fetch it once. That means you don’t need to deeply load a CoValue _just in case_ a child component might need its data, and you don’t have to worry about tracking every possible field your app needs in a top-level query. Instead, pass the CoValue ID to the child component and subscribe there β€” Jazz will only load what that component actually needs. ## Subscription Hooks On your front-end, using a subscription hook is the easiest way to manage your subscriptions. The subscription and related clean-up is handled automatically, and you can use your data like any other piece of state in your app. ### Subscribe to CoValues ```tsx import { useCoState } from "jazz-tools/react"; function ProjectView({ projectId }: { projectId: string }) { // Subscribe to a project and resolve its tasks const project = useCoState(Project, projectId, { resolve: { tasks: { $each: true } }, // Tell Jazz to load each task in the list }); if (!project.$isLoaded) { switch (project.$jazz.loadingState) { case "unauthorized": return "Project not accessible"; case "unavailable": return "Project not found"; case "loading": return "Loading project..."; } } return (

{project.name}

    {project.tasks.map((task) => (
  • {task.title}
  • ))}
); } ``` **Note:** If you don't need to load a CoValue's references, you can choose to load it _shallowly_ by omitting the resolve query. ### Subscribe to the current user's account ```tsx import { useAccount } from "jazz-tools/react"; import { MyAppAccount } from "./schema"; function ProjectList() { const me = useAccount(MyAppAccount, { resolve: { profile: true }, }); if (!me.$isLoaded) { return "Loading..."; } return (

{me.profile.name}'s projects

); } ``` ### Loading States When you load or subscribe to a CoValue through a hook (or directly), it can be either: * **Loaded** β†’ The CoValue has been successfully loaded and all its data is available * **Not Loaded** β†’ The CoValue is not yet available You can use the `$isLoaded` field to check whether a CoValue is loaded. For more detailed information about why a CoValue is not loaded, you can check `$jazz.loadingState`: * `"loading"` β†’ The CoValue is still being fetched * `"unauthorized"` β†’ The current user doesn't have permission to access this CoValue * `"unavailable"` β†’ The CoValue couldn't be found or an error (e.g. a network timeout) occurred while loading See the examples above for practical demonstrations of how to handle these three states in your application. ## Deep Loading When you're working with related CoValues (like tasks in a project), you often need to load nested references as well as the top-level CoValue. This is particularly the case when working with [CoMaps](/docs/core-concepts/covalues/comaps) that refer to other CoValues or [CoLists](/docs/core-concepts/covalues/colists) of CoValues. You can use `resolve` queries to tell Jazz what data you need to use. ### Using Resolve Queries A `resolve` query tells Jazz how deeply to load data for your app to use. We can use `true` to tell Jazz to shallowly load the tasks list here. Note that this does _not_ cause the tasks themselves to load, just the CoList that holds the tasks. ```ts const Task = co.map({ title: z.string(), description: co.plainText(), get subtasks() { return co.list(Task); }, }); const Project = co.map({ name: z.string(), tasks: co.list(Task), }); const project = await Project.load(projectId); if (!project.$isLoaded) throw new Error("Project not found or not accessible"); // This will be loaded project.name; // string // This *may not be loaded*, and *may not be accessible* project.tasks; // MaybeLoaded const projectWithTasksShallow = await Project.load(projectId, { resolve: { tasks: true, }, }); if (!projectWithTasksShallow.$isLoaded) throw new Error("Project not found or not accessible"); // This list of tasks will be shallowly loaded projectWithTasksShallow.tasks; // ListOfTasks // We can access the properties of the shallowly loaded list projectWithTasksShallow.tasks.length; // number // This *may not be loaded*, and *may not be accessible* projectWithTasksShallow.tasks[0]; // MaybeLoaded ``` We can use an `$each` expression to tell Jazz to load the items in a list. ```ts const projectWithTasks = await Project.load(projectId, { resolve: { tasks: { $each: true, }, }, }); if (!projectWithTasks.$isLoaded) throw new Error("Project not found or not accessible"); // The task will be loaded projectWithTasks.tasks[0]; // Task // Primitive fields are always loaded projectWithTasks.tasks[0].title; // string // References on the Task may not be loaded projectWithTasks.tasks[0].subtasks; // MaybeLoaded // CoTexts are CoValues too projectWithTasks.tasks[0].description; // MaybeLoaded ``` We can also build a query that _deeply resolves_ to multiple levels: ```ts const projectDeep = await Project.load(projectId, { resolve: { tasks: { $each: { subtasks: { $each: true, }, description: true, }, }, }, }); if (!projectDeep.$isLoaded) throw new Error("Project not found or not accessible"); // Primitive fields are always loaded projectDeep.tasks[0].subtasks[0].title; // string // The description will be loaded as well projectDeep.tasks[0].description; // CoPlainText ``` **Warning: Always load data explicitly** If you access a reference that wasn't included in your `resolve` query, you may find that it is already loaded, potentially because some other part of your app has already loaded it. **You should not rely on this**. Expecting data to be there which is not explicitly included in your `resolve` query can lead to subtle, hard-to-diagnose bugs. Always include every nested CoValue you need to access in your `resolve` query. ### Where To Use Resolve Queries The syntax for resolve queries is shared throughout Jazz. As well as using them in `load` and `subscribe` method calls, you can pass a resolve query to a front-end hook. ```tsx const projectId = ""; const projectWithTasksShallow = useCoState(Project, projectId, { resolve: { tasks: true, }, }); ``` You can also specify resolve queries at the schema level, using the `.resolved()` method. These queries will be used when loading CoValues from that schema (if no resolve query is provided by the user) and in types defined with [co.loaded](/docs/core-concepts/subscription-and-loading#type-safety-with-coloaded). ```ts const TaskWithDescription = Task.resolved({ description: true, }); const ProjectWithTasks = Project.resolved({ tasks: { // Use `.resolveQuery` to get the resolve query from a schema and compose it in other queries $each: TaskWithDescription.resolveQuery, } }); // .load() will use the resolve query from the schema const project = await ProjectWithTasks.load(projectId); if (!project.$isLoaded) throw new Error("Project not found or not accessible"); // Both the tasks and the descriptions are loaded project.tasks[0].description; // CoPlainText ``` ## Loading Errors A load operation will be successful **only** if all references requested (both optional and required) could be successfully loaded. If any reference cannot be loaded, the entire load operation will return a not-loaded CoValue to avoid potential inconsistencies. ```ts // If permissions on description are restricted: const task = await Task.load(taskId, { resolve: { description: true }, }); task.$isLoaded; // false task.$jazz.loadingState; // "unauthorized" ``` This is also true if **any** element of a list is inaccessible, even if all the others can be loaded. ```ts // One task in the list has restricted permissions const projectWithUnauthorizedTasks = await Project.load(projectId, { resolve: { tasks: { $each: true } }, }); project.$isLoaded; // false project.$jazz.loadingState; // "unauthorized" ``` Loading will be successful if all requested references are loaded. Non-requested references may or may not be available. ```ts // One task in the list has restricted permissions const shallowlyLoadedProjectWithUnauthorizedTasks = await Project.load( projectId, { resolve: true, }, ); if (!project.$isLoaded) throw new Error("Project not found or not accessible"); // Assuming the user has permissions on the project, this load will succeed, even if the user cannot load one of the tasks in the list project.$isLoaded; // true // Tasks may not be loaded since we didn't request them project.tasks.$isLoaded; // may be false ``` ### Catching loading errors We can use `$onError` to handle cases where some data you have requested is inaccessible, similar to a `try...catch` block in your query. For example, in case of a `project` (which the user can access) with three `task` items: | Task | User can access task? | User can access task.description? | | ---- | --------------------- | --------------------------------- | | 0 | βœ… | βœ… | | 1 | βœ… | ❌ | | 2 | ❌ | ❌ | #### Scenario 1: Skip Inaccessible List Items If some of your list items may not be accessible, you can skip loading them by specifying `$onError: 'catch'`. Inaccessible items will be not-loaded CoValues, while accessible items load properly. ```ts // Inaccessible tasks will not be loaded, but the project will const projectWithInaccessibleSkipped = await Project.load(projectId, { resolve: { tasks: { $each: { $onError: "catch" } } }, }); if (!project.$isLoaded) { throw new Error("Project not found or not accessible"); } if (!project.tasks.$isLoaded) { throw new Error("Task List not found or not accessible"); } project.tasks[0].$isLoaded; // true project.tasks[1].$isLoaded; // true project.tasks[2].$isLoaded; // false (caught by $onError) ``` #### Scenario 2: Handling Inaccessible Nested References An `$onError` applies only in the block where it's defined. If you need to handle multiple potential levels of error, you can nest `$onError` handlers. This load will fail, because the `$onError` is defined only for the `task.description`, not for failures in loading the `task` itself. ```ts // Inaccessible tasks will not be loaded, but the project will const projectWithNestedInaccessibleSkipped = await Project.load(projectId, { resolve: { tasks: { $each: { description: true, $onError: "catch", }, }, }, }); if (!project.$isLoaded) { throw new Error("Project not found or not accessible"); } project.tasks[0].$isLoaded; // true project.tasks[1].$isLoaded; // true project.tasks[2].$isLoaded; // false (caught by $onError) ``` We can fix this by adding handlers at both levels ```ts const projectWithMultipleCatches = await Project.load(projectId, { resolve: { tasks: { $each: { description: { $onError: "catch" }, // catch errors loading task descriptions $onError: "catch", // catch errors loading tasks too }, }, }, }); project.$isLoaded; // true project.tasks[0].$isLoaded; // true project.tasks[0].description.$isLoaded; // true project.tasks[1].$isLoaded; // true project.tasks[1].description.$isLoaded; // false (caught by the inner handler) project.tasks[2].$isLoaded; // false (caught by the outer handler) ``` ## Type safety with co.loaded You can tell your application how deeply your data is loaded by using the `co.loaded` type. The `co.loaded` type is especially useful when passing data between components, because it allows TypeScript to check at compile time whether data your application depends is properly loaded. The second argument lets you pass a `resolve` query to specify how deeply your data is loaded. ```tsx import { co } from "jazz-tools"; import { Project } from "./schema"; type ProjectWithTasks = co.loaded< typeof Project, { tasks: { $each: true; }; } >; // In case the project prop isn't loaded as required, TypeScript will warn function TaskList({ project }: { project: ProjectWithTasks }) { // TypeScript knows tasks are loaded, so this is type-safe return (
    {project.tasks.map((task) => (
  • {task.title}
  • ))}
); } ``` You can pass a `resolve` query of any complexity to `co.loaded`. ## Manual subscriptions If you have a CoValue's ID, you can subscribe to it anywhere in your code using `CoValue.subscribe()`. **Note:** Manual subscriptions are best suited for vanilla JavaScript β€” for example in server-side code or tests. Inside front-end components, we recommend using a subscription hook. ```ts // Subscribe by ID const unsubscribe = Task.subscribe(taskId, {}, (updatedTask) => { console.log("Updated task:", updatedTask); }); // Always clean up when finished unsubscribe(); ``` You can also subscribe to an existing CoValue instance using the `$jazz.subscribe` method. ```ts const myTask = Task.create({ title: "My new task", }); // Subscribe using $jazz.subscribe const unsubscribe = myTask.$jazz.subscribe((updatedTask) => { console.log("Updated task:", updatedTask); }); // Always clean up when finished unsubscribe(); ``` ## Selectors \[!framework=react,react-native,react-native-expo\] Sometimes, you only need to react to changes in specific parts of a CoValue. In those cases, you can provide a `select` function to specify what data you are interested in, and an optional `equalityFn` option to control re-renders. * `select`: extract the fields you care about * `equalityFn`: (optional) control when data should be considered equal ```tsx function ProjectViewWithSelector({ projectId }: { projectId: string }) { // Subscribe to a project const project = useCoState(Project, projectId, { resolve: { tasks: true, }, select: (project) => { if (!project.$isLoaded) return project.$jazz.loadingState; return { name: project.name, taskCount: project.tasks.length, }; }, // Only re-render if the name or the number of tasks change equalityFn: (a, b) => { if (typeof a === "string" || typeof b === "string") { // If either value is a string, it is not loaded, so we cannot check for equality. return false; } return a?.name === b?.name && a?.taskCount === b?.taskCount; }, }); if (typeof project === "string") { switch (project) { case "unauthorized": return "Project not accessible"; case "unavailable": return "Project not found"; case "loading": return "Loading..."; } } return (

{project.name}

{project.taskCount} task(s)
); } ``` By default, the return values of the select function will be compared using `Object.is`, but you can use the `equalityFn` to add your own logic. You can also use `useAccount` in the same way, to subscribe to only the changes in a user's account you are interested in. ```tsx import { useAccount } from "jazz-tools/react"; function ProfileName() { // Only re-renders when the profile name changes const profileName = useAccount(MyAppAccount, { resolve: { profile: true, }, select: (account) => account.$isLoaded ? account.profile.name : "Loading...", }); return
{profileName}
; } ``` ## Ensuring data is loaded In most cases, you'll have specified the depth of data you need in a `resolve` query when you first load or subscribe to a CoValue. However, sometimes you might have a CoValue instance which is not loaded deeply enough, or you're not sure how deeply loaded it is. In this case, you need to make sure data is loaded before proceeding with an operation. The `$jazz.ensureLoaded` method lets you guarantee that a CoValue and its referenced data are loaded to a specific depth (i.e. with nested references resolved): ```ts async function completeAllTasks(projectId: string) { // Load the project const project = await Project.load(projectId, { resolve: true }); if (!project.$isLoaded) return; // Ensure tasks are deeply loaded const loadedProject = await project.$jazz.ensureLoaded({ resolve: { tasks: { $each: true, }, }, }); // Now we can safely access and modify tasks loadedProject.tasks.forEach((task, i) => { task.$jazz.set("title", `Task ${i}`); }); } ``` This can be useful if you have a shallowly loaded CoValue instance, and would like to load its references deeply. ## Best practices * Load exactly what you need. Start shallow and add your nested references with care. * Always check `$isLoaded` before accessing CoValue data. Use `$jazz.loadingState` for more detailed information. * Use `$onError: 'catch'` at each level of your query that can fail to handle inaccessible data gracefully. * Never rely on data being present unless it is requested in your `resolve` query. ### 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 NEXT_PUBLIC_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 MyApp({ 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. 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 Jazz has a built-in passkey authentication component that you can use to add authentication to your app. This is the easiest way to get started with securely authenticating users into your application. By adding this component, when users access your app, they'll be greeted with an input where they can enter their name, and create a passkey. ```tsx "use client"; // tells Next.js that this component can't be server-side rendered. If you're not using Next.js, you can remove it. // [!code --:1] import { JazzReactProvider } from "jazz-tools/react"; // [!code ++:1] import { JazzReactProvider, PasskeyAuthBasicUI } from "jazz-tools/react"; import { JazzFestAccount } from "@/app/schema"; const apiKey = process.env.NEXT_PUBLIC_JAZZ_API_KEY; export function JazzWrapper({ children }: { children: React.ReactNode }) { return ( {/* [!code ++:1] */} {children} {/* [!code ++:1] */} ); } ``` ## Give it a go! ... what, already?! Yes! Run your app and try creating a passkey and logging in! ```bash npm run dev ``` ### Not working? * Did you add `` _inside_ your provider? * Does it wrap all the children? * 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 The `PasskeyAuthBasicUI` component is not customisable, so we'll implement our own Auth component so that we can extend it. ```tsx import { useState } from "react"; import { usePasskeyAuth } from "jazz-tools/react"; export function Auth({ children }: { children: React.ReactNode }) { const [name, setName] = useState(""); const auth = usePasskeyAuth({ // Must be inside the JazzProvider because the hook depends on an active Jazz context. appName: "JazzFest", }); return ( <>
setName(e.target.value)} />
{auth.state === "signedIn" && children} ); } ``` ### Use your new component ```tsx "use client"; // tells Next.js that this component can't be server-side rendered. If you're not using Next.js, you can remove it. // [!code --:1] import { JazzReactProvider, PasskeyAuthBasicUI } from "jazz-tools/react"; // [!code ++:1] import { JazzReactProvider } from "jazz-tools/react"; import { Auth } from "./Auth.tsx"; import { JazzFestAccount } from "@/app/schema"; const apiKey = process.env.NEXT_PUBLIC_JAZZ_API_KEY; export function JazzWrapper({ children }: { children: React.ReactNode }) { return ( {/* [!code ++:3] */} {children} {/* [!code --:3] */} {children} ); } ``` ### 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. ```tsx import { useState } from "react"; // [!code --:1] import { usePasskeyAuth } from "jazz-tools/react"; // [!code ++:2] import { usePasskeyAuth, usePassphraseAuth } from "jazz-tools/react"; import { wordlist } from "./wordlist"; // or the path to your wordlist export function Auth({ children }: { children: React.ReactNode }) { const [name, setName] = useState(""); const auth = usePasskeyAuth({ // Must be inside the JazzProvider because the hook depends on an active Jazz context. appName: "JazzFest", }); // [!code ++:1] const passphraseAuth = usePassphraseAuth({ wordlist }); // This should be inside the provider too return ( <>
setName(e.target.value)} />
{auth.state === "signedIn" && ( <> {children} {/* [!code ++:5]*/}