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 for more details on primitive fields):

import { import coco, import zz } from "jazz-tools";

const 
const Project: CoMapSchema<{
    name: z.z.ZodString;
    startDate: z.z.ZodDate;
    status: z.z.ZodLiteral<"planning" | "active" | "completed">;
    coordinator: z.ZodOptional<CoMapSchema<{
        name: z.z.ZodString;
    }>>;
}>
Project
= import coco.
map<{
    name: z.z.ZodString;
    startDate: z.z.ZodDate;
    status: z.z.ZodLiteral<"planning" | "active" | "completed">;
    coordinator: z.ZodOptional<CoMapSchema<{
        name: z.z.ZodString;
    }>>;
}>(shape: {
    name: z.z.ZodString;
    startDate: z.z.ZodDate;
    status: z.z.ZodLiteral<"planning" | "active" | "completed">;
    coordinator: z.ZodOptional<CoMapSchema<{
        name: z.z.ZodString;
    }>>;
}): CoMapSchema<...>
export map
map
({
name: z.z.ZodStringname: import zz.
function string(params?: string | z.z.core.$ZodStringParams): z.z.ZodString
export string
string
(),
startDate: z.z.ZodDatestartDate: import zz.
function date(params?: string | z.z.core.$ZodDateParams): z.z.ZodDate
export date
date
(),
status: z.z.ZodLiteral<"planning" | "active" | "completed">status: import zz.
literal<["planning", "active", "completed"]>(value: ["planning", "active", "completed"], params?: string | z.z.core.$ZodLiteralParams): z.z.ZodLiteral<"planning" | "active" | "completed"> (+1 overload)
export literal
literal
(["planning", "active", "completed"]),
coordinator: z.ZodOptional<CoMapSchema<{
    name: z.z.ZodString;
}>>
coordinator
: import zz.
optional<CoMapSchema<{
    name: z.z.ZodString;
}>>(innerType: CoMapSchema<{
    name: z.z.ZodString;
}>): z.ZodOptional<CoMapSchema<{
    name: z.z.ZodString;
}>>
export optional
optional
(
const Member: CoMapSchema<{
    name: z.z.ZodString;
}>
Member
),
});

You can create either struct-like CoMaps with fixed fields (as above) or record-like CoMaps for key-value pairs:

const const Inventory: CoRecordSchema<z.z.ZodString, z.z.ZodNumber>Inventory = import coco.
record<z.z.ZodString, z.z.ZodNumber>(_keyType: z.z.ZodString, valueType: z.z.ZodNumber): CoRecordSchema<z.z.ZodString, z.z.ZodNumber>
export record
record
(import zz.
function string(params?: string | z.z.core.$ZodStringParams): z.z.ZodString
export string
string
(), import zz.
function number(params?: string | z.z.core.$ZodNumberParams): z.z.ZodNumber
export number
number
());

To instantiate a CoMap:

const 
const project: {
    name: string;
    startDate: Date;
    status: "planning" | "active" | "completed";
    coordinator: ({
        name: string;
    } & CoMap) | undefined;
} & CoMap
project
=
const Project: CoMapSchema<{
    name: z.z.ZodString;
    startDate: z.z.ZodDate;
    status: z.z.ZodLiteral<"planning" | "active" | "completed">;
    coordinator: z.ZodOptional<CoMapSchema<{
        name: z.z.ZodString;
    }>>;
}>
Project
.
create: (init: {
    coordinator?: ({
        name: string;
    } & CoMap) | undefined;
    name: string;
    startDate: Date;
    status: NonNullable<"planning" | "active" | "completed">;
}, options?: Account | ... 2 more ... | undefined) => {
    ...;
} & CoMap
create
({
name: stringname: "Spring Planting", startDate: DatestartDate: new
var Date: DateConstructor
new (value: number | string | Date) => Date (+4 overloads)
Date
("2025-03-15"),
status: NonNullable<"planning" | "active" | "completed">status: "planning", }); const
const inventory: {
    [x: string]: number;
} & CoMap
inventory
= const Inventory: CoRecordSchema<z.z.ZodString, z.z.ZodNumber>Inventory.
create: (init: {
    [x: string]: number;
}, options?: {
    owner: Account | Group;
    unique?: CoValueUniqueness["uniqueness"];
} | Account | Group) => {
    ...;
} & CoMap
create
({
tomatoes: numbertomatoes: 48, basil: numberbasil: 12, });

Ownership

When creating CoMaps, you can specify ownership to control access:

// Create with default owner (current user)
const 
const privateProject: {
    name: string;
    startDate: Date;
    status: "planning" | "active" | "completed";
    coordinator: ({
        name: string;
    } & CoMap) | undefined;
} & CoMap
privateProject
=
const Project: CoMapSchema<{
    name: z.z.ZodString;
    startDate: z.z.ZodDate;
    status: z.z.ZodLiteral<"planning" | "active" | "completed">;
    coordinator: z.ZodOptional<CoMapSchema<{
        name: z.z.ZodString;
    }>>;
}>
Project
.
create: (init: {
    coordinator?: ({
        name: string;
    } & CoMap) | undefined;
    name: string;
    startDate: Date;
    status: NonNullable<"planning" | "active" | "completed">;
}, options?: Account | ... 2 more ... | undefined) => {
    ...;
} & CoMap
create
({
name: stringname: "My Herb Garden", startDate: DatestartDate: new
var Date: DateConstructor
new (value: number | string | Date) => Date (+4 overloads)
Date
("2025-04-01"),
status: NonNullable<"planning" | "active" | "completed">status: "planning", }); // Create with shared ownership const const gardenGroup: GroupgardenGroup = class Group
@categoryIdentity & Permissions
Group
.
Group.create<Group>(this: CoValueClass<Group>, options?: {
    owner: Account;
} | Account): Group
create
();
const gardenGroup: GroupgardenGroup.Group.addMember(member: Account, role: AccountRole): void (+2 overloads)addMember(
const memberAccount: Account | ({
    [x: string]: any;
} & Account)
memberAccount
, "writer");
const
const communityProject: {
    name: string;
    startDate: Date;
    status: "planning" | "active" | "completed";
    coordinator: ({
        name: string;
    } & CoMap) | undefined;
} & CoMap
communityProject
=
const Project: CoMapSchema<{
    name: z.z.ZodString;
    startDate: z.z.ZodDate;
    status: z.z.ZodLiteral<"planning" | "active" | "completed">;
    coordinator: z.ZodOptional<CoMapSchema<{
        name: z.z.ZodString;
    }>>;
}>
Project
.
create: (init: {
    coordinator?: ({
        name: string;
    } & CoMap) | undefined;
    name: string;
    startDate: Date;
    status: NonNullable<"planning" | "active" | "completed">;
}, options?: Account | ... 2 more ... | undefined) => {
    ...;
} & CoMap
create
(
{ name: stringname: "Community Vegetable Plot", startDate: DatestartDate: new
var Date: DateConstructor
new (value: number | string | Date) => Date (+4 overloads)
Date
("2025-03-20"),
status: NonNullable<"planning" | "active" | "completed">status: "planning", }, { owner: Account | Groupowner: const gardenGroup: GroupgardenGroup }, );

See Groups as permission scopes 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:

var console: Console
The `console` module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers. The module exports two specific components: * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream. * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstdout) and [`process.stderr`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module. _**Warning**_: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v20.x/api/process.html#a-note-on-process-io) for more information. Example using the global `console`: ```js console.log('hello world'); // Prints: hello world, to stdout console.log('hello %s', 'world'); // Prints: hello world, to stdout console.error(new Error('Whoops, something bad happened')); // Prints error message and stack trace to stderr: // Error: Whoops, something bad happened // at [eval]:5:15 // at Script.runInThisContext (node:vm:132:18) // at Object.runInThisContext (node:vm:309:38) // at node:internal/process/execution:77:19 // at [eval]-wrapper:6:22 // at evalScript (node:internal/process/execution:76:60) // at node:internal/main/eval_string:23:3 const name = 'Will Robinson'; console.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ``` Example using the `Console` class: ```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err); myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err const name = 'Will Robinson'; myConsole.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```
@see[source](https://github.com/nodejs/node/blob/v20.11.1/lib/console.js)
console
.Console.log(message?: any, ...optionalParams: any[]): void (+2 overloads)
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)
log
(
const project: {
    name: string;
    startDate: Date;
    status: "planning" | "active" | "completed";
    coordinator: ({
        name: string;
    } & CoMap) | undefined;
} & CoMap
project
.name: stringname); // "Spring Planting"
var console: Console
The `console` module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers. The module exports two specific components: * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream. * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstdout) and [`process.stderr`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module. _**Warning**_: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v20.x/api/process.html#a-note-on-process-io) for more information. Example using the global `console`: ```js console.log('hello world'); // Prints: hello world, to stdout console.log('hello %s', 'world'); // Prints: hello world, to stdout console.error(new Error('Whoops, something bad happened')); // Prints error message and stack trace to stderr: // Error: Whoops, something bad happened // at [eval]:5:15 // at Script.runInThisContext (node:vm:132:18) // at Object.runInThisContext (node:vm:309:38) // at node:internal/process/execution:77:19 // at [eval]-wrapper:6:22 // at evalScript (node:internal/process/execution:76:60) // at node:internal/main/eval_string:23:3 const name = 'Will Robinson'; console.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ``` Example using the `Console` class: ```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err); myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err const name = 'Will Robinson'; myConsole.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```
@see[source](https://github.com/nodejs/node/blob/v20.11.1/lib/console.js)
console
.Console.log(message?: any, ...optionalParams: any[]): void (+2 overloads)
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)
log
(
const project: {
    name: string;
    startDate: Date;
    status: "planning" | "active" | "completed";
    coordinator: ({
        name: string;
    } & CoMap) | undefined;
} & CoMap
project
.status: "planning" | "active" | "completed"status); // "planning"

Handling Optional Fields

Optional fields require checks before access:

if (
const project: {
    name: string;
    startDate: Date;
    status: "planning" | "active" | "completed";
    coordinator: ({
        name: string;
    } & CoMap) | undefined;
} & CoMap
project
.
coordinator: ({
    name: string;
} & CoMap) | undefined
coordinator
) {
var console: Console
The `console` module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers. The module exports two specific components: * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream. * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstdout) and [`process.stderr`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module. _**Warning**_: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v20.x/api/process.html#a-note-on-process-io) for more information. Example using the global `console`: ```js console.log('hello world'); // Prints: hello world, to stdout console.log('hello %s', 'world'); // Prints: hello world, to stdout console.error(new Error('Whoops, something bad happened')); // Prints error message and stack trace to stderr: // Error: Whoops, something bad happened // at [eval]:5:15 // at Script.runInThisContext (node:vm:132:18) // at Object.runInThisContext (node:vm:309:38) // at node:internal/process/execution:77:19 // at [eval]-wrapper:6:22 // at evalScript (node:internal/process/execution:76:60) // at node:internal/main/eval_string:23:3 const name = 'Will Robinson'; console.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ``` Example using the `Console` class: ```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err); myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err const name = 'Will Robinson'; myConsole.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```
@see[source](https://github.com/nodejs/node/blob/v20.11.1/lib/console.js)
console
.Console.log(message?: any, ...optionalParams: any[]): void (+2 overloads)
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)
log
(
const project: {
    name: string;
    startDate: Date;
    status: "planning" | "active" | "completed";
    coordinator: ({
        name: string;
    } & CoMap) | undefined;
} & CoMap
project
.
coordinator: {
    name: string;
} & CoMap
coordinator
.name: stringname); // Safe access
}

Working with Record CoMaps

For record-type CoMaps, you can access values using bracket notation:

const 
const inventory: {
    [x: string]: number;
} & CoMap
inventory
= const Inventory: CoRecordSchema<z.z.ZodString, z.z.ZodNumber>Inventory.
create: (init: {
    [x: string]: number;
}, options?: {
    owner: Account | Group;
    unique?: CoValueUniqueness["uniqueness"];
} | Account | Group) => {
    ...;
} & CoMap
create
({
tomatoes: numbertomatoes: 48, peppers: numberpeppers: 24, basil: numberbasil: 12 }); var console: Console
The `console` module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers. The module exports two specific components: * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream. * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstdout) and [`process.stderr`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module. _**Warning**_: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v20.x/api/process.html#a-note-on-process-io) for more information. Example using the global `console`: ```js console.log('hello world'); // Prints: hello world, to stdout console.log('hello %s', 'world'); // Prints: hello world, to stdout console.error(new Error('Whoops, something bad happened')); // Prints error message and stack trace to stderr: // Error: Whoops, something bad happened // at [eval]:5:15 // at Script.runInThisContext (node:vm:132:18) // at Object.runInThisContext (node:vm:309:38) // at node:internal/process/execution:77:19 // at [eval]-wrapper:6:22 // at evalScript (node:internal/process/execution:76:60) // at node:internal/main/eval_string:23:3 const name = 'Will Robinson'; console.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ``` Example using the `Console` class: ```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err); myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err const name = 'Will Robinson'; myConsole.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```
@see[source](https://github.com/nodejs/node/blob/v20.11.1/lib/console.js)
console
.Console.log(message?: any, ...optionalParams: any[]): void (+2 overloads)
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)
log
(
const inventory: {
    [x: string]: number;
} & CoMap
inventory
["tomatoes"]); // 48

Updating CoMaps

Updating CoMap properties uses standard JavaScript assignment:

const project: {
    name: string;
    startDate: Date;
    status: "planning" | "active" | "completed";
    coordinator: ({
        name: string;
    } & CoMap) | undefined;
} & CoMap
project
.name: stringname = "Spring Vegetable Garden"; // Update name
const project: {
    name: string;
    startDate: Date;
    status: "planning" | "active" | "completed";
    coordinator: ({
        name: string;
    } & CoMap) | undefined;
} & CoMap
project
.startDate: DatestartDate = new
var Date: DateConstructor
new (value: number | string | Date) => Date (+4 overloads)
Date
("2025-03-20"); // Update date

Type Safety

CoMaps are fully typed in TypeScript, giving you autocomplete and error checking:

const project: {
    name: string;
    startDate: Date;
    status: "planning" | "active" | "completed";
    coordinator: ({
        name: string;
    } & CoMap) | undefined;
} & CoMap
project
.name: stringname = "Spring Vegetable Planting"; // ✓ Valid string
project.startDate = "2025-03-15"; // ✗ Type error: expected Date
Type 'string' is not assignable to type 'Date'.

Soft Deletion

Implementing a soft deletion pattern by using a deleted flag allows you to maintain data for potential recovery and auditing.

const 
const Project: CoMapSchema<{
    name: z.z.ZodString;
    deleted: z.ZodOptional<z.z.ZodBoolean>;
}>
Project
= import coco.
map<{
    name: z.z.ZodString;
    deleted: z.ZodOptional<z.z.ZodBoolean>;
}>(shape: {
    name: z.z.ZodString;
    deleted: z.ZodOptional<z.z.ZodBoolean>;
}): CoMapSchema<{
    name: z.z.ZodString;
    deleted: z.ZodOptional<z.z.ZodBoolean>;
}>
export map
map
({
name: z.z.ZodStringname: import zz.
function string(params?: string | z.z.core.$ZodStringParams): z.z.ZodString
export string
string
(),
deleted: z.ZodOptional<z.z.ZodBoolean>deleted: import zz.
optional<z.z.ZodBoolean>(innerType: z.z.ZodBoolean): z.ZodOptional<z.z.ZodBoolean>
export optional
optional
(import zz.
function boolean(params?: string | z.z.core.$ZodBooleanParams): z.z.ZodBoolean
export boolean
boolean
()),
});

When an object needs to be "deleted", instead of removing it from the system, the deleted flag is set to true. This gives us a property to omit it in the future.

Deleting Properties

You can delete properties from CoMaps:

delete 
const inventory: {
    [x: string]: number;
} & CoMap
inventory
["basil"]; // Remove a key-value pair
// For optional fields in struct-like CoMaps
const project: {
    name: string;
    startDate: Date;
    status: "planning" | "active" | "completed";
    coordinator: ({
        name: string;
    } & CoMap) | undefined;
} & CoMap
project
.
coordinator: ({
    name: string;
} & CoMap) | undefined
coordinator
= var undefinedundefined; // 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, 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:

const 
const Task: CoMapSchema<{
    done: z.z.ZodBoolean;
    text: PlainTextSchema;
    version: z.z.ZodLiteral<1 | 2>;
    priority: z.z.ZodEnum<{
        low: "low";
        medium: "medium";
        high: "high";
    }>;
}, z.z.core.$ZodObjectConfig, Account | Group>
Task
= import coco
.
map<{
    done: z.z.ZodBoolean;
    text: PlainTextSchema;
    version: z.z.ZodLiteral<1 | 2>;
    priority: z.z.ZodEnum<{
        low: "low";
        medium: "medium";
        high: "high";
    }>;
}>(shape: {
    done: z.z.ZodBoolean;
    text: PlainTextSchema;
    version: z.z.ZodLiteral<1 | 2>;
    priority: z.z.ZodEnum<{
        low: "low";
        medium: "medium";
        high: "high";
    }>;
}): CoMapSchema<...>
export map
map
({
done: z.z.ZodBooleandone: import zz.
function boolean(params?: string | z.z.core.$ZodBooleanParams): z.z.ZodBoolean
export boolean
boolean
(),
text: PlainTextSchematext: import coco.
function plainText(): PlainTextSchema
export plainText
plainText
(),
version: z.z.ZodLiteral<1 | 2>version: import zz.
literal<[1, 2]>(value: [1, 2], params?: string | z.z.core.$ZodLiteralParams): z.z.ZodLiteral<1 | 2> (+1 overload)
export literal
literal
([1, 2]),
priority: z.z.ZodEnum<{
    low: "low";
    medium: "medium";
    high: "high";
}>
priority
: import zz.
enum<readonly ["low", "medium", "high"]>(values: readonly ["low", "medium", "high"], params?: string | z.z.core.$ZodEnumParams): z.z.ZodEnum<{
    low: "low";
    medium: "medium";
    high: "high";
}> (+1 overload)
export enum
enum
(["low", "medium", "high"]), // new field
}) .
function withMigration(migration: (value: {
    done: boolean;
    text: CoPlainText | null;
    version: 1 | 2;
    priority: "low" | "medium" | "high";
} & CoMap) => undefined): CoMapSchema<{
    done: z.z.ZodBoolean;
    text: PlainTextSchema;
    version: z.z.ZodLiteral<...>;
    priority: z.z.ZodEnum<...>;
}, z.z.core.$ZodObjectConfig, Account | Group>
withMigration
((
task: {
    done: boolean;
    text: CoPlainText | null;
    version: 1 | 2;
    priority: "low" | "medium" | "high";
} & CoMap
task
) => {
if (
task: {
    done: boolean;
    text: CoPlainText | null;
    version: 1 | 2;
    priority: "low" | "medium" | "high";
} & CoMap
task
.version: 1 | 2version === 1) {
task: {
    done: boolean;
    text: CoPlainText | null;
    version: 1 | 2;
    priority: "low" | "medium" | "high";
} & CoMap
task
.priority: "low" | "medium" | "high"priority = "medium";
// Upgrade the version so the migration won't run again
task: {
    done: boolean;
    text: CoPlainText | null;
    version: 1 | 2;
    priority: "low" | "medium" | "high";
} & CoMap
task
.version: 1 | 2version = 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:

const 
const TaskV1: CoMapSchema<{
    version: z.z.ZodLiteral<1>;
    done: z.z.ZodBoolean;
    text: z.z.ZodString;
}>
TaskV1
= import coco.
map<{
    version: z.z.ZodLiteral<1>;
    done: z.z.ZodBoolean;
    text: z.z.ZodString;
}>(shape: {
    version: z.z.ZodLiteral<1>;
    done: z.z.ZodBoolean;
    text: z.z.ZodString;
}): CoMapSchema<{
    version: z.z.ZodLiteral<1>;
    done: z.z.ZodBoolean;
    text: z.z.ZodString;
}>
export map
map
({
version: z.z.ZodLiteral<1>version: import zz.
literal<1>(value: 1, params?: string | z.z.core.$ZodLiteralParams): z.z.ZodLiteral<1> (+1 overload)
export literal
literal
(1),
done: z.z.ZodBooleandone: import zz.
function boolean(params?: string | z.z.core.$ZodBooleanParams): z.z.ZodBoolean
export boolean
boolean
(),
text: z.z.ZodStringtext: import zz.
function string(params?: string | z.z.core.$ZodStringParams): z.z.ZodString
export string
string
(),
}); const
const TaskV2: CoMapSchema<{
    version: z.z.ZodLiteral<2>;
    done: z.z.ZodBoolean;
    text: z.z.ZodString;
    priority: z.z.ZodEnum<{
        low: "low";
        medium: "medium";
        high: "high";
    }>;
}, z.z.core.$ZodObjectConfig, Account | Group>
TaskV2
= import coco.
map<{
    version: z.z.ZodLiteral<2>;
    done: z.z.ZodBoolean;
    text: z.z.ZodString;
    priority: z.z.ZodEnum<{
        low: "low";
        medium: "medium";
        high: "high";
    }>;
}>(shape: {
    version: z.z.ZodLiteral<2>;
    done: z.z.ZodBoolean;
    text: z.z.ZodString;
    priority: z.z.ZodEnum<{
        low: "low";
        medium: "medium";
        high: "high";
    }>;
}): CoMapSchema<...>
export map
map
({
// We need to be more strict about the version to make the // discriminated union work version: z.z.ZodLiteral<2>version: import zz.
literal<2>(value: 2, params?: string | z.z.core.$ZodLiteralParams): z.z.ZodLiteral<2> (+1 overload)
export literal
literal
(2),
done: z.z.ZodBooleandone: import zz.
function boolean(params?: string | z.z.core.$ZodBooleanParams): z.z.ZodBoolean
export boolean
boolean
(),
text: z.z.ZodStringtext: import zz.
function string(params?: string | z.z.core.$ZodStringParams): z.z.ZodString
export string
string
(),
priority: z.z.ZodEnum<{
    low: "low";
    medium: "medium";
    high: "high";
}>
priority
: import zz.
enum<readonly ["low", "medium", "high"]>(values: readonly ["low", "medium", "high"], params?: string | z.z.core.$ZodEnumParams): z.z.ZodEnum<{
    low: "low";
    medium: "medium";
    high: "high";
}> (+1 overload)
export enum
enum
(["low", "medium", "high"]),
}).
function withMigration(migration: (value: {
    version: 2;
    done: boolean;
    text: string;
    priority: "low" | "medium" | "high";
} & CoMap) => undefined): CoMapSchema<{
    version: z.z.ZodLiteral<2>;
    done: z.z.ZodBoolean;
    text: z.z.ZodString;
    priority: z.z.ZodEnum<...>;
}, z.z.core.$ZodObjectConfig, Account | Group>
withMigration
((
task: {
    version: 2;
    done: boolean;
    text: string;
    priority: "low" | "medium" | "high";
} & CoMap
task
) => {
// @ts-expect-error - check if we need to run the migration if (
task: {
    version: 2;
    done: boolean;
    text: string;
    priority: "low" | "medium" | "high";
} & CoMap
task
.version: 2version === 1) {
task: {
    version: 2;
    done: boolean;
    text: string;
    priority: "low" | "medium" | "high";
} & CoMap
task
.version: 2version = 2;
task: {
    version: 2;
    done: boolean;
    text: string;
    priority: "low" | "medium" | "high";
} & CoMap
task
.priority: "low" | "medium" | "high"priority = "medium";
} }); // Export the discriminated union; because some users might // not be able to run the migration export const
const Task: z.z.ZodDiscriminatedUnion<[CoMapSchema<{
    version: z.z.ZodLiteral<1>;
    done: z.z.ZodBoolean;
    text: z.z.ZodString;
}>, CoMapSchema<{
    version: z.z.ZodLiteral<2>;
    done: z.z.ZodBoolean;
    text: z.z.ZodString;
    priority: z.z.ZodEnum<{
        low: "low";
        medium: "medium";
        high: "high";
    }>;
}, z.z.core.$ZodObjectConfig, Account | Group>]>
Task
= import zz.
discriminatedUnion<[CoMapSchema<{
    version: z.z.ZodLiteral<1>;
    done: z.z.ZodBoolean;
    text: z.z.ZodString;
}>, CoMapSchema<{
    version: z.z.ZodLiteral<2>;
    done: z.z.ZodBoolean;
    text: z.z.ZodString;
    priority: z.z.ZodEnum<...>;
}, z.z.core.$ZodObjectConfig, Account | Group>]>(discriminator: string, options: [...], params?: string | z.z.core.$ZodDiscriminatedUnionParams): z.z.ZodDiscriminatedUnion<...>
export discriminatedUnion
discriminatedUnion
("version", [
const TaskV1: CoMapSchema<{
    version: z.z.ZodLiteral<1>;
    done: z.z.ZodBoolean;
    text: z.z.ZodString;
}>
TaskV1
,
const TaskV2: CoMapSchema<{
    version: z.z.ZodLiteral<2>;
    done: z.z.ZodBoolean;
    text: z.z.ZodString;
    priority: z.z.ZodEnum<{
        low: "low";
        medium: "medium";
        high: "high";
    }>;
}, z.z.core.$ZodObjectConfig, Account | Group>
TaskV2
,
]);

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:

import { import coco, import zz } from "jazz-tools";

const 
const Project: CoMapSchema<{
    name: z.z.ZodString;
    startDate: z.z.ZodDate;
    endDate: z.ZodOptional<z.z.ZodDate>;
}>
Project
= import coco.
map<{
    name: z.z.ZodString;
    startDate: z.z.ZodDate;
    endDate: z.ZodOptional<z.z.ZodDate>;
}>(shape: {
    name: z.z.ZodString;
    startDate: z.z.ZodDate;
    endDate: z.ZodOptional<z.z.ZodDate>;
}): CoMapSchema<...>
export map
map
({
name: z.z.ZodStringname: import zz.
function string(params?: string | z.z.core.$ZodStringParams): z.z.ZodString
export string
string
(),
startDate: z.z.ZodDatestartDate: import zz.
function date(params?: string | z.z.core.$ZodDateParams): z.z.ZodDate
export date
date
(),
endDate: z.ZodOptional<z.z.ZodDate>endDate: import zz.
optional<z.z.ZodDate>(innerType: z.z.ZodDate): z.ZodOptional<z.z.ZodDate>
export optional
optional
(import zz.
function date(params?: string | z.z.core.$ZodDateParams): z.z.ZodDate
export date
date
()),
}); type
type Project = {
    name: string;
    startDate: Date;
    endDate: Date | undefined;
} & CoMap
Project
= import coco.
type loaded<T extends CoValueClass | AnyCoSchema, R extends ResolveQuery<T> = true> = R extends boolean | undefined ? NonNullable<InstanceOfSchemaCoValuesNullable<T>> : [NonNullable<InstanceOfSchemaCoValuesNullable<T>>] extends [...] ? Exclude<...> extends CoValue ? R extends {
    ...;
} ? ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...
export loaded
loaded
<typeof
const Project: CoMapSchema<{
    name: z.z.ZodString;
    startDate: z.z.ZodDate;
    endDate: z.ZodOptional<z.z.ZodDate>;
}>
Project
>;
export function function isProjectActive(project: Project): booleanisProjectActive(
project: {
    name: string;
    startDate: Date;
    endDate: Date | undefined;
} & CoMap
project
:
type Project = {
    name: string;
    startDate: Date;
    endDate: Date | undefined;
} & CoMap
Project
) {
const const now: Datenow = new
var Date: DateConstructor
new () => Date (+4 overloads)
Date
();
return const now: Datenow >=
project: {
    name: string;
    startDate: Date;
    endDate: Date | undefined;
} & CoMap
project
.startDate: DatestartDate && (!
project: {
    name: string;
    startDate: Date;
    endDate: Date | undefined;
} & CoMap
project
.endDate: Date | undefinedendDate || const now: Datenow <=
project: {
    name: string;
    startDate: Date;
    endDate: Date | undefined;
} & CoMap
project
.endDate: DateendDate);
} export function function formatProjectDuration(project: Project, format: "short" | "full"): stringformatProjectDuration(
project: {
    name: string;
    startDate: Date;
    endDate: Date | undefined;
} & CoMap
project
:
type Project = {
    name: string;
    startDate: Date;
    endDate: Date | undefined;
} & CoMap
Project
, format: "short" | "full"format: "short" | "full") {
const const start: stringstart =
project: {
    name: string;
    startDate: Date;
    endDate: Date | undefined;
} & CoMap
project
.startDate: DatestartDate.Date.toLocaleDateString(locales?: Intl.LocalesArgument, options?: Intl.DateTimeFormatOptions): string (+2 overloads)
Converts a date to a string by using the current or specified locale.
@paramlocales A locale string, array of locale strings, Intl.Locale object, or array of Intl.Locale objects that contain one or more language or locale tags. If you include more than one locale string, list them in descending order of priority so that the first entry is the preferred locale. If you omit this parameter, the default locale of the JavaScript runtime is used.@paramoptions An object that contains one or more properties that specify comparison options.
toLocaleDateString
();
if (!
project: {
    name: string;
    startDate: Date;
    endDate: Date | undefined;
} & CoMap
project
.endDate: Date | undefinedendDate) {
return format: "short" | "full"format === "full" ? `Started on ${const start: stringstart}, ongoing` : `From ${const start: stringstart}`; } const const end: stringend =
project: {
    name: string;
    startDate: Date;
    endDate: Date | undefined;
} & CoMap
project
.endDate: DateendDate.Date.toLocaleDateString(locales?: Intl.LocalesArgument, options?: Intl.DateTimeFormatOptions): string (+2 overloads)
Converts a date to a string by using the current or specified locale.
@paramlocales A locale string, array of locale strings, Intl.Locale object, or array of Intl.Locale objects that contain one or more language or locale tags. If you include more than one locale string, list them in descending order of priority so that the first entry is the preferred locale. If you omit this parameter, the default locale of the JavaScript runtime is used.@paramoptions An object that contains one or more properties that specify comparison options.
toLocaleDateString
();
return format: "short" | "full"format === "full" ? `From ${const start: stringstart} to ${const end: stringend}` : `${(
project: {
    name: string;
    startDate: Date;
    endDate: Date | undefined;
} & CoMap
project
.endDate: DateendDate.Date.getTime(): number
Returns the stored time value in milliseconds since midnight, January 1, 1970 UTC.
getTime
() -
project: {
    name: string;
    startDate: Date;
    endDate: Date | undefined;
} & CoMap
project
.startDate: DatestartDate.Date.getTime(): number
Returns the stored time value in milliseconds since midnight, January 1, 1970 UTC.
getTime
()) / 86400000} days`;
} const
const project: {
    name: string;
    startDate: Date;
    endDate: Date | undefined;
} & CoMap
project
=
const Project: CoMapSchema<{
    name: z.z.ZodString;
    startDate: z.z.ZodDate;
    endDate: z.ZodOptional<z.z.ZodDate>;
}>
Project
.
create: (init: {
    endDate?: Date | undefined;
    name: string;
    startDate: Date;
}, options?: Account | Group | {
    owner: Account | Group;
    unique?: CoValueUniqueness["uniqueness"];
} | undefined) => {
    ...;
} & CoMap
create
({
name: stringname: "My project", startDate: DatestartDate: new
var Date: DateConstructor
new (value: number | string | Date) => Date (+4 overloads)
Date
("2025-04-01"),
endDate?: Date | undefinedendDate: new
var Date: DateConstructor
new (value: number | string | Date) => Date (+4 overloads)
Date
("2025-04-04"),
}); var console: Console
The `console` module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers. The module exports two specific components: * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream. * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstdout) and [`process.stderr`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module. _**Warning**_: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v20.x/api/process.html#a-note-on-process-io) for more information. Example using the global `console`: ```js console.log('hello world'); // Prints: hello world, to stdout console.log('hello %s', 'world'); // Prints: hello world, to stdout console.error(new Error('Whoops, something bad happened')); // Prints error message and stack trace to stderr: // Error: Whoops, something bad happened // at [eval]:5:15 // at Script.runInThisContext (node:vm:132:18) // at Object.runInThisContext (node:vm:309:38) // at node:internal/process/execution:77:19 // at [eval]-wrapper:6:22 // at evalScript (node:internal/process/execution:76:60) // at node:internal/main/eval_string:23:3 const name = 'Will Robinson'; console.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ``` Example using the `Console` class: ```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err); myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err const name = 'Will Robinson'; myConsole.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```
@see[source](https://github.com/nodejs/node/blob/v20.11.1/lib/console.js)
console
.Console.log(message?: any, ...optionalParams: any[]): void (+2 overloads)
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)
log
(function isProjectActive(project: Project): booleanisProjectActive(
const project: {
    name: string;
    startDate: Date;
    endDate: Date | undefined;
} & CoMap
project
)); // false
var console: Console
The `console` module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers. The module exports two specific components: * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream. * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstdout) and [`process.stderr`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module. _**Warning**_: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v20.x/api/process.html#a-note-on-process-io) for more information. Example using the global `console`: ```js console.log('hello world'); // Prints: hello world, to stdout console.log('hello %s', 'world'); // Prints: hello world, to stdout console.error(new Error('Whoops, something bad happened')); // Prints error message and stack trace to stderr: // Error: Whoops, something bad happened // at [eval]:5:15 // at Script.runInThisContext (node:vm:132:18) // at Object.runInThisContext (node:vm:309:38) // at node:internal/process/execution:77:19 // at [eval]-wrapper:6:22 // at evalScript (node:internal/process/execution:76:60) // at node:internal/main/eval_string:23:3 const name = 'Will Robinson'; console.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ``` Example using the `Console` class: ```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err); myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err const name = 'Will Robinson'; myConsole.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```
@see[source](https://github.com/nodejs/node/blob/v20.11.1/lib/console.js)
console
.Console.log(message?: any, ...optionalParams: any[]): void (+2 overloads)
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)
log
(function formatProjectDuration(project: Project, format: "short" | "full"): stringformatProjectDuration(
const project: {
    name: string;
    startDate: Date;
    endDate: Date | undefined;
} & CoMap
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:

// This will not work as `learning-jazz` is not a CoValue ID
const 
const myTask: ({
    text: string;
} & CoMap) | null
myTask
= await
const Task: CoMapSchema<{
    text: z.z.ZodString;
}>
Task
.
load<true>(id: string, options?: {
    resolve?: RefsToResolve<{
        text: string;
    } & CoMap, 10, []> | undefined;
    loadAs?: Account | AnonymousJazzAgent;
    skipRetry?: boolean;
} | undefined): Promise<...>
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.

// Given the project owner, myTask will have always the same id
const 
const learnJazzTask: {
    text: string;
} & CoMap
learnJazzTask
= await
const Task: CoMapSchema<{
    text: z.z.ZodString;
}>
Task
.
create: (init: {
    text: string;
}, options?: Account | Group | {
    owner: Account | Group;
    unique?: CoValueUniqueness["uniqueness"];
} | undefined) => {
    ...;
} & CoMap
create
({
text: stringtext: "Let's learn some Jazz!", }, { unique?: JsonValue | undefinedunique: "learning-jazz", owner: Account | Groupowner:
const project: {
    name: string;
    startDate: Date;
    endDate: Date | undefined;
} & CoMap
project
.CoValueBase._owner: Account | Group
@categoryCollaboration
_owner
, // Different owner, different id
});

Now you can use CoMap.loadUnique to easily load the CoMap using the human-readable identifier:

const 
const learnJazzTask: ({
    text: string;
} & CoMap) | null
learnJazzTask
= await
const Task: CoMapSchema<{
    text: z.z.ZodString;
}>
Task
.
loadUnique<true>(unique: CoValueUniqueness["uniqueness"], ownerID: string, options?: {
    resolve?: RefsToResolve<{
        text: string;
    } & CoMap, 10, []> | undefined;
    loadAs?: Account | AnonymousJazzAgent;
} | undefined): Promise<...>
loadUnique
(
"learning-jazz",
const project: {
    name: string;
    startDate: Date;
    endDate: Date | undefined;
} & CoMap
project
.CoValueBase._owner: Account | Group
@categoryCollaboration
_owner
.id: string
@categoryContent@categoryContent
id
);

It's also possible to combine the create+load operation using CoMap.upsertUnique:

const 
const learnJazzTask: ({
    text: string;
} & CoMap) | null
learnJazzTask
= await
const Task: CoMapSchema<{
    text: z.z.ZodString;
}>
Task
.
upsertUnique: <true>(options: {
    value: {
        text: string;
    };
    unique: CoValueUniqueness["uniqueness"];
    owner: Account | Group;
    resolve?: RefsToResolve<{
        text: string;
    } & CoMap, 10, []> | undefined;
}) => Promise<...>
upsertUnique
(
{
value: {
    text: string;
}
value
: {
text: stringtext: "Let's learn some Jazz!", }, unique: JsonValueunique: "learning-jazz", owner: Account | Groupowner:
const project: {
    name: string;
    startDate: Date;
    endDate: Date | undefined;
} & CoMap
project
.CoValueBase._owner: Account | Group
@categoryCollaboration
_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)