CoValues API Reference

Understanding how to work with CoValues is critical to building apps with Jazz. This reference guide is intended to help you get up and running by quickly demonstrating the most common use cases. For more in depth detail, you should review the linked dedicated pages.

If you have any questions, we'd be happy to chat on our Discord server!

TypeScript TypeCorresponding CoValueUsage
objectCoMapKey-value stores with pre-defined keys (struct-like)
Record<string, T>CoRecordKey-value stores with arbitrary string keys (dict-like)
T[]CoListLists
T[] (append-only)CoFeedSession-based append-only lists
stringCoPlainText/CoRichTextCollaborative text
Blob | FileFileStreamFiles
Blob | File (image)ImageDefinitionImages
number[] | Float32ArrayCoVectorEmbeddings
T | U (discriminated)DiscriminatedUnionLists of different types of items

Defining Schemas

CoValues are defined using schemas which combine CoValue types with Zod schemas. You can find out more about schemas in the schemas section of the overview guide.

schema.ts
// 1. CoMaps: Object-like with fixed keys
const ToDo = co.map({
  task: z.string(),
  completed: z.boolean(),
  dueDate: z.date().optional(),
});

// 2. CoRecords: Object-like with arbitrary string keys
const PhoneBook = co.record(z.string(), z.string()); // co.record(keyType, valueType)

// 3. CoLists: Array-like ordered list
const ToDoList = co.list(ToDo); // co.list(itemType)

// 4. CoFeeds: Array-like append-only list
const Message = co.map({ text: z.string() });
const ChatMessages = co.feed(Message); // co.feed(itemType)

// 5. CoPlainTexts/CoRichTexts: String-like
const Description = co.plainText(); // or co.richText();

// 6. FileStreams: Blob-like
const UploadedPDF = co.fileStream();

// 7. ImageDefinitions: Blob-like
const UploadedImage = co.image();

// 8. CoVectors: Array-like list of numbers/Float32Array
const Embedding = co.vector(384); // co.vector(dimensions)

// 9. DiscriminatedUnions: Union of different types of items
const ThisSchema = co.map({
  type: z.literal("this"),
  thisProperty: z.string(),
});
const ThatSchema = co.map({
  type: z.literal("that"),
  thatProperty: z.string(),
});
const MyThisOrThat = co.discriminatedUnion("type", [ThisSchema, ThatSchema]); // co.discriminatedUnion(discriminatorKey, arrayOfSchemas)

You can use the following Zod types to describe primitive data types:

TypeUsageComment
z.string()stringFor simple strings which don't need character-level collaboration
z.number()number
z.boolean()boolean
z.date()Date
z.literal()literalFor enums — pass possible values as an array
z.object()objectAn immutable, non-collaborative object
z.tuple()tupleAn immutable, non-collaborative array
z.optional(schema)optionalPass a Zod schema for an optional property with that schema type
Tip

You can also use the .optional() method on both CoValue and Zod schemas to mark them as optional.

There are three additional purpose-specific variants of the CoMap type you are likely to need while building Jazz applications.

  • co.account() — a Jazz account
  • co.profile() — a user profile
  • co.group() — a group of users

Creating CoValues

Explicit Creation

Once you have a schema, you can create new CoValue instances using that schema using the .create() static method. You should pass an initial value as the first argument to this method.

CoValue TypeExample
CoMapTask.create({ task: "Check out Jazz", completed: false })
CoRecordPhoneBook.create({ "Jenny": "867-5309" })
CoListTaskList.create([task1, task2])
CoPlainText/CoRichTextDescription.create("Hello World")
CoFeedChatMessages.create([{ message: "Hello world!" }])
FileStreamUploadedPDF.createFromBlob(myFile)
ImageDefinitioncreateImage(myFile)
note that using the helper is the preferred way to create an ImageDefinition
CoVectorEmbedding.create([0.1, 0.2, ...])
DiscriminatedUnionMyThis.create({ type: "this", ... })
note that you can only instantiate one of the schemas in the union
create vs. createFromBlob

FileStream CoValues can be created using the .create() method. This will create an empty FileStream for you to push chunks into (useful for advanced streaming cases). However, in many cases, using .createFromBlob(blobOrFile) to create a FileStream directly from a File or Blob will be more convenient.

Inline Creation

Where a schema has references, you can create nested CoValues one by one and attach them, but Jazz also allows you to create them inline by specifying their initial values.

const Task = co.map({
  title: z.string(),
  completed: z.boolean(),
});

const TaskList = co.list(Task);
const taskList = TaskList.create([
  { title: "Task 1", completed: false }, // These will create new Task CoValues
  { title: "Task 2", completed: false }, // both will be inserted into the TaskList
]);

Permissions

When creating any CoValue, the .create method accepts an optional options object as the second argument, which allows you to specify the owner of the CoValue.

const group = co.group().create();
const task = Task.create(
  { title: "Buy milk", completed: false },
  { owner: group },
);

If you don't pass an options object, or if owner is omitted, Jazz will check if there are permissions configured at a schema level.

If no permissions are set at a schema level when creating CoValues inline, a new group will be created extending the containing CoValue's ownership group. In the "Inline Creation" example above, a new group would be created for each task, each extending the ownership group of the taskList CoList.

It is a good idea to read through the permissions section to understand how to manage permissions on CoValues, as unlike in other databases, permissions are fundamental to how Jazz works at a low level, rather than a supporting feature.

Loading and Reading CoValues

In order to read data, you need to load a CoValue instance. There are several ways to do this. We recommend using Jazz with a framework, as this allows you to create reactive subscriptions to CoValues easily, but it is also possible to load a CoValue instance using the .load() static method on the schema.

Once you have a loaded CoValue instance, you can normally read it similarly to the corresponding TypeScript type.

CoMap (and the CoRecord sub-type)

Behaves like a TypeScript object when reading.

// CoMap: Access fixed keys
console.log(user.name); // "Alice"

// CoRecord: Access arbitrary keys
const phone = phoneBook["Jenny"];

// Iteration works as with a TypeScript object
for (const [name, number] of Object.entries(phoneBook)) {
  console.log(name, number);
}

Read more →

CoList

Behaves like a TypeScript array when reading.

const firstTask = taskList[0];
const length = taskList.length;

// Iteration works as with a TypeScript array
taskList.map((task) => console.log(task.title));
for (const task of taskList) {
  // Do something
}

Read more →

CoPlainText/CoRichText

Behaves like a TypeScript string when reading.

// String operations
const summary = description.substring(0, 100);

Read more →

Note: Although CoPlainTexts/CoRichTexts behave the same as strings in most circumstances, they are not strings. If you need an actual string type, you can use the toString() method.

CoFeed

CoFeeds do not correspond neatly to a TypeScript type. They are collaborative streams of entries split by session/account, and so there are various ways to access the underlying data.

// Get the feed for a specific session (e.g. this browser tab)
const thisSessionsFeed = chatMessages.perSession[thisSessionId]; // or .inCurrentSession as shorthand
const latestMessageFromThisSession = thisSessionsFeed.value;
const allMessagesFromThisSession = thisSessionsFeed.all;

// Get the feed for a specific account
const accountFeed = chatMessages.perAccount[accountId];
const latestMessageFromThisAccount = accountFeed.value;
const allMessagesFromThisAccount = accountFeed.all;

// Get the feed for my account
const myFeed = chatMessages.byMe; // shorthand for chatMessages.perAccount[myAccountId]
const latestMessageFromMyAccount = myFeed?.value;
const allMessagesFromMyAccount = myFeed?.all;

// Iterate over all entries in a CoFeed
for (const userId of Object.keys(chatMessages.perAccount)) {
  const accountFeed = chatMessages.perAccount[userId];
  for (const entry of accountFeed.all) {
    if (entry.value.$isLoaded) {
      console.log(entry.value);
    }
  }
}

The .all property allows you to iterate over all entries in a per-session or per-account feed. If you need to convert a feed to an array, you can use Array.from() or the spread operator.

Read more →

CoFeed Structure
┌────────┐
│ CoFeed └────────────────────────────────────────────────────────────┐ 
│ ┌────────┐                                                          │
│ │ userA  └────────────────────────────────────────────────────────┐ │
│ │ ┌───────────┐                                                   │ │
│ │ │ Session 1 └─────────────────────────────────────────────────┐ │ │
│ │ │ ┌────────────────┐  ┌─────────────────┐ ┌─────────────────┐ │ │ │
│ │ │ │ value: someVal │  │ value: someVal2 │ │ value: someVal3 │ │ │ │
│ │ │ │ by: userA      │  │ by: userA       │ │ by: userA       │ │ │ │
│ │ │ │ madeAt: 10:00  │  │ madeAt: 10:01   │ │ madeAt: 10:02   │ │ │ │
│ │ │ └────────────────┘  └─────────────────┘ └─────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ ┌───────────┐                                                   │ │
│ │ │ Session 2 └─────────────────────────────────────────────────┐ │ │
│ │ │ ┌─────────────────┐                                         │ │ │
│ │ │ │ value: someVal3 │                                         │ │ │
│ │ │ │ by: userA       │                                         │ │ │
│ │ │ │ madeAt: 12:00   │                                         │ │ │
│ │ │ └─────────────────┘                                         │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ ┌───────┐                                                           │
│ │ userB └─────────────────────────────────────────────────────────┐ │
│ │ ┌───────────┐                                                   │ │
│ │ │ Session 1 └─────────────────────────────────────────────────┐ │ │
│ │ │ ┌─────────────────┐                                         │ │ │
│ │ │ │ value: someVal4 │                                         │ │ │
│ │ │ │ by: userB       │                                         │ │ │
│ │ │ │ madeAt: 10:05   │                                         │ │ │
│ │ │ └─────────────────┘                                         │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘

FileStream

FileStreams can be converted to Blob types or read as binary chunks.

// Get raw data chunks and metadata.
// Optionally pass { allowUnfinished: true } to get chunks of a FileStream which is not yet fully synced.
const fileData = fileStream.getChunks({ allowUnfinished: true });

// Convert to a Blob for use in a <a> tag or <iframe>
const fileBlob = fileStream.toBlob();
const fileUrl = fileBlob && URL.createObjectURL(fileBlob);

Read more →

ImageDefinition

Although you can read an ImageDefinition as a CoMap, Jazz provides an <Image /> component for front-end frameworks which vastly simplifies their use.

If you want to use the ImageDefinition type without a framework, or you need access to the image itself you can use the loadImageBySize helper.

// Component-based usage (recommended)
<Image image={productImage} width = {300} />
// Imperative usage: Access the highest available resolution
import { loadImageBySize } from "jazz-tools/media";

// Not guaranteed to exist if no variant exists that fulfills your constraints
const imageDef = await loadImageBySize(productImage, 300, 400); // Takes either an ImageDefinition or an ID, and returns a FileStream.

Once you have a loaded image, you can convert it to a blob with the .toBlob() method, as you would with any other FileStream. You can then use this to create a URL to use as an <img> source.

const blob = imageDef && imageDef.image.toBlob();
const url = blob && URL.createObjectURL(blob); // Don't forget to clean this up when you're done!
myImg.src = url ?? "";

Read more →

CoVector

You can read a CoVector in the same way as you'd read an array, but in most cases, you'll want to use the cosineSimilarity helper.

// Calculate similarity between two vectors
const similarity = myEmbedding.$jazz.cosineSimilarity(targetVector);

Read more →

DiscriminatedUnion

After loading a DiscriminatedUnion, you will be able to read any property that is common to all members, otherwise, you will need to narrow the type to be able to access properties which are only on some subset of members of the union.

// Use the discriminator to check the type
if (item.type === "task") {
  console.log(item.title);
} else if (item.type === "note") {
  console.log(item.content);
}

Read more →

Updating CoValues

Most CoValues are updated using mutation methods in the .$jazz namespace. Some CoValues, such as CoPlainText and CoRichText, have methods available directly on the instance itself.

Updating CoMaps (and the CoRecord sub-type)

CoRecords will allow you to arbitrarily set and delete keys, while CoMaps will only allow you to set and delete keys defined in the schema.

  • .$jazz.set(key, value): void
  • .$jazz.delete(key): void
  • .$jazz.applyDiff(diff): void
// Set or update a property
todo.$jazz.set("task", "Try out Jazz");

// Delete an optional property
todo.$jazz.delete("dueDate");

// Update multiple properties at once
todo.$jazz.applyDiff({
  task: "Apply a diff to update a task",
  completed: true,
});

Read more →

Updating CoLists

CoLists implement most of the usual array mutation methods under the .$jazz namespace (except sort and reverse).

  • .$jazz.push(...items: T[]): number
  • .$jazz.unshift(...items: T[]): number
  • .$jazz.pop(): T | undefined
  • .$jazz.shift(): T | undefined
  • .$jazz.remove(...indexes: number[]): T[]
  • .$jazz.remove(predicate: (item: T, index: number, coList: L) => boolean): T[]
    • Removes elements by index(es) or predicate. Returns the removed elements.
  • .$jazz.retain(predicate: (item: T, index: number, coList: L) => boolean): T[]
    • Retains only elements matching the predicate. Returns the removed elements.
  • .$jazz.splice(start: number, deleteCount: number, ...items: T[]): T[]
  • .$jazz.applyDiff(result: T[]): L
    • Updates the list to match another list, efficiently calculated
// Add items
tasks.$jazz.push(newTask);
tasks.$jazz.unshift(importantTask);

// Remove items
const removed = tasks.$jazz.remove(0); // Remove by index, returns removed items
tasks.$jazz.remove((task) => task.completed); // Remove by predicate
const lastTask = tasks.$jazz.pop(); // Remove and return last item
const task1 = tasks.$jazz.shift(); // Remove and return first item

// Retain only matching items
tasks.$jazz.retain((task) => !task.completed); // Keep only incomplete tasks

// Replace/Move
tasks.$jazz.splice(1, 1, replacementTask);

if (!task1?.$isLoaded) throw new Error(); // [!code hide]
// Efficiently update to match another list
tasks.$jazz.applyDiff([task1, task2, task3]); // Updates list to match exactly

Read more →

Updating CoPlainTexts/CoRichTexts

CoPlainText and CoRichText methods are mostly available directly on the instance.

  • insertAfter(after: number, text: string): void
  • insertBefore(before: number, text: string): void
  • deleteRange(range: { from: number, to: number }): void
  • .$jazz.applyDiff(newText: string): void
const message = co.plainText().create("Hello world!"); // Hello world
message.insertAfter(4, ","); // Hello, world!
message.insertBefore(7, "everybody in the "); // Hello, everybody in the world!
message.deleteRange({ from: 16, to: 29 }); // Hello, everybody!

// Update efficiently
message.$jazz.applyDiff("Hello, my Jazzy friends!"); // 'Hello, ' has not changed and will not be updated
// Use directly with templating languages (e.g. React, Svelte)
<span>{message}</span>

Read more →

Updating CoFeeds

CoFeeds are append-only. The only mutation you can perform is push() (in the .$jazz namespace), which adds a new entry to the feed.

feed.$jazz.push(newMessage);

Read more →

Updating FileStreams

If you wish to push binary data into an existing FileStream, you can start it with metadata, then push chunks of data, and finally end it.

  • start(metadata: { mimeType: string; fileName?: string; totalSizeBytes?: number; }): void
  • push(chunk: Uint8Array): void
  • end(): void
const myUploadedPDF = UploadedPDF.create(); // Advanced usage: manual chunk streaming
myUploadedPDF.start({ mimeType: "application/pdf" });
myUploadedPDF.push(chunks); // Uint8Array
myUploadedPDF.end();

Read more →

Updating ImageDefinitions

If you would like to update an ImageDefinition, you will need to create a new FileStream and set it on the ImageDefinition. The ImageDefinition behaves like a CoRecord with keys for each image resolution as <width>x<height>, and the values are FileStream instances. You can replace resolutions or add new ones.

const w = 800;
const h = 600;
const imageFile = await co.fileStream().createFromBlob(some800x600Blob);
myUploadedImage.$jazz.set(`${w}x${h}`, imageFile);

Read more →

Updating CoVectors

CoVectors are immutable. You cannot update them. You should create a new CoVector to replace the old one.

Read more →

Updating DiscriminatedUnions

When updating a discriminated union, you should first narrow the type using the discriminator. After that, you'll be able to update it using the $jazz.set, $jazz.applyDiff, or $jazz.delete methods.

const myLoadedThisOrThat = await MyThisOrThat.load("co_z...");

if (myLoadedThisOrThat.$isLoaded && myLoadedThisOrThat.type === "this") {
  myLoadedThisOrThat.$jazz.set("thisProperty", "Only available on 'this'!");
} else if (myLoadedThisOrThat.$isLoaded && myLoadedThisOrThat.type === "that") {
  myLoadedThisOrThat.$jazz.set("thatProperty", "Only available on 'that'!");
}

Read more →

Shared Properties and Methods

Universal CoValue Interface

These properties and methods are available directly on every CoValue instance.

FeatureProperty/MethodDescription
Loading$isLoadedCheck if the CoValue is fully loaded and ready to read.
Inspection.toJSON()Get a plain JS representation of the CoValue.

Common $jazz Interface

Every CoValue instance has a .$jazz namespace with the following common utilities (some are only available on specific CoValue types).

FeatureProperty/MethodDescription
Basicsid, ownerKey metadata.
Life-cyclecreatedAt, lastUpdatedAt, createdByAudit trails and creation timestamps.
Reactivitysubscribe(), waitForSync()Listen for changes or wait for network persistence.
LoadingloadingState, ensureLoaded()Check loading state or load a copy of the CoValue with a different resolution depth.
Inspectionrefs, hasAvailable on CoMaps, check for the existence of references/specific keys.
Version ControlisBranched, branchName, unstable_merge()Utilities for local branching and merging.

Metadata

  • .$jazz.id: ID<CoValue>
    • The definitive, globally unique ID (e.g., co_zXy...).
  • .$jazz.owner: Group
    • The group that this CoValue is owned by.
  • .$jazz.createdAt: number
    • The time that this CoValue was created (in milliseconds since the Unix epoch).
  • .$jazz.createdBy: ID<Account> | undefined
    • The ID of the account that originally created this CoValue (undefined for accounts themselves).
  • .$jazz.lastUpdatedAt: number
    • The time that this CoValue was last updated (in milliseconds since the Unix epoch).
  • .$jazz.refs: Record<key, Ref> | undefined (CoMap/CoRecord only)
    • Access nested CoValues as references (with IDs) without loading them. Useful for checking if a reference exists or getting its ID without triggering a network fetch.
    // Check if a reference exists without loading it
    if (person.$jazz.refs.pet) {
      console.log("Pet ID:", person.$jazz.refs.pet.id);
    }
  • .$jazz.has(key): boolean (CoMap/CoRecord only)
    • Checks if a key exists in the CoValue.

Loading

  • $isLoaded: boolean
    • True if the CoValue is fully loaded and ready to read.
  • .$jazz.loadingState: "loading" | "loaded" | "unavailable" | "unauthorized"
    • Current state of the CoValue. See Loading States for details on handling each case.
  • .$jazz.ensureLoaded<T>(resolveQuery: { resolve: ResolveQuery<T> }): Promise<Resolved<T>>
    • Waits for nested references to load to the specified depth.
    • Returns: A new typed instance Resolved<T> where specified properties are guaranteed to be fully loaded.
    const profile = await shallowProfile.$jazz.ensureLoaded({
      resolve: {
        avatar: true,
      },
    });
    console.log(profile.avatar); // Safe to access
    Note: When calling .$jazz.ensureLoaded() on a discriminated union, you must first narrow the type using the discriminator property. If any nested reference cannot be loaded, the entire operation may fail (see Loading Errors).
    Mental ModelensureLoaded returns a copy of the CoValue where the specific fields you requested in the resolve query are guaranteed to be present and non-null.

Subscription & Sync

  • .$jazz.subscribe(listener: (updated: this) => void): () => void
    • Listen for any changes to this CoValue. Only needed if you are manually handling your subscriptions.
    • Returns an unsubscribe function for manual teardown.
    • For error handling in manual subscriptions, see Manual Subscriptions.
    const unsub = person.$jazz.subscribe((updatedPerson) => {
      console.log("Person updated:", updatedPerson);
    });
  • .$jazz.waitForSync(): Promise<void>
    • Waits until changes are synced to other peers (useful for tests or critical saves).

Version Control

  • .$jazz.branchName: string | undefined
    • If this CoValue is branched, the name of the branch, otherwise undefined.
  • .$jazz.isBranched: boolean
    • Whether this CoValue is branched.
  • .$jazz.unstable_merge(): void
    • Merges this branched CoValue back into the main branch. Only works when the CoValue is branched.
Was this page helpful?