# Jazz ## Documentation ### Getting started #### Introduction # Learn some Jazz Welcome to the Jazz documentation! **Note:** We just released [Jazz 0.14.0](/docs/upgrade/0-14-0) with a bunch of breaking changes and are still cleaning the docs up - see the [upgrade guide](/docs/upgrade/0-14-0) for details. ## Quickstart You can use [`create-jazz-app`](/docs/tools/create-jazz-app) to create a new Jazz project from one of our starter templates or example apps: ```sh npx create-jazz-app@latest --api-key you@example.com ``` Or set up Jazz yourself, using the following instructions for your framework of choice: - [React](/docs/react/project-setup) - [Next.js](/docs/react/project-setup#nextjs) - [React Native](/docs/react-native/project-setup) - [React Native Expo](/docs/react-native-expo/project-setup) - [Vue](/docs/vue/project-setup) - [Svelte](/docs/svelte/project-setup) {/* Or you can follow this [React step-by-step guide](/docs/react/guide) where we walk you through building an issue tracker app. */} ## Example apps You can also find [example apps](/examples) with code most similar to what you want to build. These apps make use of different features such as auth, file upload, and more. ## Sync and storage Sync and persist your data by setting up a [sync and storage infrastructure](/docs/sync-and-storage) using Jazz Cloud, or do it yourself. ## Collaborative values Learn how to structure your data using [collaborative values](/docs/schemas/covalues). ## LLM Docs Get better results with AI by [importing the Jazz docs](/docs/ai-tools) into your context window. ## Get support If you have any questions or need assistance, please don't hesitate to reach out to us on [Discord](https://discord.gg/utDMjHYg42). We would love to help you get started. #### Example apps #### FAQs # Frequently Asked Questions ## How established is Jazz? Jazz is backed by fantastic angel and institutional investors with experience and know-how in devtools and has been in development since 2020. ## Will Jazz be around long-term? We're committed to Jazz being around for a long time! We understand that when you choose Jazz for your projects, you're investing time and making a significant architectural choice, and we take that responsibility seriously. That's why we've designed Jazz with longevity in mind from the start: - The open source nature of our sync server means you'll always be able to run your own infrastructure - Your data remains accessible even if our cloud services change - We're designing the protocol as an open specification This approach creates a foundation that can continue regardless of any single company's involvement. The local-first architecture means your apps will always work, even offline, and your data remains yours. ### Project setup #### Installation ### react-native-expo Implementation # React Native (Expo) Installation and Setup Jazz supports Expo through the dedicated `jazz-expo` package, which is specifically designed for Expo applications. If you're building for React Native without Expo, please refer to the [React Native](/docs/react-native/project-setup) guide instead. Jazz requires an [Expo development build](https://docs.expo.dev/develop/development-builds/introduction/) using [Expo Prebuild](https://docs.expo.dev/workflow/prebuild/) for native code. It is **not compatible** with Expo Go. Jazz also supports the [New Architecture](https://docs.expo.dev/guides/new-architecture/). Tested with: ```json "expo": "~53.0.0", "react-native": "0.79.2", "react": "18.3.1" ``` ## Installation ### Create a new project (Skip this step if you already have one) ```bash npx create-expo-app -e with-router-tailwind my-jazz-app cd my-jazz-app npx expo prebuild ``` ### Install dependencies ```bash # Expo dependencies npx expo install expo-linking expo-secure-store expo-file-system @react-native-community/netinfo @bam.tech/react-native-image-resizer # React Native polyfills npm i -S @azure/core-asynciterator-polyfill react-native-url-polyfill readable-stream react-native-get-random-values @craftzdog/react-native-buffer # Jazz dependencies npm i -S jazz-tools jazz-expo jazz-react-native-media-images ``` **Note**: Hermes has added support for `atob` and `btoa` in React Native 0.74. If you are using earlier versions, you may also need to polyfill `atob` and `btoa` in your `package.json`. Packages to try include: `text-encoding`, `base-64`, and you can drop `@bacons/text-decoder`. #### Fix incompatible dependencies If you encounter incompatible dependencies, you can try to fix them with the following command: ```bash npx expo install --fix ``` ### Configure Metro #### Regular repositories If you are not working within a monorepo, create a new file `metro.config.js` in the root of your project with the following content: ```ts twoslash // @noErrors: 2304 // metro.config.js const { getDefaultConfig } = require("expo/metro-config"); const config = getDefaultConfig(__dirname); config.resolver.sourceExts = ["mjs", "js", "json", "ts", "tsx"]; config.resolver.requireCycleIgnorePatterns = [/(^|\/|\\)node_modules($|\/|\\)/]; module.exports = config; ``` #### Monorepos For monorepos, use the following `metro.config.js`: ```ts twoslash // @noErrors: 2304 // metro.config.js const { getDefaultConfig } = require("expo/metro-config"); const { FileStore } = require("metro-cache"); const path = require("path"); // eslint-disable-next-line no-undef const projectRoot = __dirname; const workspaceRoot = path.resolve(projectRoot, "../.."); const config = getDefaultConfig(projectRoot); config.watchFolders = [workspaceRoot]; config.resolver.nodeModulesPaths = [ path.resolve(projectRoot, "node_modules"), path.resolve(workspaceRoot, "node_modules"), ]; config.resolver.sourceExts = ["mjs", "js", "json", "ts", "tsx"]; config.resolver.requireCycleIgnorePatterns = [/(^|\/|\\)node_modules($|\/|\\)/]; config.cacheStores = [ new FileStore({ root: path.join(projectRoot, "node_modules", ".cache", "metro"), }), ]; module.exports = config; ``` ### Additional monorepo configuration (for pnpm) If you're using `pnpm`, you'll need to make sure that your expo app's `package.json` has this: ```json // package.json { "main": "index.js", ... } ``` For more information, refer to [this Expo monorepo example](https://github.com/byCedric/expo-monorepo-example#pnpm-workarounds). ## Authentication Jazz provides authentication to help users access their data across multiple devices. For details on implementing authentication with Expo, check our [Authentication Overview](/docs/authentication/overview) guide and see the [Expo Chat Demo](https://github.com/garden-co/jazz/tree/main/examples/chat-rn-expo-clerk) for a complete example. ## Next Steps Now that you've set up your Expo project for Jazz, you'll need to: 1. [Set up the Jazz Provider](/docs/react-native-expo/project-setup/providers) - Configure how your app connects to Jazz 2. [Add authentication](/docs/authentication/overview) (optional) - Enable users to access data across devices 3. Define your schema - See the [schema docs](/docs/schemas/covalues) for more information 4. Run your app: ```sh npx expo run:ios # or npx expo run:android ``` ## Verification Ready to see if everything's working? Let's fire up your app: ```sh npx expo run:ios # or npx expo run:android ``` If all goes well, your app should start up without any angry red error screens. Take a quick look at the Metro console too - no Jazz-related errors there means you're all set! If you see your app's UI come up smoothly, you've nailed the installation. If you run into any issues that aren't covered in the Common Issues section, [drop by our Discord for help](https://discord.gg/utDMjHYg42). ## Common Issues - **Metro bundler errors**: If you see errors about missing polyfills, ensure all polyfills are properly imported. - **iOS build failures**: Make sure you've run `pod install` after adding the dependencies. - **Android build failures**: Ensure you've run `npx expo prebuild` to generate native code. - **Expo Go incompatibility**: Remember that Jazz requires a development build and won't work with Expo Go. ### Install CocoaPods If you're compiling for iOS, you'll need to install CocoaPods for your project. If you need to install it, we recommend using [`pod-install`](https://www.npmjs.com/package/pod-install): ```bash npx pod-install ``` --- ### react-native Implementation # React Native Installation and Setup This guide covers setting up Jazz for React Native applications from scratch. If you're using Expo, please refer to the [React Native - Expo](/docs/react-native-expo/project-setup) guide instead. If you just want to get started quickly, you can use our [React Native Chat Demo](https://github.com/garden-co/jazz/tree/main/examples/chat-rn) as a starting point. Jazz supports the [New Architecture](https://reactnative.dev/architecture/landing-page) for React Native. Tested with: ```json "react-native": "0.79.2", "react": "18.3.1" ``` ## Installation ### Create a new project (Skip this step if you already have one) ```bash npx @react-native-community/cli init myjazzapp cd myjazzapp ``` If you intend to build for iOS, you can accept the invitation to install CocoaPods. If you decline, or you get an error, [you can install it with `pod-install`](#install-cocoapods). ### Install dependencies ```bash # React Native dependencies npm install @react-native-community/netinfo @bam.tech/react-native-image-resizer # React Native polyfills npm i -S @azure/core-asynciterator-polyfill react-native-url-polyfill readable-stream react-native-get-random-values @craftzdog/react-native-buffer @op-engineering/op-sqlite react-native-mmkv # Jazz dependencies npm i -S jazz-tools jazz-react-native jazz-react-native-media-images ``` **Note**: Hermes has added support for `atob` and `btoa` in React Native 0.74. If you are using earlier versions, you may also need to polyfill `atob` and `btoa` in your `package.json`. Packages to try include `text-encoding` and `base-64`, and you can drop `@bacons/text-decoder`. ### Configure Metro #### Regular repositories If you are not working within a monorepo, create a new file `metro.config.js` in the root of your project with the following content: ```ts twoslash // metro.config.js const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); const config = { resolver: { sourceExts: ["mjs", "js", "json", "ts", "tsx"], requireCycleIgnorePatterns: [/(^|\/|\\)node_modules($|\/|\\)/] } }; module.exports = mergeConfig(getDefaultConfig(__dirname), config); ``` #### Monorepos For monorepos, use the following `metro.config.js`: ```ts twoslash // metro.config.js const path = require("path"); const { makeMetroConfig } = require("@rnx-kit/metro-config"); const MetroSymlinksResolver = require("@rnx-kit/metro-resolver-symlinks"); // Define workspace root const projectRoot = __dirname; const workspaceRoot = path.resolve(projectRoot, "../.."); // Add packages paths const extraNodeModules = { modules: path.resolve(workspaceRoot, "node_modules"), }; const watchFolders = [ path.resolve(workspaceRoot, "node_modules"), path.resolve(workspaceRoot, "packages"), ]; const nodeModulesPaths = [ path.resolve(projectRoot, "node_modules"), path.resolve(workspaceRoot, "node_modules"), ]; module.exports = makeMetroConfig({ resolver: { resolveRequest: MetroSymlinksResolver(), extraNodeModules, nodeModulesPaths, }, sourceExts: ["mjs", "js", "json", "ts", "tsx"], watchFolders, }); ``` ### Additional monorepo configuration (for pnpm) - Add `node-linker=hoisted` to the root `.npmrc` (create this file if it doesn't exist). - Add the following to the root `package.json`: ```json // package.json "pnpm": { "peerDependencyRules": { "ignoreMissing": [ "@babel/*", "typescript" ] } } ``` ### Add polyfills Create a file `polyfills.js` at the project root with the following content: ```ts twoslash // @noErrors: 7016 // polyfills.js polyfillGlobal("Buffer", () => Buffer); // polyfill Buffer polyfillGlobal("ReadableStream", () => ReadableStream); // polyfill ReadableStream ``` Update `index.js`: ```ts twoslash // @noErrors: 2307 // index.js AppRegistry.registerComponent(appName, () => App); ``` Lastly, ensure that the `"main"` field in your `package.json` points to `index.js`: ```json // package.json { "main": "index.js", ... } ``` ## Authentication Jazz provides authentication to help users access their data across multiple devices. For details on implementing authentication, check our [Authentication Overview](/docs/authentication/overview) guide and see the [React Native Chat Demo](https://github.com/garden-co/jazz/tree/main/examples/chat-rn) for a complete example. ## Next Steps Now that you've set up your React Native project for Jazz, you'll need to: 1. [Set up the Jazz Provider](/docs/react-native/project-setup/providers) - Configure how your app connects to Jazz 2. [Add authentication](/docs/authentication/overview) (optional) - Enable users to access data across devices 3. Define your schema - See the [schema docs](/docs/schemas/covalues) for more information 4. Run your app: ```sh npx react-native run-ios npx react-native run-android ``` ## Verification Ready to see if everything's working? Let's fire up your app: ```sh npx react-native run-ios # or npx react-native run-android ``` If all goes well, your app should start up without any angry red error screens. Take a quick look at the Metro console too - no Jazz-related errors there means you're all set! If you see your app's UI come up smoothly, you've nailed the installation. If you run into any issues that aren't covered in the Common Issues section, [drop by our Discord for help](https://discord.gg/utDMjHYg42). ## Common Issues - **Metro bundler errors**: If you see errors about missing polyfills, ensure all polyfills are properly imported in your `polyfills.js` file. - **iOS build failures**: Make sure you've run `pod install` after adding the dependencies. - **Android build failures**: Ensure your Android SDK and NDK versions are compatible with the native modules. ### Install CocoaPods If you're compiling for iOS, you'll need to install CocoaPods for your project. If you need to install it, we recommend using [`pod-install`](https://www.npmjs.com/package/pod-install): ```bash npx pod-install ``` --- ### react Implementation # Installation and Setup Add Jazz to your React application in minutes. This setup covers standard React apps, Next.js, and gives an overview of experimental SSR approaches. Integrating Jazz with React is straightforward. You'll define data schemas that describe your application's structure, then wrap your app with a provider that handles sync and storage. The whole process takes just three steps: 1. [Install dependencies](#install-dependencies) 2. [Write your schema](#write-your-schema) 3. [Wrap your app in ``](#standard-react-setup) Looking for complete examples? Check out our [example applications](/examples) for chat apps, collaborative editors, and more. ## Install dependencies First, install the required packages: ```bash pnpm install jazz-react jazz-tools ``` ## Write your schema Define your data schema using [CoValues](/docs/schemas/covalues) from `jazz-tools`. ```tsx twoslash // schema.ts export const TodoItem = co.map({ title: z.string(), completed: z.boolean(), }); export const AccountRoot = co.map({ todos: co.list(TodoItem), }); export const MyAppAccount = co.account({ root: AccountRoot, profile: co.map({ name: z.string() }), }); ``` See [CoValues](/docs/schemas/covalues) for more information on how to define your schema. ## Standard React Setup Wrap your application with `` to connect to the Jazz network and define your data schema: ```tsx twoslash // @filename: schema.ts export const TodoItem = co.map({ title: z.string(), completed: z.boolean(), }); export const AccountRoot = co.map({ todos: co.list(TodoItem), }); export const MyAppAccount = co.account({ root: AccountRoot, profile: co.map({ name: z.string() }), }); // @filename: app.tsx function App() { return
Hello, world!
; } // ---cut--- // app.tsx createRoot(document.getElementById("root")!).render( ); ```
This setup handles: - Connection to the Jazz sync server - Schema registration for type-safe data handling - Local storage configuration With this in place, you're ready to start using Jazz hooks in your components. [Learn how to access and update your data](/docs/using-covalues/subscription-and-loading#subscription-hooks). ## Next.js Integration ### Client-side Only (Easiest) The simplest approach for Next.js is client-side only integration: ```tsx twoslash // @filename: schema.ts export const TodoItem = co.map({ title: z.string(), completed: z.boolean(), }); export const AccountRoot = co.map({ todos: co.list(TodoItem), }); export const MyAppAccount = co.account({ root: AccountRoot, profile: co.map({ name: z.string() }), }); // @filename: app.tsx // ---cut--- // app.tsx "use client" // Mark as client component export function JazzWrapper({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` Remember to mark any component that uses Jazz hooks with `"use client"`: ```tsx twoslash // @filename: schema.ts export const TodoItem = co.map({ title: z.string(), completed: z.boolean(), }); export const AccountRoot = co.map({ todos: co.list(TodoItem), }); export const MyAppAccount = co.account({ root: AccountRoot, profile: co.map({ name: z.string() }), }); // @filename: Profile.tsx // ---cut--- // Profile.tsx "use client"; // [!code ++] export function Profile() { const { me } = useAccount(MyAppAccount, { resolve: { profile: true } }); return
Hello, {me?.profile.name}
; } ```
### SSR Support (Experimental) For server-side rendering, Jazz offers experimental approaches: - Pure SSR - Hybrid SSR + Client Hydration #### Pure SSR Use Jazz in server components by directly loading data with `CoValue.load()`. {/* ```tsx twoslash // @errors: 18047 // @filename: schema.ts export class MyItem extends CoMap { title = co.string; } export class MyCollection extends CoList.Of(co.ref(MyItem)) {} // @filename: PublicData.tsx const collectionID = "co_z123" as ID; // ---cut--- // Server Component (no "use client" directive) export default async function PublicData() { // Load data directly in the server component const items = await MyCollection.load(collectionID); if (!items) { return
Loading...
; } return (
    {items.map(item => ( item ?
  • {item.title}
  • : null ))}
); } ```
*/} This works well for public data accessible to the server account. #### Hybrid SSR + Client Hydration For more complex cases, you can pre-render on the server and hydrate on the client: 1. Create a shared rendering component. {/* ```tsx twoslash // @filename: schema.ts export class MyItem extends CoMap { title = co.string; } // @filename: ItemList.tsx // ---cut--- // ItemList.tsx - works in both server and client contexts export function ItemList({ items }: { items: MyItem[] }) { return (
    {items.map(item => (
  • {item.title}
  • ))}
); } ```
*/} 2. Create a client hydration component. {/* ```tsx twoslash // @filename: schema.ts export class MyItem extends CoMap { title = co.string; } export class MyCollection extends CoList.Of(co.ref(MyItem)) {} // @filename: ItemList.tsx export function ItemList({ items }: { items: MyItem[] }) { return (
    {items.map(item => (
  • {item.title}
  • ))}
); } // @filename: ItemListHydrator.tsx // ItemListHydrator.tsx const myCollectionID = "co_z123" as ID; // ---cut--- "use client" export function ItemListHydrator({ initialItems }: { initialItems: MyItem[] }) { // Hydrate with real-time data once client loads const myCollection = useCoState(MyCollection, myCollectionID); // Filter out nulls for type safety const items = Array.from(myCollection?.values() || []).filter( (item): item is MyItem => !!item ); // Use server data until client data is available const displayItems = items || initialItems; return ; } ```
*/} 3. Create a server component that pre-loads data. {/* ```tsx twoslash // @filename: schema.ts export class MyItem extends CoMap { title = co.string; } export class MyCollection extends CoList.Of(co.ref(MyItem)) {} // @filename: ItemList.tsx export function ItemList({ items }: { items: MyItem[] }) { return (
    {items.map(item => (
  • {item.title}
  • ))}
); } // @filename: ItemListHydrator.tsx // ItemListHydrator.tsx const myCollectionID = "co_z123" as ID; // ---cut--- "use client" export function ItemListHydrator({ initialItems }: { initialItems: MyItem[] }) { // Hydrate with real-time data once client loads const myCollection = useCoState(MyCollection, myCollectionID); // Filter out nulls for type safety const items = Array.from(myCollection?.values() || []).filter( (item): item is MyItem => !!item ); // Use server data until client data is available const displayItems = items || initialItems; return ; } // @filename: ServerItemPage.tsx const myCollectionID = "co_z123" as ID; // ---cut--- // ServerItemPage.tsx export default async function ServerItemPage() { // Pre-load data on the server const initialItems = await MyCollection.load(myCollectionID); // Filter out nulls for type safety const items = Array.from(initialItems?.values() || []).filter( (item): item is MyItem => !!item ); // Pass to client hydrator return ; } ```
*/} This approach gives you the best of both worlds: fast initial loading with server rendering, plus real-time updates on the client. ## Further Reading - [Schemas](/docs/schemas/covalues) - Learn about defining your data model - [Provider Configuration](/docs/project-setup/providers) - Learn about other configuration options for Providers - [Authentication](/docs/authentication/overview) - Set up user authentication - [Sync and Storage](/docs/sync-and-storage) - Learn about data persistence --- ### server-side Implementation # Node.JS / server workers The main detail to understand when using Jazz server-side is that Server Workers have Jazz `Accounts`, just like normal users do. This lets you share CoValues with Server Workers, having precise access control by adding the Worker to `Groups` with specific roles just like you would with other users. [See the full example here.](https://github.com/garden-co/jazz/tree/main/examples/jazz-paper-scissors) ## Generating credentials Server Workers typically have static credentials, consisting of a public Account ID and a private Account Secret. To generate new credentials for a Server Worker, you can run: ```sh npx jazz-run account create --name "My Server Worker" ``` The name will be put in the public profile of the Server Worker's `Account`, which can be helpful when inspecting metadata of CoValue edits that the Server Worker has done. ## Storing & providing credentials Server Worker credentials are typically stored and provided as environmental variables. **Take extra care with the Account Secret — handle it like any other secret environment variable such as a DB password.** ## Starting a server worker You can use `startWorker` from `jazz-nodejs` to start a Server Worker. Similarly to setting up a client-side Jazz context, it: - takes a custom `AccountSchema` if you have one (for example, because the worker needs to store information in it's private account root) - takes a URL for a sync & storage server `startWorker` expects credentials in the `JAZZ_WORKER_ACCOUNT` and `JAZZ_WORKER_SECRET` environment variables by default (as printed by `npx account create ...`), but you can also pass them manually as `accountID` and `accountSecret` parameters if you get them from elsewhere. ```ts twoslash class MyWorkerAccount extends Account {} // ---cut--- const { worker } = await startWorker({ AccountSchema: MyWorkerAccount, syncServer: 'wss://cloud.jazz.tools/?key=you@example.com', }); ``` `worker` acts like `me` (as returned by `useAccount` on the client) - you can use it to: - load/subscribe to CoValues: `MyCoValue.subscribe(id, worker, {...})` - create CoValues & Groups `const val = MyCoValue.create({...}, { owner: worker })` ## Using CoValues instead of requests Just like traditional backend functions, you can use Server Workers to do useful stuff (computations, calls to third-party APIs etc.) and put the results back into CoValues, which subscribed clients automatically get notified about. What's less clear is how you can trigger this work to happen. - One option is to define traditional HTTP API handlers that use the Jazz Worker internally. This is helpful if you need to mutate Jazz state in response to HTTP requests such as for webhooks or non-Jazz API clients - The other option is to have the Jazz Worker subscribe to CoValues which they will then collaborate on with clients. - A common pattern is to implement a state machine represented by a CoValue, where the client will do some state transitions (such as `draft -> ready`), which the worker will notice and then do some work in response, feeding the result back in a further state transition (such as `ready -> success & data`, or `ready -> failure & error details`). - This way, client and worker don't have to explicitly know about each other or communicate directly, but can rely on Jazz as a communication mechanism - with computation progressing in a distributed manner wherever and whenever possible. --- ### svelte Implementation # Svelte Installation Jazz can be used with Svelte or in a SvelteKit app. To add some Jazz to your Svelte app, you can use the following steps: 1. Install Jazz dependencies ```sh pnpm install jazz-tools jazz-svelte ``` 2. Write your schema See the [schema docs](/docs/schemas/covalues) for more information. ```ts // src/lib/schema.ts export class MyProfile extends Profile { name = coField.string; counter = coField.number; // This will be publically visible } export class MyAccount extends Account { profile = coField.ref(MyProfile); // ... } ``` 3. Set up the Provider in your root layout ```svelte ``` 4. Use Jazz hooks in your components ```svelte ``` For a complete example of Jazz with Svelte, check out our [file sharing example](https://github.com/gardencmp/jazz/tree/main/examples/file-share-svelte) which demonstrates, Passkey authentication, file uploads and access control. --- ### vue Implementation # VueJS demo todo app guide This guide provides step-by-step instructions for setting up and running a Jazz-powered Todo application using VueJS. See the full example [here](https://github.com/garden-co/jazz/tree/main/examples/todo-vue). --- ## Setup ### Create a new app Run the following command to create a new VueJS application: ```bash ❯ pnpm create vue@latest ✔ Project name: … vue-setup-guide ✔ Add TypeScript? … Yes ✔ Add JSX Support? … No ✔ Add Vue Router for Single Page Application development? … Yes ✔ Add Pinia for state management? … No ✔ Add Vitest for Unit Testing? … No ✔ Add an End-to-End Testing Solution? › No ✔ Add ESLint for code quality? › Yes ✔ Add Prettier for code formatting? … Yes ``` ### Install dependencies Run the following command to install Jazz libraries: ```bash pnpm install jazz-tools jazz-browser jazz-vue ``` ### Implement `schema.ts` Define the schema for your application. Example schema inside `src/schema.ts` for a todo app: ```typescript export class ToDoItem extends CoMap { name = coField.string; completed = coField.boolean; } export class ToDoList extends CoList.Of(coField.ref(ToDoItem)) {} export class Folder extends CoMap { name = coField.string; items = coField.ref(ToDoList); } export class FolderList extends CoList.Of(coField.ref(Folder)) {} export class ToDoAccountRoot extends CoMap { folders = coField.ref(FolderList); } export class ToDoAccount extends Account { profile = coField.ref(Profile); root = coField.ref(ToDoAccountRoot); migrate() { if (!this._refs.root) { const group = Group.create({ owner: this }); const firstFolder = Folder.create( { name: "Default", items: ToDoList.create([], { owner: group }), }, { owner: group }, ); this.root = ToDoAccountRoot.create( { folders: FolderList.create([firstFolder], { owner: this, }), }, { owner: this }, ); } } } ``` ### Refactor `main.ts` Update the `src/main.ts` file to integrate Jazz: ```typescript declare module "jazz-vue" { interface Register { Account: ToDoAccount; } } const RootComponent = defineComponent({ name: "RootComponent", setup() { return () => [ h( JazzProvider, { AccountSchema: ToDoAccount, auth: authMethod.value, peer: "wss://cloud.jazz.tools/?key=vue-todo-example-jazz@garden.co", }, { default: () => h(App), }, ), ]; }, }); const app = createApp(RootComponent); app.use(router); app.mount("#app"); ``` ### Set up `router/index.ts`: Create a basic Vue router configuration. For example: ```typescript const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: "/", name: "Home", component: HomeView, }, ], }); export default router; ``` ### Implement `App.vue` Update the `App.vue` file to include logout functionality: ```typescript ``` ## Subscribing to a CoValue Subscribe to a CoValue inside `src/views/HomeView.vue`: ```typescript {@render children()} ``` ## Provider Options ### Sync Options The `sync` property configures how your application connects to the Jazz network: ```ts twoslash // @filename: src/routes/layout.svelte // ---cut--- const syncConfig: SyncConfig = { // Connection to Jazz Cloud or your own sync server peer: "wss://cloud.jazz.tools/?key=your-api-key", // When to sync: "always" (default), "never", or "signedUp" when: "always", } ``` See [Authentication States](/docs/authentication/authentication-states#controlling-sync-for-different-authentication-states) for more details on how the `when` property affects synchronization based on authentication state. ### Account Schema The `AccountSchema` property defines your application's account structure: ```svelte > {@render children()} ``` ### Additional Options The provider accepts these additional options: ```svelte {@render children} ``` See [Authentication States](/docs/authentication/authentication-states) for more information on authentication states, guest mode, and handling anonymous accounts. ## Authentication `` works with various authentication methods to enable users to access their data across multiple devices. For a complete guide to authentication, see our [Authentication Overview](/docs/authentication/overview). ## Need Help? If you have questions about configuring the Jazz Provider for your specific use case, [join our Discord community](https://discord.gg/utDMjHYg42) for help. ### Tools #### AI tools # Using AI to build Jazz apps AI tools, particularly large language models (LLMs), can accelerate your development with Jazz. Searching docs, responding to questions and even helping you write code are all things that LLMs are starting to get good at. However, Jazz is a rapidly evolving framework, so sometimes AI might get things a little wrong. To help the LLMs, we provide the Jazz documentation in a txt file that is optimized for use with AI tools, like Cursor. llms-full.txt ## Setting up AI tools Every tool is different, but generally, you'll need to either paste the contents of the [llms-full.txt](https://jazz.tools/llms-full.txt) file directly in your prompt, or attach the file to the tool. ### ChatGPT and v0 Upload the txt file in your prompt. ![ChatGPT prompt with llms-full.txt attached](/chatgpt-with-llms-full-txt.jpg) ### Cursor 1. Go to Settings > Cursor Settings > Features > Docs 2. Click "Add new doc" 3. Enter the following URL: ``` https://jazz.tools/llms-full.txt ``` ## llms.txt convention We follow the llms.txt [proposed standard](https://llmstxt.org/) for providing documentation to AI tools at inference time that helps them understand the context of the code you're writing. ## Limitations and considerations AI is amazing, but it's not perfect. What works well this week could break next week (or be twice as good). We're keen to keep up with changes in tooling to help support you building the best apps, but if you need help from humans (or you have issues getting set up), please let us know on [Discord](https://discord.gg/utDMjHYg42). #### create-jazz-app # create-jazz-app Jazz comes with a CLI tool that helps you quickly scaffold new Jazz applications. There are two main ways to get started: 1. **Starter templates** - Pre-configured setups to start you off with your preferred framework 2. **Example apps** - Extend one of our [example applications](https://jazz.tools/examples) to build your project ## Quick Start with Starter Templates Create a new Jazz app from a starter template in seconds: ```bash npx create-jazz-app@latest --api-key you@example.com ``` This launches an interactive CLI that guides you through selecting: - Pre-configured frameworks and authentication methods (See [Available Starters](#available-starters)) - Package manager - Project name - Jazz Cloud API key (optional) - Provides seamless sync and storage for your app ## Command Line Options If you know what you want, you can specify options directly from the command line: ```bash # Basic usage with project name npx create-jazz-app@latest my-app --framework react --api-key you@example.com # Specify a starter template npx create-jazz-app@latest my-app --starter react-passkey-auth --api-key you@example.com # Specify example app npx create-jazz-app@latest my-app --example chat --api-key you@example.com ``` ### Available Options - `directory` - Directory to create the project in (defaults to project name) - `-f, --framework` - Framework to use (React, React Native, Svelte, Vue) - `-s, --starter` - Starter template to use - `-e, --example` - Example project to use - `-p, --package-manager` - Package manager to use (npm, yarn, pnpm, bun, deno) - `-k, --api-key` - Jazz Cloud API key (during our [free public alpha](/docs/react/sync-and-storage#free-public-alpha), you can use your email as the API key) - `-h, --help` - Display help information ## Start From an Example App Want to start from one of [our example apps](https://jazz.tools/examples)? Our example apps include specific examples of features and use cases. They demonstrate real-world patterns for building with Jazz. Use one as your starting point: ```bash npx create-jazz-app@latest --example chat ``` ## Available Starters Starter templates are minimal setups that include the basic configuration needed to get started with Jazz. They're perfect when you want a clean slate to build on. Choose from these ready-to-use starter templates: - `react-passkey-auth` - React with Passkey authentication (easiest to start with) - `react-clerk-auth` - React with Clerk authentication - `vue-demo-auth` - Vue with Demo authentication - `svelte-passkey-auth` - Svelte with Passkey authentication - `rn-clerk-auth` - React Native with Clerk authentication Run `npx create-jazz-app --help` to see the latest list of available starters. ## What Happens Behind the Scenes When you run `create-jazz-app`, we'll: 1. Ask for your preferences (or use your command line arguments) 2. Clone the appropriate starter template 3. Update dependencies to their latest versions 4. Install all required packages 5. Set up your project and show next steps ## Requirements - Node.js 14.0.0 or later - Your preferred package manager (npm, yarn, pnpm, bun, or deno) #### Inspector # Jazz Inspector [Jazz Inspector](https://inspector.jazz.tools) is a tool to visually inspect a Jazz account or other CoValues. For now, you can get your account credentials from the `jazz-logged-in-secret` local storage key from within your Jazz app. [https://inspector.jazz.tools](https://inspector.jazz.tools) ## Exporting current account to Inspector from your app [!framework=react,svelte,vue,vanilla] In development mode, you can launch the Inspector from your Jazz app to inspect your account by pressing `Cmd+J`. ## Embedding the Inspector widget into your app [!framework=react,svelte,vue,vanilla] Alternatively, you can embed the Inspector directly into your app, so you don't need to open a separate window. Install the package. ```sh npm install jazz-inspector ``` Render the component within your `JazzProvider`. ```tsx // [!code ++] ``` ```sh npm install jazz-inspector-element ``` Render the component. ```ts document.body.appendChild(document.createElement("jazz-inspector")) ``` Or ```tsx ``` This will show the Inspector launch button on the right of your page. ### Positioning the Inspector button [!framework=react] You can also customize the button position with the following options: - right (default) - left - bottom right - bottom left - top right - top left For example: ```tsx ```
Your app
Check out the [music player app](https://github.com/garden-co/jazz/blob/main/examples/music-player/src/2_main.tsx) for a full example. Check out the [file share app](https://github.com/garden-co/jazz/blob/main/examples/file-share-svelte/src/src/routes/%2Blayout.svelte) for a full example. ### Upgrade guides #### 0.14.0 - Zod-based schemas # Jazz 0.14.0 Introducing Zod-based schemas We're excited to move from our own schema syntax to using Zod v4. This is the first step in a series of releases to make Jazz more familiar and to make CoValues look more like regular data structures. **Note: This is a huge release that we're still cleaning up and documenting.** We're still in the process of: - updating all our docs - double-checking all our framework bindings - completing all the details of this upgrade guide **Note: React Native is currently broken based on an [underlying Zod v4 issue](https://github.com/colinhacks/zod/issues/4148).** If you see something broken, please let us know on [Discord](https://discord.gg/utDMjHYg42) and check back in a couple hours. Thanks for your patience! ## Overview: So far, Jazz has relied on our own idiosyncratic schema definition syntax where you had to extend classes and be careful to use `co.ref` for references. ```ts // BEFORE export class Message extends CoMap { text = co.ref(CoPlainText); image = co.optional.ref(ImageDefinition); important = co.boolean; } export class Chat extends CoList.Of(co.ref(Message)) {} ``` While this had certain ergonomic benefits it relied on unclean hacks to work. In addition, many of our adopters expressed a preference for avoiding class syntax, and LLMs consistently expected to be able to use Zod. For this reason, we completely overhauled how you define and use CoValue schemas: ```ts twoslash // AFTER export const Message = co.map({ text: co.plainText(), image: z.optional(co.image()), important: z.boolean(), }); export const Chat = co.list(Message); ``` ## Major breaking changes ### Schema definitions You now define CoValue schemas using two new exports from `jazz-tools`: - a new `co` definer that mirrors Zod's object/record/array syntax to define CoValue types - `co.map()`, `co.record()`, `co.list()`, `co.feed()` - `co.account()`, `co.profile()` - `co.plainText()`, `co.richText()`, - `co.fileStream()`, `co.image()` - see the updated [Defining CoValue Schemas](/docs/schemas/covalues) - `z` re-exported from Zod v4 - primitives like `z.string()`, `z.number()`, `z.literal()` - **note**: additional constraints like `z.min()` and `z.max()` are not yet enforced, we'll add validation in future releases - complex types like `z.object()` and `z.array()` to define JSON-like fields without internal collaboration - combinators like `z.optional()` and `z.discriminatedUnion()` - these also work on CoValue types! - see the updated [Docs on Primitive Fields](/docs/schemas/covalues#primitive-fields), [Docs on Optional References](/docs/schemas/covalues#optional-references) and [Docs on Unions of CoMaps](/docs/schemas/covalues#unions-of-comaps-declaration) Similar to Zod v4's new object syntax, recursive and mutually recursive types are now [much easier to express](/docs/react/schemas/covalues#recursive-references). ### How to pass loaded CoValues Calls to `useCoState()` work just the same, but they return a slightly different type than before. And while you can still read from the type just as before... ```tsx twoslash // ---cut--- const Pet = co.map({ name: z.string(), age: z.number(), }); type Pet = co.loaded; const Person = co.map({ name: z.string(), age: z.number(), pets: co.list(Pet), }); type Person = co.loaded; function MyComponent({ id }: { id: string }) { const person = useCoState(Person, id); return person && ; } function PersonName({ person }: { person: Person }) { return
{person.name}
; } ```
`co.loaded` can also take a second argument to specify the loading depth of the expected CoValue, mirroring the `resolve` options for `useCoState`, `load`, `subscribe`, etc. ```tsx twoslash // ---cut--- const Pet = co.map({ name: z.string(), age: z.number(), }); type Pet = co.loaded; const Person = co.map({ name: z.string(), age: z.number(), pets: co.list(Pet), }); type Person = co.loaded; function MyComponent({ id }: { id: string }) { const personWithPets = useCoState(Person, id, { resolve: { pets: { $each: true } } // [!code ++] }); return personWithPets && ; } function PersonAndFirstPetName({ person }: { person: co.loaded // [!code ++] }) { return
{person.name} & {person.pets[0].name}
; } ```
We've removed the `useCoState`, `useAccount` and `useAccountOrGuest` hooks. You should now use the `CoState` and `AccountCoState` reactive classes instead. These provide greater stability and are significantly easier to work with. Calls to `new CoState()` work just the same, but they return a slightly different type than before. And while you can still read from the type just as before... ```ts twoslash filename="schema.ts" // @filename: schema.ts const Pet = co.map({ name: z.string(), age: z.number(), }); type Pet = co.loaded; const Person = co.map({ name: z.string(), age: z.number(), pets: co.list(Pet), }); type Person = co.loaded; ``` ```svelte twoslash filename="app.svelte" // @filename: app.svelte
{person.current?.name}
```
`co.loaded` can also take a second argument to specify the loading depth of the expected CoValue, mirroring the `resolve` options for `CoState`, `load`, `subscribe`, etc. ```svelte twoslash
{props.person.name}
    {#each props.person.pets as pet}
  • {pet.name}
  • {/each}
```
### Removing AccountSchema registration We have removed the Typescript AccountSchema registration. It was causing some deal of confusion to new adopters so we have decided to replace the magic inference with a more explicit approach. When using `useAccount` you should now pass the `Account` schema directly: ```tsx twoslash // @filename: schema.ts export const MyAccount = co.account({ profile: co.profile(), root: co.map({}) }); // @filename: app.tsx // ---cut--- function MyComponent() { const { me } = useAccount(MyAccount, { resolve: { profile: true, }, }); return
{me?.profile.name}
; } ```
When using `AccountCoState` you should now pass the `Account` schema directly: ```svelte twoslash filename="app.svelte"
{account.current?.profile.name}
```
### Defining migrations Now account schemas need to be defined with `co.account()` and migrations can be declared using `withMigration()`: ```ts twoslash const Pet = co.map({ name: z.string(), age: z.number(), }); const MyAppRoot = co.map({ pets: co.list(Pet), }); const MyAppProfile = co.profile({ name: z.string(), age: z.number().optional(), }); export const MyAppAccount = co.account({ root: MyAppRoot, profile: MyAppProfile, }).withMigration((account, creationProps?: { name: string }) => { if (account.root === undefined) { account.root = MyAppRoot.create({ pets: co.list(Pet).create([]), }); } if (account.profile === undefined) { const profileGroup = Group.create(); profileGroup.addMember("everyone", "reader"); account.profile = MyAppProfile.create({ name: creationProps?.name ?? "New user", }, profileGroup); } }); ``` ### Defining Schema helper methods TODO ## Minor breaking changes ### `_refs` and `_edits` are now potentially null The type of `_refs` and `_edits` is now nullable. ```ts twoslash // ---cut--- const Person = co.map({ name: z.string(), age: z.number(), }); const person = Person.create({ name: "John", age: 30 }); person._refs; // now nullable person._edits; // now nullable ``` ### `members` and `by` now return basic `Account` We have removed the Account schema registration, so now `members` and `by` methods now always return basic `Account`. This means that you now need to rely on `useCoState` on them to load their using your account schema. ```tsx twoslash const Pet = co.map({ name: z.string(), age: z.number(), }); const MyAppRoot = co.map({ pets: co.list(Pet), }); const MyAppProfile = co.profile({ name: z.string(), age: z.number().optional(), }); export const MyAppAccount = co.account({ root: MyAppRoot, profile: MyAppProfile, }); // ---cut--- function GroupMembers({ group }: { group: Group }) { const members = group.members; return (
{members.map((member) => ( ))}
); } function GroupMemberDetails({ accountId }: { accountId: string }) { const account = useCoState(MyAppAccount, accountId, { resolve: { profile: true, root: { pets: { $each: true }, }, }, }); return (
{account?.profile.name}
    {account?.root.pets.map((pet) =>
  • {pet.name}
  • )}
); } ```
### Defining schemas #### CoValues # Defining schemas: CoValues **CoValues ("Collaborative Values") are the core abstraction of Jazz.** They're your bread-and-butter datastructures that you use to represent everything in your app. As their name suggests, CoValues are inherently collaborative, meaning **multiple users and devices can edit them at the same time.** **Think of CoValues as "super-fast Git for lots of tiny data."** - CoValues keep their full edit histories, from which they derive their "current state". - The fact that this happens in an eventually-consistent way makes them [CRDTs](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type). - Having the full history also means that you often don't need explicit timestamps and author info - you get this for free as part of a CoValue's [edit metadata](/docs/using-covalues/history). CoValues model JSON with CoMaps and CoLists, but also offer CoFeeds for simple per-user value feeds, and let you represent binary data with FileStreams. ## Start your app with a schema Fundamentally, CoValues are as dynamic and flexible as JSON, but in Jazz you use them by defining fixed schemas to describe the shape of data in your app. This helps correctness and development speed, but is particularly important... - when you evolve your app and need migrations - when different clients and server workers collaborate on CoValues and need to make compatible changes Thinking about the shape of your data is also a great first step to model your app. Even before you know the details of how your app will work, you'll probably know which kinds of objects it will deal with, and how they relate to each other. In Jazz, you define schemas using `co` for CoValues and `z` (from [Zod](https://zod.dev/)) for their primitive fields. ```ts twoslash // schema.ts const ListOfTasks = co.list(z.string()); export const TodoProject = co.map({ title: z.string(), tasks: ListOfTasks, }); ``` This gives us schema info that is available for type inference *and* at runtime. Check out the inferred type of `project` in the example below, as well as the input `.create()` expects. ```ts twoslash // @filename: schema.ts export const ListOfTasks = co.list(z.string()); export const TodoProject = co.map({ title: z.string(), tasks: ListOfTasks, }); // @filename: app.ts // ---cut--- // app.ts const project = TodoProject.create( { title: "New Project", tasks: ListOfTasks.create([], Group.create()), }, Group.create() ); ``` ## Types of CoValues ### `CoMap` (declaration) CoMaps are the most commonly used type of CoValue. They are the equivalent of JSON objects (Collaborative editing follows a last-write-wins strategy per-key). You can either declare struct-like CoMaps: ```ts twoslash // schema.ts // ---cut--- const Task = co.map({ title: z.string(), completed: z.boolean(), }); ``` Or record-like CoMaps (key-value pairs, where keys are always `string`): ```ts twoslash const Fruit = co.map({ name: z.string(), color: z.string(), }); // ---cut--- const ColorToHex = co.record(z.string(), z.string()); const ColorToFruit = co.record(z.string(), Fruit); ``` See the corresponding sections for [creating](/docs/using-covalues/comaps#creating-comaps), [subscribing/loading](/docs/using-covalues/subscription-and-loading), [reading from](/docs/using-covalues/comaps#reading-from-comaps) and [updating](/docs/using-covalues/comaps#updating-comaps) CoMaps. ### `CoList` (declaration) CoLists are ordered lists and are the equivalent of JSON arrays. (They support concurrent insertions and deletions, maintaining a consistent order.) You define them by specifying the type of the items they contain: ```ts twoslash const Task = co.map({ title: z.string(), completed: z.boolean(), }); // ---cut--- const ListOfColors = co.list(z.string()); const ListOfTasks = co.list(Task); ``` See the corresponding sections for [creating](/docs/using-covalues/colists#creating-colists), [subscribing/loading](/docs/using-covalues/subscription-and-loading), [reading from](/docs/using-covalues/colists#reading-from-colists) and [updating](/docs/using-covalues/colists#updating-colists) CoLists. ### `CoFeed` (declaration) CoFeeds are a special CoValue type that represent a feed of values for a set of users/sessions (Each session of a user gets its own append-only feed). They allow easy access of the latest or all items belonging to a user or their sessions. This makes them particularly useful for user presence, reactions, notifications, etc. You define them by specifying the type of feed item: ```ts twoslash const Task = co.map({ title: z.string(), completed: z.boolean(), }); // ---cut--- const FeedOfTasks = co.feed(Task); ``` See the corresponding sections for [creating](/docs/using-covalues/cofeeds#creating-cofeeds), [subscribing/loading](/docs/using-covalues/subscription-and-loading), [reading from](/docs/using-covalues/cofeeds#reading-from-cofeeds) and [writing to](/docs/using-covalues/cofeeds#writing-to-cofeeds) CoFeeds. ### `FileStream` (declaration) FileStreams are a special type of CoValue that represent binary data. (They are created by a single user and offer no internal collaboration.) They allow you to upload and reference files. You typically don't need to declare or extend them yourself, you simply refer to the built-in `co.fileStream()` from another CoValue: ```ts twoslash // ---cut--- const Document = co.map({ title: z.string(), file: co.fileStream(), }); ``` See the corresponding sections for [creating](/docs/using-covalues/filestreams#creating-filestreams), [subscribing/loading](/docs/using-covalues/subscription-and-loading), [reading from](/docs/using-covalues/filestreams#reading-from-filestreams) and [writing to](/docs/using-covalues/filestreams#writing-to-filestreams) FileStreams. **Note: For images, we have a special, higher-level `co.image()` helper, see [ImageDefinition](/docs/using-covalues/imagedef).** ### Unions of CoMaps (declaration) You can declare unions of CoMaps that have discriminating fields, using `z.discriminatedUnion()`. ```ts twoslash // ---cut--- const ButtonWidget = co.map({ type: z.literal("button"), label: z.string(), }); const SliderWidget = co.map({ type: z.literal("slider"), min: z.number(), max: z.number(), }); const WidgetUnion = z.discriminatedUnion([ButtonWidget, SliderWidget]); ``` See the corresponding sections for [creating](/docs/using-covalues/schemaunions#creating-schemaunions), [subscribing/loading](/docs/using-covalues/subscription-and-loading) and [narrowing](/docs/using-covalues/schemaunions#narrowing) SchemaUnions. ## CoValue field/item types Now that we've seen the different types of CoValues, let's see more precisely how we declare the fields or items they contain. ### Primitive fields You can declare primitive field types using `z` (re-exported in `jazz-tools` from [Zod](https://zod.dev/)): ```ts twoslash const Person = co.map({ title: z.string(), }) export const ListOfColors = co.list(z.string()); ``` Here's a quick overview of the primitive types you can use: ```ts twoslash // ---cut--- z.string(); // For simple strings z.number(); // For numbers z.boolean(); // For booleans z.null(); // For null z.date(); // For dates z.literal(["waiting", "ready"]); // For enums ``` Finally, for more complex JSON data, that you *don't want to be collaborative internally* (but only ever update as a whole), you can use more complex Zod types. For example, you can use `z.object()` to represent an internally immutable position: ```ts twoslash // ---cut--- const Sprite = co.map({ // assigned as a whole position: z.object({ x: z.number(), y: z.number() }), }); ``` Or you could use a `z.tuple()`: ```ts twoslash // ---cut--- const Sprite = co.map({ // assigned as a whole position: z.tuple([z.number(), z.number()]), }); ``` ### References to other CoValues To represent complex structured data with Jazz, you form trees or graphs of CoValues that reference each other. Internally, this is represented by storing the IDs of the referenced CoValues in the corresponding fields, but Jazz abstracts this away, making it look like nested CoValues you can get or assign/insert. The important caveat here is that **a referenced CoValue might or might not be loaded yet,** but we'll see what exactly that means in [Subscribing and Deep Loading](/docs/using-covalues/subscription-and-loading). In Schemas, you declare references by just using the schema of the referenced CoValue: ```ts twoslash // ---cut--- // schema.ts const Person = co.map({ name: z.string(), }); const ListOfPeople = co.list(Person); const Company = co.map({ members: ListOfPeople, }); ``` #### Optional References You can make references optional with `z.optional()`: ```ts twoslash const Pet = co.map({ name: z.string(), }); // ---cut--- const Person = co.map({ pet: z.optional(Pet), }); ``` #### Recursive References You can refer to the same schema from within itself using getters: ```ts twoslash // ---cut--- const Person = co.map({ name: z.string(), get bestFriend() { return Person; } }); ``` You can use the same technique for mutually recursive references, but you'll need to help TypeScript along: ```ts twoslash // ---cut--- const Person = co.map({ name: z.string(), get friends(): CoListSchema { return ListOfPeople; } }); const ListOfPeople = co.list(Person); ``` Note: similarly, if you use modifiers like `z.optional()` you'll need to help TypeScript along: ```ts twoslash // ---cut--- const Person = co.map({ name: z.string(), get bestFriend(): z.ZodOptional { return z.optional(Person); } }); ``` ### Helper methods You can use the `withHelpers` method on CoValue schemas to add helper functions to the schema itself. These typically take a parameter of a loaded CoValue of the schema. ```ts twoslash function differenceInYears(date1: Date, date2: Date) { const diffTime = Math.abs(date1.getTime() - date2.getTime()); return Math.ceil(diffTime / (1000 * 60 * 60 * 24 * 365.25)); } // ---cut--- const Person = co.map({ firstName: z.string(), lastName: z.string(), dateOfBirth: z.date(), }).withHelpers((Self) => ({ fullName(person: Loaded) { return `${person.firstName} ${person.lastName}`; }, ageAsOf(person: Loaded, date: Date) { return differenceInYears(date, person.dateOfBirth); } })); const person = Person.create({ firstName: "John", lastName: "Doe", dateOfBirth: new Date("1990-01-01"), }); const fullName = Person.fullName(person); const age = Person.ageAsOf(person, new Date()); ``` #### Accounts & migrations # Accounts & Migrations ## CoValues as a graph of data rooted in accounts Compared to traditional relational databases with tables and foreign keys, Jazz is more like a graph database, or GraphQL APIs — where CoValues can arbitrarily refer to each other and you can resolve references without having to do a join. (See [Subscribing & deep loading](/docs/using-covalues/subscription-and-loading)). To find all data related to a user, the account acts as a root node from where you can resolve all the data they have access to. These root references are modeled explicitly in your schema, distinguishing between data that is typically public (like a user's profile) and data that is private (like their messages). ### `Account.root` - private data a user cares about Every Jazz app that wants to refer to per-user data needs to define a custom root `CoMap` schema and declare it in a custom `Account` schema as the `root` field: ```ts twoslash const Chat = co.map({}); // ---cut--- const MyAppRoot = co.map({ myChats: co.list(Chat), }); export const MyAppAccount = co.account({ root: MyAppRoot, profile: co.profile(), }); ``` ### `Account.profile` - public data associated with a user The built-in `Account` schema class comes with a default `profile` field, which is a CoMap (in a Group with `"everyone": "reader"` - so publicly readable permissions) that is set up for you based on the username the `AuthMethod` provides on account creation. Their pre-defined schemas roughly look like this: ```ts twoslash // @noErrors: 2416 // ---cut--- // ...somewhere in jazz-tools itself... const Account = co.account({ root: co.map({}), profile: co.profile({ name: z.string(), }), }); ``` If you want to keep the default `co.profile()` schema, but customise your account's private `root`, all you have to do is define a new `root` field in your account schema and use `co.profile()` without options: ```ts twoslash const Chat = co.map({}); // ---cut--- const MyAppRoot = co.map({ // [!code ++:3] myChats: co.list(Chat), }); export const MyAppAccount = co.account({ root: MyAppRoot, // [!code ++] profile: co.profile(), }); ``` If you want to extend the `profile` to contain additional fields (such as an avatar `co.image()`), you can declare your own profile schema class using `co.profile({...})`: ```ts twoslash const Chat = co.map({}); // ---cut--- export const MyAppRoot = co.map({ myChats: co.list(Chat), }); export const MyAppProfile = co.profile({ // [!code ++:4] name: z.string(), // compatible with default Profile schema avatar: z.optional(co.image()), }); export const MyAppAccount = co.account({ root: MyAppRoot, profile: MyAppProfile, // [!code ++] }); ``` ## Resolving CoValues starting at `profile` or `root` To use per-user data in your app, you typically use `useAccount` somewhere in a high-level component, pass it your custom Account schema and specify which references to resolve using a resolve query (see [Subscribing & deep loading](/docs/using-covalues/subscription-and-loading)). ```tsx twoslash const Chat = co.map({}); const MyAppRoot = co.map({ myChats: co.list(Chat), }); const MyAppProfile = co.profile(); const MyAppAccount = co.account({ root: MyAppRoot, profile: MyAppProfile, }); class ChatPreview extends React.Component<{ chat: Loaded }> {}; class ContactPreview extends React.Component<{ contact: Loaded }> {}; // ---cut--- function DashboardPageComponent() { const { me } = useAccount(MyAppAccount, { resolve: { profile: true, root: { myChats: { $each: true }, } }}); return (

Dashboard

{me ? (

Logged in as {me.profile.name}

My chats

{me.root.myChats.map((chat) => ( ))}
) : ( "Loading..." )}
); } ```
## Populating and evolving `root` and `profile` schemas with migrations As you develop your app, you'll likely want to - initialise data in a user's `root` and `profile` - add more data to your `root` and `profile` schemas You can achieve both by overriding the `migrate()` method on your `Account` schema class. ### When migrations run Migrations are run after account creation and every time a user logs in. Jazz waits for the migration to finish before passing the account to your app's context. ### Initialising user data after account creation ```ts twoslash const Chat = co.map({}); const Bookmark = co.map({}); const MyAppRoot = co.map({ myChats: co.list(Chat), }); const MyAppProfile = co.profile({ name: z.string(), bookmarks: co.list(Bookmark), }); // ---cut--- export const MyAppAccount = co.account({ root: MyAppRoot, profile: MyAppProfile, }).withMigration((account, creationProps?: { name: string }) => { // we specifically need to check for undefined, // because the root might simply be not loaded (`null`) yet if (account.root === undefined) { account.root = MyAppRoot.create({ // Using a group to set the owner is always a good idea. // This way if in the future we want to share // this coValue we can do so easily. myChats: co.list(Chat).create([], Group.create()), }); } if (account.profile === undefined) { const profileGroup = Group.create(); // Unlike the root, we want the profile to be publicly readable. profileGroup.addMember("everyone", "reader"); account.profile = MyAppProfile.create({ name: creationProps?.name ?? "New user", bookmarks: co.list(Bookmark).create([], profileGroup), }, profileGroup); } }); ``` ### Adding/changing fields to `root` and `profile` To add new fields to your `root` or `profile` schemas, amend their corresponding schema classes with new fields, and then implement a migration that will populate the new fields for existing users (by using initial data, or by using existing data from old fields). To do deeply nested migrations, you might need to use the asynchronous `ensureLoaded()` method before determining whether the field already exists, or is simply not loaded yet. Now let's say we want to add a `myBookmarks` field to the `root` schema: ```ts twoslash const Chat = co.map({}); const Bookmark = co.map({}); const MyAppProfile = co.profile({ name: z.string(), bookmarks: co.list(Bookmark), }); // ---cut--- const MyAppRoot = co.map({ myChats: co.list(Chat), myBookmarks: z.optional(co.list(Bookmark)), // [!code ++:1] }); export const MyAppAccount = co.account({ root: MyAppRoot, profile: MyAppProfile, }).withMigration(async (account) => { if (account.root === undefined) { account.root = MyAppRoot.create({ myChats: co.list(Chat).create([], Group.create()), }); } // We need to load the root field to check for the myContacts field const { root } = await account.ensureLoaded({ resolve: { root: true } }); // we specifically need to check for undefined, // because myBookmarks might simply be not loaded (`null`) yet if (root.myBookmarks === undefined) { // [!code ++:3] root.myBookmarks = co.list(Bookmark).create([], Group.create()); } }); ``` {/* TODO: Add best practice: only ever add fields Note: explain and reassure that there will be more guardrails in the future https://github.com/garden-co/jazz/issues/1160 */} ### Using CoValues #### CoMaps # CoMaps CoMaps are key-value objects that work like JavaScript objects. You can access properties with dot notation and define typed fields that provide TypeScript safety. They're ideal for structured data that needs type validation. ## Creating CoMaps CoMaps are typically defined with `co.map()` and specifying primitive fields using `z` (see [Defining schemas: CoValues](/docs/schemas/covalues) for more details on primitive fields): ```ts twoslash const Member = co.map({ name: z.string(), }); // ---cut--- const Project = co.map({ name: z.string(), startDate: z.date(), status: z.literal(["planning", "active", "completed"]), coordinator: z.optional(Member), }); ``` You can create either struct-like CoMaps with fixed fields (as above) or record-like CoMaps for key-value pairs: ```ts twoslash // ---cut--- const Inventory = co.record(z.string(), z.number()); ``` To instantiate a CoMap: ```ts twoslash const me = await createJazzTestAccount(); const Member = co.map({ name: z.string(), }); const Project = co.map({ name: z.string(), startDate: z.date(), status: z.literal(["planning", "active", "completed"]), coordinator: z.optional(Member), }); const Inventory = co.record(z.string(), z.number()); // ---cut--- const project = Project.create({ name: "Spring Planting", startDate: new Date("2025-03-15"), status: "planning", }); const inventory = Inventory.create({ tomatoes: 48, basil: 12, }); ``` ### Ownership When creating CoMaps, you can specify ownership to control access: ```ts twoslash const me = await createJazzTestAccount(); const memberAccount = await createJazzTestAccount(); const Member = co.map({ name: z.string(), }); const Project = co.map({ name: z.string(), startDate: z.date(), status: z.literal(["planning", "active", "completed"]), coordinator: z.optional(Member), }); // ---cut--- // Create with default owner (current user) const privateProject = Project.create({ name: "My Herb Garden", startDate: new Date("2025-04-01"), status: "planning", }); // Create with shared ownership const gardenGroup = Group.create(); gardenGroup.addMember(memberAccount, "writer"); const communityProject = Project.create( { name: "Community Vegetable Plot", startDate: new Date("2025-03-20"), status: "planning", }, { owner: gardenGroup }, ); ``` See [Groups as permission scopes](/docs/groups/intro) for more information on how to use groups to control access to CoMaps. ## Reading from CoMaps CoMaps can be accessed using familiar JavaScript object notation: ```ts twoslash const me = await createJazzTestAccount(); const Member = co.map({ name: z.string(), }); const Project = co.map({ name: z.string(), startDate: z.date(), status: z.literal(["planning", "active", "completed"]), coordinator: z.optional(Member), }); const project = Project.create( { name: "Spring Planting", startDate: new Date("2025-03-20"), status: "planning", }, ); // ---cut--- console.log(project.name); // "Spring Planting" console.log(project.status); // "planning" ``` ### Handling Optional Fields Optional fields require checks before access: ```ts twoslash const me = await createJazzTestAccount(); const Member = co.map({ name: z.string(), }); const Project = co.map({ name: z.string(), startDate: z.date(), status: z.literal(["planning", "active", "completed"]), coordinator: z.optional(Member), }); const project = Project.create( { name: "Spring Planting", startDate: new Date("2025-03-20"), status: "planning" }, ); // ---cut--- if (project.coordinator) { console.log(project.coordinator.name); // Safe access } ``` ### Working with Record CoMaps For record-type CoMaps, you can access values using bracket notation: ```ts twoslash const me = await createJazzTestAccount(); const Inventory = co.record(z.string(), z.number()); // ---cut--- const inventory = Inventory.create({ tomatoes: 48, peppers: 24, basil: 12 }); console.log(inventory["tomatoes"]); // 48 ``` ## Updating CoMaps Updating CoMap properties uses standard JavaScript assignment: ```ts twoslash const me = await createJazzTestAccount(); const Member = co.map({ name: z.string(), }); const Project = co.map({ name: z.string(), startDate: z.date(), status: z.literal(["planning", "active", "completed"]), coordinator: z.optional(Member), }); const Inventory = co.record(z.string(), z.number()); const project = Project.create( { name: "Spring Planting", startDate: new Date("2025-03-20"), status: "planning" }, ); // ---cut--- project.name = "Spring Vegetable Garden"; // Update name project.startDate = new Date("2025-03-20"); // Update date ``` ### Type Safety CoMaps are fully typed in TypeScript, giving you autocomplete and error checking: ```ts twoslash const me = await createJazzTestAccount(); const Member = co.map({ name: z.string(), }); const Project = co.map({ name: z.string(), startDate: z.date(), status: z.literal(["planning", "active", "completed"]), coordinator: z.optional(Member), }); const Inventory = co.record(z.string(), z.number()); const project = Project.create( { name: "Spring Planting", startDate: new Date("2025-03-20"), status: "planning" }, ); // ---cut--- project.name = "Spring Vegetable Planting"; // ✓ Valid string // @errors: 2322 project.startDate = "2025-03-15"; // ✗ Type error: expected Date ``` ### Deleting Properties You can delete properties from CoMaps: ```ts twoslash const me = await createJazzTestAccount(); const Member = co.map({ name: z.string(), }); const Project = co.map({ name: z.string(), startDate: z.date(), status: z.literal(["planning", "active", "completed"]), coordinator: z.optional(Member), }); const Inventory = co.record(z.string(), z.number()); const project = Project.create( { name: "Spring Planting", startDate: new Date("2025-03-20"), status: "planning" }, ); const inventory = Inventory.create({ tomatoes: 48, peppers: 24, basil: 12 }); // ---cut--- delete inventory["basil"]; // Remove a key-value pair // For optional fields in struct-like CoMaps project.coordinator = undefined; // Remove the reference ``` ## Best Practices ### Structuring Data - Use struct-like CoMaps for entities with fixed, known properties - Use record-like CoMaps for dynamic key-value collections - Group related properties into nested CoMaps for better organization ### Common Patterns #### Helper methods You can add helper methods to your CoMap schema to make it more useful: ```ts twoslash const me = await createJazzTestAccount(); // ---cut--- const Project = co.map({ name: z.string(), startDate: z.date(), endDate: z.optional(z.date()), }).withHelpers((Self) => ({ isActive(project: Loaded) { const now = new Date(); return now >= project.startDate && (!project.endDate || now <= project.endDate); }, formatDuration(project: Loaded, format: "short" | "full") { const start = project.startDate.toLocaleDateString(); if (!project.endDate) { return format === "full" ? `Started on ${start}, ongoing` : `From ${start}`; } const end = project.endDate.toLocaleDateString(); return format === "full" ? `From ${start} to ${end}` : `${(project.endDate.getTime() - project.startDate.getTime()) / 86400000} days`; } })); const project = Project.create({ name: "My project", startDate: new Date("2025-04-01"), endDate: new Date("2025-04-04"), }); console.log(Project.isActive(project)); // false console.log(Project.formatDuration(project, "short")); // "3 days" ``` #### CoLists # CoLists CoLists are ordered collections that work like JavaScript arrays. They provide indexed access, iteration methods, and length properties, making them perfect for managing sequences of items. ## Creating CoLists CoLists are defined by specifying the type of items they contain: ```ts twoslash const Task = co.map({ title: z.string(), status: z.literal(["todo", "in-progress", "complete"]), }); // ---cut--- const ListOfResources = co.list(z.string()); const ListOfTasks = co.list(Task); ``` To create a `CoList`: ```ts twoslash const Task = co.map({ title: z.string(), status: z.literal(["todo", "in-progress", "complete"]), }); // ---cut--- // Create an empty list const resources = co.list(z.string()).create([]); // Create a list with initial items const tasks = co.list(Task).create([ Task.create({ title: "Prepare soil beds", status: "in-progress" }), Task.create({ title: "Order compost", status: "todo" }) ]); ``` ### Ownership Like other CoValues, you can specify ownership when creating CoLists. ```ts twoslash const me = await createJazzTestAccount(); const colleagueAccount = await createJazzTestAccount(); const Task = co.map({ title: z.string(), status: z.string(), }); // ---cut--- // Create with shared ownership const teamGroup = Group.create(); teamGroup.addMember(colleagueAccount, "writer"); const teamList = co.list(Task).create([], { owner: teamGroup }); ``` See [Groups as permission scopes](/docs/groups/intro) for more information on how to use groups to control access to CoLists. ## Reading from CoLists CoLists support standard array access patterns: ```ts twoslash const Task = co.map({ title: z.string(), status: z.literal(["todo", "in-progress", "complete"]), }); const ListOfTasks = co.list(Task); const tasks = ListOfTasks.create([ Task.create({ title: "Prepare soil beds", status: "todo" }), Task.create({ title: "Order compost", status: "todo" }), ]); // ---cut--- // Access by index const firstTask = tasks[0]; console.log(firstTask.title); // "Prepare soil beds" // Get list length console.log(tasks.length); // 2 // Iteration tasks.forEach(task => { console.log(task.title); // "Prepare soil beds" // "Order compost" }); // Array methods const todoTasks = tasks.filter(task => task.status === "todo"); console.log(todoTasks.length); // 1 ``` ## Updating CoLists Update `CoList`s just like you would JavaScript arrays: ```ts twoslash const Task = co.map({ title: z.string(), status: z.literal(["todo", "in-progress", "complete"]), }); const ListOfTasks = co.list(Task); const ListOfResources = co.list(z.string()); const resources = ListOfResources.create([]); const tasks = ListOfTasks.create([]); // ---cut--- // Add items resources.push("Tomatoes"); // Add to end resources.unshift("Lettuce"); // Add to beginning tasks.push(Task.create({ // Add complex items title: "Install irrigation", status: "todo" })); // Replace items resources[0] = "Cucumber"; // Replace by index // Modify nested items tasks[0].status = "complete"; // Update properties of references ``` ### Deleting Items Remove specific items by index with `splice`, or remove the first or last item with `pop` or `shift`: ```ts twoslash const ListOfResources = co.list(z.string()); const resources = ListOfResources.create([ "Tomatoes", "Cucumber", "Peppers", ]); // ---cut--- // Remove 2 items starting at index 1 resources.splice(1, 2); console.log(resources); // ["Cucumber", "Peppers"] // Remove a single item at index 0 resources.splice(0, 1); console.log(resources); // ["Peppers"] // Remove items const lastItem = resources.pop(); // Remove and return last item resources.shift(); // Remove first item ``` ### Array Methods `CoList`s support the standard JavaScript array methods you already know: ```ts twoslash const ListOfResources = co.list(z.string()); const resources = ListOfResources.create([]); // ---cut--- // Add multiple items at once resources.push("Tomatoes", "Basil", "Peppers"); // Find items const basil = resources.find(r => r === "Basil"); // Filter (returns regular array, not a CoList) const tItems = resources.filter(r => r.startsWith("T")); console.log(tItems); // ["Tomatoes"] // Sort (modifies the CoList in-place) resources.sort(); console.log(resources); // ["Basil", "Peppers", "Tomatoes"] ``` ### Type Safety CoLists maintain type safety for their items: ```ts twoslash const Task = co.map({ title: z.string(), status: z.literal(["todo", "in-progress", "complete"]), }); const ListOfTasks = co.list(Task); const ListOfResources = co.list(z.string()); const resources = ListOfResources.create([]); const tasks = ListOfTasks.create([]); // ---cut--- // TypeScript catches type errors resources.push("Carrots"); // ✓ Valid string // @errors: 2345 resources.push(42); // ✗ Type error: expected string // For lists of references tasks.forEach(task => { console.log(task.title); // TypeScript knows task has title }); ``` ## Best Practices ### Common Patterns #### List Rendering CoLists work well with UI rendering libraries: ```tsx twoslash const Task = co.map({ title: z.string(), status: z.literal(["todo", "in-progress", "complete"]), }); // ---cut--- const ListOfTasks = co.list(Task); // React example function TaskList({ tasks }: { tasks: Loaded }) { return (
    {tasks.map(task => ( task ? (
  • {task.title} - {task.status}
  • ): null ))}
); } ```
#### Managing Relations CoLists can be used to create one-to-many relationships: ```ts twoslash const Task = co.map({ title: z.string(), status: z.literal(["todo", "in-progress", "complete"]), get project(): z.ZodOptional { return z.optional(Project); } }); const ListOfTasks = co.list(Task); const Project = co.map({ name: z.string(), get tasks(): CoListSchema { return ListOfTasks; } }); const project = Project.create( { name: "Garden Project", tasks: ListOfTasks.create([]), }, ); const task = Task.create({ title: "Plant seedlings", status: "todo", project: project, // Add a reference to the project }); // Add a task to a garden project project.tasks.push(task); // Access the project from the task console.log(task.project); // { name: "Garden Project", tasks: [task] } ``` #### CoFeeds # CoFeeds CoFeeds are append-only data structures that track entries from different user sessions and accounts. Unlike other CoValues where everyone edits the same data, CoFeeds maintain separate streams for each session. Each account can have multiple sessions (different browser tabs, devices, or app instances), making CoFeeds ideal for building features like activity logs, presence indicators, and notification systems. The following examples demonstrate a practical use of CoFeeds: - [Multi-cursors](https://github.com/garden-co/jazz/tree/main/examples/multi-cursors) - track user presence on a canvas with multiple cursors and out of bounds indicators - [Reactions](https://github.com/garden-co/jazz/tree/main/examples/reactions) - store per-user emoji reaction using a CoFeed ## Creating CoFeeds CoFeeds are defined by specifying the type of items they'll contain, similar to how you define CoLists: ```ts twoslash // ---cut--- // Define a schema for feed items const Activity = co.map({ timestamp: z.date(), action: z.literal(["watering", "planting", "harvesting", "maintenance"]), notes: z.optional(z.string()), }); // Define a feed of garden activities const ActivityFeed = co.feed(Activity); // Create a feed instance const activityFeed = ActivityFeed.create([]); ``` ### Ownership Like other CoValues, you can specify ownership when creating CoFeeds. ```ts twoslash const me = await createJazzTestAccount(); const colleagueAccount = await createJazzTestAccount(); const Activity = co.map({ timestamp: z.date(), action: z.literal(["watering", "planting", "harvesting", "maintenance"]), notes: z.optional(z.string()), }); const ActivityFeed = co.feed(Activity); // ---cut--- const teamGroup = Group.create(); teamGroup.addMember(colleagueAccount, "writer"); const teamFeed = ActivityFeed.create([], { owner: teamGroup }); ``` See [Groups as permission scopes](/docs/groups/intro) for more information on how to use groups to control access to CoFeeds. ## Reading from CoFeeds Since CoFeeds are made of entries from users over multiple sessions, you can access entries in different ways - from a specific user's session or from their account as a whole. ### Per-Session Access To retrieve entries from a session: ```ts twoslash const me = await createJazzTestAccount(); const Activity = co.map({ timestamp: z.date(), action: z.literal(["watering", "planting", "harvesting", "maintenance"]), notes: z.optional(z.string()), }); const ActivityFeed = co.feed(Activity); const activityFeed = ActivityFeed.create([]); const sessionId = `${me.id}_session_z1` as SessionID; // ---cut--- // Get the feed for a specific session const sessionFeed = activityFeed.perSession[sessionId]; // Latest entry from a session console.log(sessionFeed?.value?.action); // "watering" ``` For convenience, you can also access the latest entry from the current session with `inCurrentSession`: ```ts twoslash const me = await createJazzTestAccount(); const Activity = co.map({ timestamp: z.date(), action: z.literal(["watering", "planting", "harvesting", "maintenance"]), notes: z.optional(z.string()), }); const ActivityFeed = co.feed(Activity); const activityFeed = ActivityFeed.create([]); const sessionId = `${me.id}_session_z1` as SessionID; // ---cut--- // Get the feed for the current session const currentSessionFeed = activityFeed.inCurrentSession; // Latest entry from the current session console.log(currentSessionFeed?.value?.action); // "harvesting" ``` ### Per-Account Access To retrieve entries from a specific account (with entries from all sessions combined) use `perAccount`: ```ts twoslash const me = await createJazzTestAccount(); const Activity = co.map({ timestamp: z.date(), action: z.literal(["watering", "planting", "harvesting", "maintenance"]), notes: z.optional(z.string()), }); const ActivityFeed = co.feed(Activity); const activityFeed = ActivityFeed.create([]); const accountId = me.id; // ---cut--- // Get the feed for a specific account const accountFeed = activityFeed.perAccount[accountId]; // Latest entry from the account console.log(accountFeed.value?.action); // "watering" ``` For convenience, you can also access the latest entry from the current account with `byMe`: ```ts twoslash const me = await createJazzTestAccount(); const Activity = co.map({ timestamp: z.date(), action: z.literal(["watering", "planting", "harvesting", "maintenance"]), notes: z.optional(z.string()), }); const ActivityFeed = co.feed(Activity); const activityFeed = ActivityFeed.create([]); const accountId = me.id; // ---cut--- // Get the feed for the current account const myLatestEntry = activityFeed.byMe; // Latest entry from the current account console.log(myLatestEntry?.value?.action); // "harvesting" ``` ### Feed Entries #### All Entries To retrieve all entries from a CoFeed: ```ts twoslash const me = await createJazzTestAccount(); const Activity = co.map({ timestamp: z.date(), action: z.literal(["watering", "planting", "harvesting", "maintenance"]), notes: z.optional(z.string()), }); const ActivityFeed = co.feed(Activity); const activityFeed = ActivityFeed.create([]); const accountId = me.id; const sessionId = `${me.id}_session_z1` as SessionID; // ---cut--- // Get the feeds for a specific account and session const accountFeed = activityFeed.perAccount[accountId]; const sessionFeed = activityFeed.perSession[sessionId]; // Iterate over all entries from the account for (const entry of accountFeed.all) { console.log(entry.value); } // Iterate over all entries from the session for (const entry of sessionFeed.all) { console.log(entry.value); } ``` #### Latest Entry To retrieve the latest entry from a CoFeed, ie. the last update: ```ts twoslash const me = await createJazzTestAccount(); const Activity = co.map({ timestamp: z.date(), action: z.literal(["watering", "planting", "harvesting", "maintenance"]), notes: z.optional(z.string()), }); const ActivityFeed = co.feed(Activity); const activityFeed = ActivityFeed.create([]); // ---cut--- // Get the latest entry from the current account const latestEntry = activityFeed.byMe; console.log(`My last action was ${latestEntry?.value?.action}`); // "My last action was harvesting" // Get the latest entry from each account const latestEntriesByAccount = Object.values(activityFeed.perAccount).map(entry => ({ accountName: entry.by?.profile?.name, value: entry.value, })); ``` ## Writing to CoFeeds CoFeeds are append-only; you can add new items, but not modify existing ones. This creates a chronological record of events or activities. ### Adding Items ```ts twoslash const me = await createJazzTestAccount(); const Activity = co.map({ timestamp: z.date(), action: z.literal(["watering", "planting", "harvesting", "maintenance"]), notes: z.optional(z.string()), }); const ActivityFeed = co.feed(Activity); const activityFeed = ActivityFeed.create([]); // ---cut--- // Log a new activity activityFeed.push(Activity.create({ timestamp: new Date(), action: "watering", notes: "Extra water for new seedlings" })); ``` Each item is automatically associated with the current user's session. You don't need to specify which session the item belongs to - Jazz handles this automatically. ### Understanding Session Context Each entry is automatically added to the current session's feed. When a user has multiple open sessions (like both a mobile app and web browser), each session creates its own separate entries: ```ts twoslash const me = await createJazzTestAccount(); const Activity = co.map({ timestamp: z.date(), action: z.literal(["watering", "planting", "harvesting", "maintenance"]), notes: z.optional(z.string()), }); const ActivityFeed = co.feed(Activity); const fromMobileFeed = ActivityFeed.create([]); const fromBrowserFeed = ActivityFeed.create([]); // ---cut--- // On mobile device: fromMobileFeed.push(Activity.create({ timestamp: new Date(), action: "harvesting", notes: "Vegetable patch" })); // On web browser (same user): fromBrowserFeed.push(Activity.create({ timestamp: new Date(), action: "planting", notes: "Flower bed" })); // These are separate entries in the same feed, from the same account ``` ## Metadata CoFeeds support metadata, which is useful for tracking information about the feed itself. ### By The `by` property is the account that made the entry. ```ts twoslash const me = await createJazzTestAccount(); const Activity = co.map({ timestamp: z.date(), action: z.literal(["watering", "planting", "harvesting", "maintenance"]), notes: z.optional(z.string()), }); const ActivityFeed = co.feed(Activity); const activityFeed = ActivityFeed.create([]); const accountId = me.id; // ---cut--- const accountFeed = activityFeed.perAccount[accountId]; // Get the account that made the last entry console.log(accountFeed?.by); ``` ### MadeAt The `madeAt` property is a timestamp of when the entry was added to the feed. ```ts twoslash const me = await createJazzTestAccount(); const Activity = co.map({ timestamp: z.date(), action: z.literal(["watering", "planting", "harvesting", "maintenance"]), notes: z.optional(z.string()), }); const ActivityFeed = co.feed(Activity); const activityFeed = ActivityFeed.create([]); const accountId = me.id; // ---cut--- const accountFeed = activityFeed.perAccount[accountId]; // Get the timestamp of the last update console.log(accountFeed?.madeAt); // Get the timestamp of each entry for (const entry of accountFeed.all) { console.log(entry.madeAt); } ``` ## Best Practices ### When to Use CoFeeds - **Use CoFeeds when**: - You need to track per-user/per-session data - Time-based information matters (activity logs, presence) - **Consider alternatives when**: - Data needs to be collaboratively edited (use CoMaps or CoLists) - You need structured relationships (use CoMaps/CoLists with references) #### CoTexts # CoTexts Jazz provides two CoValue types for collaborative text editing, collectively referred to as "CoText" values: - **co.plainText()** for simple text editing without formatting - **co.richText()** for rich text with HTML-based formatting (extends co.plainText()) Both types enable real-time collaborative editing of text content while maintaining consistency across multiple users. **Note:** If you're looking for a quick way to add rich text editing to your app, check out [jazz-richtext-prosemirror](#using-rich-text-with-prosemirror). ```ts twoslash const me = await createJazzTestAccount(); // ---cut--- const note = co.plainText().create("Meeting notes", { owner: me }); // Update the text note.applyDiff("Meeting notes for Tuesday"); console.log(note.toString()); // "Meeting notes for Tuesday" ``` For a full example of CoTexts in action, see [our Richtext example app](https://github.com/garden-co/jazz/tree/main/examples/richtext), which shows plain text and rich text editing. ## co.plainText() vs z.string() While `z.string()` is perfect for simple text fields, `co.plainText()` is the right choice when you need: - Frequent text edits that aren't just replacing the whole field - Fine-grained control over text edits (inserting, deleting at specific positions) - Multiple users editing the same text simultaneously - Character-by-character collaboration - Efficient merging of concurrent changes Both support real-time updates, but `co.plainText()` provides specialized tools for collaborative editing scenarios. ## Creating CoText Values CoText values are typically used as fields in your schemas: ```ts twoslash // ---cut--- const Profile = co.profile({ name: z.string(), bio: co.plainText(), // Plain text field description: co.richText(), // Rich text with formatting }); ``` Create a CoText value with a simple string: ```ts twoslash const me = await createJazzTestAccount(); // ---cut--- // Create plaintext with default ownership (current user) const note = co.plainText().create("Meeting notes", { owner: me }); // Create rich text with HTML content const document = co.richText().create("

Project overview

", { owner: me } ); ```
### Ownership Like other CoValues, you can specify ownership when creating CoTexts. ```ts twoslash const me = await createJazzTestAccount(); const colleagueAccount = await createJazzTestAccount(); // ---cut--- // Create with shared ownership const teamGroup = Group.create(); teamGroup.addMember(colleagueAccount, "writer"); const teamNote = co.plainText().create("Team updates", { owner: teamGroup }); ``` See [Groups as permission scopes](/docs/groups/intro) for more information on how to use groups to control access to CoText values. ## Reading Text CoText values work similarly to JavaScript strings: ```ts twoslash const me = await createJazzTestAccount(); const note = co.plainText().create("Meeting notes", { owner: me }); // ---cut--- // Get the text content console.log(note.toString()); // "Meeting notes" console.log(`${note}`); // "Meeting notes" // Check the text length console.log(note.length); // 14 ``` When using CoTexts in JSX, you can read the text directly: ```tsx twoslash const me = await createJazzTestAccount(); const note = co.plainText().create("Meeting notes", { owner: me }); // ---cut--- <>

{note.toString()}

{note}

```
## Making Edits Insert and delete text with intuitive methods: ```ts twoslash const me = await createJazzTestAccount(); const note = co.plainText().create("Meeting notes", { owner: me }); // ---cut--- // Insert text at a specific position note.insertBefore(8, "weekly "); // "Meeting weekly notes" // Insert after a position note.insertAfter(21, " for Monday"); // "Meeting weekly notes for Monday" // Delete a range of text note.deleteRange({ from: 8, to: 15 }); // "Meeting notes for Monday" // Apply a diff to update the entire text note.applyDiff("Team meeting notes for Tuesday"); ``` ### Applying Diffs Use `applyDiff` to efficiently update text with minimal changes: ```ts twoslash const me = await createJazzTestAccount(); // ---cut--- // Original text: "Team status update" const minutes = co.plainText().create("Team status update", { owner: me }); // Replace the entire text with a new version minutes.applyDiff("Weekly team status update for Project X"); // Make partial changes let text = minutes.toString(); text = text.replace("Weekly", "Monday"); minutes.applyDiff(text); // Efficiently updates only what changed ``` Perfect for handling user input in form controls: ```tsx twoslash const me = await createJazzTestAccount(); // ---cut--- function TextEditor({ textId }: { textId: string }) { const note = useCoState(co.plainText(), textId); return ( note &&