# Jazz
## 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](https://jazz.tools/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
\--- Section applies only to react ---
We'll be using Next.js for this guide per the [React team's recommendation](https://react.dev/learn/creating-a-react-app), but Jazz works great with vanilla React and other full-stack frameworks too.
You can accept the defaults for all the questions, or customise the project as you like.
##### npm:
```sh
npx create-next-app@latest --typescript jazzfest
cd jazzfest
```
##### pnpm:
```sh
pnpx create-next-app@latest --typescript jazzfest
cd jazzfest
```
\--- End of react specific section ---
\--- Section applies only to svelte ---
We'll be using SvelteKit for this guide, per the [Svelte team's recommendation](https://svelte.dev/docs/svelte/getting-started), but Jazz works great with vanilla Svelte too.
You can accept the defaults for all the questions, or customise the project as you like.
##### npm:
```sh
npx sv create --types ts --template minimal jazzfest
cd jazzfest
```
##### pnpm:
```sh
pnpx sv create --types ts --template minimal jazzfest
cd jazzfest
```
\--- End of svelte specific section ---
**Note: Requires Node.js 20+**
## Install Jazz
The `jazz-tools` package includes everything you're going to need to build your first Jazz app.
##### npm:
```sh
npm install jazz-tools
```
##### pnpm:
```sh
pnpm add 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.
\--- Section applies only to react ---
**File name: .env**
\--- End of react specific section ---
\--- Section applies only to svelte ---
**File name: .env**
\--- End of svelte specific section ---
##### React:
```bash
NEXT_PUBLIC_JAZZ_API_KEY="you@example.com" # or your API key
```
##### Svelte:
```bash
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.
\--- Section applies only to react ---
**File name: app/schema.ts**
\--- End of react specific section ---
\--- Section applies only to svelte ---
**File name: src/lib/schema.ts**
\--- End of svelte specific section ---
```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.
\--- Section applies only to react ---
**File name: app/components/JazzWrapper.tsx**
```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.
import { JazzReactProvider } 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 (
{children}
);
}
```
\--- End of react specific section ---
\--- Section applies only to react ---
**File name: app/layout.tsx**
\--- End of react specific section ---
\--- Section applies only to svelte ---
**File name: src/routes/+layout.svelte**
\--- End of svelte specific section ---
##### React:
```tsx
import { JazzWrapper } from "@/app/components/JazzWrapper";
export default function RootLayout({
children
}: {
children: React.ReactNode
}) {
return (
{children}
);
}
```
##### Svelte:
```svelte
{@render children?.()}
```
## Start your app
Moment of truth β time to start your app and see if it works.
##### npm:
```bash
npm run dev
```
##### pnpm:
```bash
pnpm run dev
```
\--- Section applies only to react ---
If everything's going according to plan, you should see the default Next.js welcome page!
\--- End of react specific section ---
\--- Section applies only to svelte ---
If everything's going according to plan, you should see the default SvelteKit welcome page!
\--- End of svelte specific section ---
### Not loading?
If you're not seeing the welcome page:
\--- Section applies only to react ---
* Check you wrapped your app with the Jazz Provider in `app/layout.tsx`
* Check your schema is properly defined in `app/schema.ts`
\--- End of react specific section ---
\--- Section applies only to svelte ---
* Check you wrapped your app with the Jazz Provider in `src/routes/+layout.svelte`
* Check your schema is properly defined in `src/lib/schema.ts`
\--- End of svelte specific section ---
**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.
\--- Section applies only to react ---
**File name: app/components/NewBand.tsx**
\--- End of react specific section ---
\--- Section applies only to svelte ---
**File name: src/lib/components/NewBand.svelte**
\--- End of svelte specific section ---
##### React:
```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) return; // not loaded yet
me.root.myFestival.$jazz.push({ name });
setName("");
};
return (
setName(e.target.value)}
/>
);
}
```
##### Svelte:
```svelte
```
## Display your data
Now we've got a way to create data, so let's add a component to display it.
\--- Section applies only to react ---
**File name: app/components/Festival.tsx**
\--- End of react specific section ---
\--- Section applies only to svelte ---
**File name: src/lib/components/Festival.svelte**
\--- End of svelte specific section ---
##### React:
```tsx
"use client";
import { useAccount } from "jazz-tools/react";
import { JazzFestAccount } from "@/app/schema";
export function Festival() {
const { me } = useAccount(JazzFestAccount, {
resolve: { root: { myFestival: true } }
});
if (!me) return null; // not loaded yet
return (
{me?.root.myFestival.map((band) => band &&
{band.name}
)}
);
}
```
##### Svelte:
```svelte
{#each me.current?.root.myFestival || [] as band}
{band?.name}
{/each}
```
## Put it all together
You've built all your components, time to put them together.
\--- Section applies only to react ---
**File name: app/page.tsx**
\--- End of react specific section ---
\--- Section applies only to svelte ---
**File name: src/routes/+page.svelte**
\--- End of svelte specific section ---
##### React:
```tsx
import { Festival } from "@/app/components/Festival";
import { NewBand } from "@/app/components/NewBand";
export default function Home() {
return (
πͺ My Festival
);
}
```
##### Svelte:
```svelte
πͺ 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.
\--- Section applies only to react ---
Psst! Got a few more minutes and want to add Server Side Rendering to your app? [We've got you covered!](/docs/server-side/ssr)
\--- End of react specific section ---
## 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
### 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.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.
```ts
// schema.ts
import { co, z } from "jazz-tools";
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
// @filename: schema.ts
import { co, z, CoMap, CoList } from "jazz-tools";
export const ListOfTasks = co.list(z.string());
export const TodoProject = co.map({
title: z.string(),
tasks: ListOfTasks,
});
// @filename: app.ts
// ---cut---
// app.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
// @filename: schema.ts
import { co, z, CoMap, CoList } from "jazz-tools";
export const ListOfTasks = co.list(z.string());
export const TodoProject = co.map({
title: z.string(),
tasks: ListOfTasks,
});
// @filename: app.ts
// ---cut---
// app.ts
import { Group } from "jazz-tools";
import { TodoProject, ListOfTasks } from "./schema";
const group = Group.create().makePublic();
const project = 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
// schema.ts
import { co, z } from "jazz-tools";
// ---cut---
const Task = co.map({
title: z.string(),
completed: z.boolean(),
});
```
Or record-like CoMaps (key-value pairs, where keys are always `string`):
```ts
import { co, z } from "jazz-tools";
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/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
import { co, z } from "jazz-tools";
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/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
import { co, z } from "jazz-tools";
const Task = co.map({
title: z.string(),
completed: z.boolean(),
});
// ---cut---
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
import { co, z } from "jazz-tools";
// ---cut---
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
import { co, z } from "jazz-tools";
// ---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 = 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/)):
```ts
import { co, z } from "jazz-tools";
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
import { z } from "jazz-tools";
// ---cut---
z.string(); // For simple strings
z.number(); // For numbers
z.boolean(); // For booleans
z.date(); // For dates
z.literal(["waiting", "ready"]); // For enums
```
Finally, for more complex JSON data, that you _don't want to be collaborative internally_ (but only ever update as a whole), you can use more complex Zod types.
For example, you can use `z.object()` to represent an internally immutable position:
```ts
import { co, z } from "jazz-tools";
// ---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
import { co, z } from "jazz-tools";
// ---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/core-concepts/subscription-and-loading).
In Schemas, you declare references by just using the schema of the referenced CoValue:
```ts
import { co, z } from "jazz-tools";
// ---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 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
import { co, z } from "jazz-tools";
const Pet = co.map({
name: z.string(),
});
// ---cut---
const Person = 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
import { co, z } from "jazz-tools";
// ---cut---
const Person = co.map({
name: z.string(),
get bestFriend() {
return Person;
}
});
```
You can use the same technique for mutually recursive references:
```ts
// ---cut---
import { co, z } from "jazz-tools";
const Person = co.map({
name: z.string(),
get friends() {
return ListOfPeople;
}
});
const ListOfPeople = 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
import { co, z } from "jazz-tools";
function differenceInYears(date1: Date, date2: Date) {
const diffTime = Math.abs(date1.getTime() - date2.getTime());
return Math.ceil(diffTime / (1000 * 60 * 60 * 24 * 365.25));
}
// ---cut---
const Person = co.map({
firstName: z.string(),
lastName: z.string(),
dateOfBirth: z.date(),
});
type Person = co.loaded;
export function getPersonFullName(person: Person) {
return `${person.firstName} ${person.lastName}`;
}
export function getPersonAgeAsOf(person: Person, date: Date) {
return differenceInYears(date, person.dateOfBirth);
}
const person = Person.create({
firstName: "John",
lastName: "Doe",
dateOfBirth: new Date("1990-01-01"),
});
const fullName = getPersonFullName(person);
const age = getPersonAgeAsOf(person, new Date());
```
Similarly, you can encapsulate logic needed to update CoValues:
```ts
import { co, z } from "jazz-tools";
// ---cut---
const Person = co.map({
firstName: z.string(),
lastName: z.string(),
});
type Person = co.loaded;
export function updatePersonName(person: Person, fullName: string) {
const [firstName, lastName] = fullName.split(" ");
person.$jazz.set("firstName", firstName);
person.$jazz.set("lastName", lastName);
}
const person = Person.create({
firstName: "John",
lastName: "Doe",
});
console.log(person.firstName, person.lastName) // John Doe
updatePersonName(person, "Jane Doe");
console.log(person.firstName, person.lastName) // Jane Doe
```
### 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
const Member = co.map({
name: z.string(),
});
// ---cut---
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
import { co, z } from "jazz-tools";
// ---cut---
const Inventory = co.record(z.string(), z.number());
```
To instantiate a CoMap:
```ts
import { co, z } from "jazz-tools";
import { createJazzTestAccount } from 'jazz-tools/testing';
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: co.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
import { Group, co, z } from "jazz-tools";
import { createJazzTestAccount } from 'jazz-tools/testing';
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: co.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/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
import { co, z } from "jazz-tools";
import { createJazzTestAccount } from 'jazz-tools/testing';
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: co.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
import { co, z } from "jazz-tools";
import { createJazzTestAccount } from 'jazz-tools/testing';
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: co.optional(Member),
});
const project = Project.create(
{
name: "Spring Planting",
startDate: new Date("2025-03-20"),
status: "planning"
},
);
// ---cut---
if (project.coordinator) {
console.log(project.coordinator.name); // Safe access
}
```
### Recursive references
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 Member = co.map({
name: z.string(),
});
// ---cut---
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 Member = co.map({
name: z.string(),
});
// ---cut---
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 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
import { co, z } from "jazz-tools";
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
import { co, z } from "jazz-tools";
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
import { co, z } from "jazz-tools";
import { createJazzTestAccount } from 'jazz-tools/testing';
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
To update a CoMap's properties, use the `$jazz.set` method:
```ts
import { co, z } from "jazz-tools";
import { createJazzTestAccount } from 'jazz-tools/testing';
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: co.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.$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
import { co, z } from "jazz-tools";
// ---cut---
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
import { co, z } from "jazz-tools";
import { createJazzTestAccount } from 'jazz-tools/testing';
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: co.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.$jazz.set("name", "Spring Vegetable Planting"); // β Valid string
// @errors: 2345
project.$jazz.set("startDate", "2025-03-15"); // β Type error: expected Date
```
### Soft Deletion
Implementing a soft deletion pattern by using a `deleted` flag allows you to maintain data for potential recovery and auditing.
```ts
import { co, z } from "jazz-tools";
// ---cut---
const Project = co.map({
name: z.string(),
deleted: z.optional(z.boolean()), // [!code ++]
});
```
When an object needs to be "deleted", instead of removing it from the system, the deleted flag is set to true. This gives us a property to omit it in the future.
### Deleting Properties
You can delete properties from CoMaps:
```ts
import { co, z } from "jazz-tools";
import { createJazzTestAccount } from 'jazz-tools/testing';
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: co.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---
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
import { co, z } from "jazz-tools";
// ---cut---
const Task = co
.map({
done: z.boolean(),
text: co.plainText(),
version: z.literal([1, 2]),
priority: z.enum(["low", "medium", "high"]), // new field
})
.withMigration((task) => {
if (task.version === 1) {
task.$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
import { co, z } from "jazz-tools";
// ---cut---
const TaskV1 = co.map({
version: z.literal(1),
done: z.boolean(),
text: z.string(),
});
const TaskV2 = co.map({
// We need to be more strict about the version to make the
// discriminated union work
version: z.literal(2),
done: z.boolean(),
text: z.string(),
priority: z.enum(["low", "medium", "high"]),
}).withMigration((task) => {
// @ts-expect-error - check if we need to run the migration
if (task.version === 1) {
task.$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 { createJazzTestAccount } from 'jazz-tools/testing';
const me = await createJazzTestAccount();
// ---cut---
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
import { co, z, Group, ID } from "jazz-tools";
const Task = co.map({
text: z.string(),
});
// ---cut---
// This will not work as `learning-jazz` is not a CoValue ID
const myTask = await Task.load("learning-jazz");
```
To make it possible to use human-readable identifiers Jazz lets you to define a `unique` property on CoMaps.
Then the CoValue ID is deterministically derived from the `unique` property and the owner of the CoMap.
```ts
import { co, z, Group, ID } from "jazz-tools";
const Task = co.map({
text: z.string(),
});
const Project = co.map({
name: z.string(),
startDate: z.date(),
endDate: z.optional(z.date()),
});
const project = Project.create({
name: "My project",
startDate: new Date("2025-04-01"),
endDate: new Date("2025-04-04"),
});
// ---cut---
// Given the project owner, myTask will have always the same id
const learnJazzTask = await Task.create({
text: "Let's learn some Jazz!",
}, {
unique: "learning-jazz",
owner: project.$jazz.owner, // Different owner, different id
});
```
Now you can use `CoMap.loadUnique` to easily load the CoMap using the human-readable identifier:
```ts
import { co, z, Group, ID } from "jazz-tools";
const Task = co.map({
text: z.string(),
});
const Project = co.map({
name: z.string(),
startDate: z.date(),
endDate: z.optional(z.date()),
});
const project = Project.create({
name: "My project",
startDate: new Date("2025-04-01"),
endDate: new Date("2025-04-04"),
});
// ---cut---
const learnJazzTask = await Task.loadUnique(
"learning-jazz",
project.$jazz.owner.$jazz.id
);
```
It's also possible to combine the create+load operation using `CoMap.upsertUnique`:
```ts
import { co, z, Group, ID } from "jazz-tools";
const Task = co.map({
text: z.string(),
});
const Project = co.map({
name: z.string(),
startDate: z.date(),
endDate: z.optional(z.date()),
});
const project = Project.create({
name: "My project",
startDate: new Date("2025-04-01"),
endDate: new Date("2025-04-04"),
});
// ---cut---
const learnJazzTask = await Task.upsertUnique(
{
value: {
text: "Let's learn some Jazz!",
},
unique: "learning-jazz",
owner: project.$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)
### 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
const Task = co.map({
title: z.string(),
status: z.literal(["todo", "in-progress", "complete"]),
});
// ---cut---
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
import { co, z } from "jazz-tools";
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([
{ title: "Prepare soil beds", status: "in-progress" },
{ title: "Order compost", status: "todo" }
]);
```
### Ownership
Like other CoValues, you can specify ownership when creating CoLists.
```ts
import { Group, co, z } from "jazz-tools";
import { createJazzTestAccount } from 'jazz-tools/testing';
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/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
import { co, z } from "jazz-tools";
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
Methods to update a CoList's items are grouped inside the `$jazz` namespace:
```ts
import { co, z } from "jazz-tools";
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.$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
import { co, z } from "jazz-tools";
// ---cut---
const Task = co.map({
title: z.string(),
status: z.literal(["todo", "in-progress", "complete"]),
deleted: z.optional(z.boolean()) // [!code ++]
});
type Task = typeof Task;
const ListOfTasks = co.list(Task);
type ListOfTasks = typeof ListOfTasks;
export function getCurrentTasks(list: co.loaded) {
return list.filter(
(task): task is co.loaded => !task?.deleted
);
}
async function main() {
const myTaskList = ListOfTasks.create([]);
myTaskList.$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
import { co, z } from "jazz-tools";
const ListOfResources = co.list(z.string());
const resources = ListOfResources.create([
"Tomatoes",
"Cucumber",
"Peppers",
]);
// ---cut---
// 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
import { co, z } from "jazz-tools";
const ListOfResources = co.list(z.string());
const resources = ListOfResources.create([
"Tomatoes",
"Cucumber",
"Peppers",
]);
// ---cut---
// 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
import { co, z } from "jazz-tools";
const ListOfResources = co.list(z.string());
const resources = ListOfResources.create([]);
// ---cut---
// 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
import { co, z } from "jazz-tools";
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.$jazz.push("Carrots"); // β Valid string
// @errors: 2345
resources.$jazz.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
import * as React from "react";
const Task = co.map({
title: z.string(),
status: z.literal(["todo", "in-progress", "complete"]),
});
// ---cut---
import { co, z } from "jazz-tools";
const ListOfTasks = co.list(Task);
// React example
function TaskList({ tasks }: { tasks: co.loaded }) {
return (
{tasks.map(task => (
task ? (
{task.title} - {task.status}
): null
))}
);
}
```
#### Managing Relations
CoLists can be used to create one-to-many relationships:
```ts
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] }
```
### 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
import { co, z } from "jazz-tools";
// ---cut---
// Define a schema for feed items
const Activity = co.map({
timestamp: z.date(),
action: z.literal(["watering", "planting", "harvesting", "maintenance"]),
notes: z.optional(z.string()),
});
export type Activity = co.loaded;
// Define a feed of garden activities
const ActivityFeed = co.feed(Activity);
// Create a feed instance
const activityFeed = ActivityFeed.create([]);
```
### Ownership
Like other CoValues, you can specify ownership when creating CoFeeds.
```ts
import { Group, co, z } from "jazz-tools";
import { createJazzTestAccount } from 'jazz-tools/testing';
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/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
import { co, z, SessionID } from "jazz-tools";
import { createJazzTestAccount } from 'jazz-tools/testing';
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.$jazz.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
import { co, z, SessionID } from "jazz-tools";
import { createJazzTestAccount } from 'jazz-tools/testing';
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.$jazz.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
import { co, z, SessionID } from "jazz-tools";
import { createJazzTestAccount } from 'jazz-tools/testing';
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.$jazz.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
import { co, z, SessionID } from "jazz-tools";
import { createJazzTestAccount } from 'jazz-tools/testing';
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.$jazz.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
import { co, z, SessionID } from "jazz-tools";
import { createJazzTestAccount } from 'jazz-tools/testing';
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.$jazz.id;
const sessionId = `${me.$jazz.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
import { co, z, SessionID } from "jazz-tools";
import { createJazzTestAccount } from 'jazz-tools/testing';
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
import { co, z } from "jazz-tools";
import { createJazzTestAccount } from 'jazz-tools/testing';
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.$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
import { co, z } from "jazz-tools";
import { createJazzTestAccount } from 'jazz-tools/testing';
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.$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
import { co, z } from "jazz-tools";
import { createJazzTestAccount } from 'jazz-tools/testing';
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.$jazz.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
import { co, z } from "jazz-tools";
import { createJazzTestAccount } from 'jazz-tools/testing';
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.$jazz.id;
// ---cut---
const accountFeed = activityFeed.perAccount[accountId];
// Get the timestamp of the last update
console.log(accountFeed?.madeAt);
// Get the timestamp of each entry
for (const entry of accountFeed.all) {
console.log(entry.madeAt);
}
```
## Best Practices
### When to Use CoFeeds
* **Use CoFeeds when**:
* You need to track per-user/per-session data
* Time-based information matters (activity logs, presence)
* **Consider alternatives when**:
* Data needs to be collaboratively edited (use CoMaps or CoLists)
* You need structured relationships (use CoMaps/CoLists with references)
### CoTexts
# CoTexts
Jazz provides two CoValue types for collaborative text editing, collectively referred to as "CoText" values:
* **`co.plainText()`** for simple text editing without formatting
* **`co.richText()`** for rich text with HTML-based formatting (extends `co.plainText()`)
Both types enable real-time collaborative editing of text content while maintaining consistency across multiple users.
**Note:** If you're looking for a quick way to add rich text editing to your app, check out [our prosemirror plugin](#using-rich-text-with-prosemirror).
```ts
import { co } from "jazz-tools";
import { createJazzTestAccount } from 'jazz-tools/testing';
const me = await createJazzTestAccount();
// ---cut---
const note = co.plainText().create("Meeting notes", { owner: me });
// 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
import { co, z } from "jazz-tools";
// ---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
import { co, z } from "jazz-tools";
import { createJazzTestAccount } from 'jazz-tools/testing';
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
import { co, z, Group } from "jazz-tools";
import { createJazzTestAccount } from 'jazz-tools/testing';
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/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
import { co, z } from "jazz-tools";
import { createJazzTestAccount } from 'jazz-tools/testing';
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
```
\--- Section applies only to react ---
When using CoTexts in JSX, you can read the text directly:
```tsx
import * as React from "react";
import { co, z } from "jazz-tools";
import { createJazzTestAccount } from 'jazz-tools/testing';
const me = await createJazzTestAccount();
const note = co.plainText().create("Meeting notes", { owner: me });
// ---cut---
<>
{note.toString()}
{note}
>
```
\--- End of react specific section ---
## Making Edits
Insert and delete text with intuitive methods:
```ts
import { co, z } from "jazz-tools";
import { createJazzTestAccount } from 'jazz-tools/testing';
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.$jazz.applyDiff("Team meeting notes for Tuesday");
```
### Applying Diffs
Use `applyDiff` to efficiently update text with minimal changes:
```ts
import { co, z } from "jazz-tools";
import { createJazzTestAccount } from 'jazz-tools/testing';
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.$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:
\--- Section applies only to react ---
```tsx
import { co, z } from "jazz-tools";
import { createJazzTestAccount } from 'jazz-tools/testing';
import { useCoState } from "jazz-tools/react";
import React, { useState } from "react";
const me = await createJazzTestAccount();
// ---cut---
function TextEditor({ textId }: { textId: string }) {
const note = useCoState(co.plainText(), textId);
return (
note &&