Design and implement collaborative data schemas using the Jazz framework. Use this skill when building or working with Jazz apps to define data structures using CoValues. This skill focuses exclusively on schema definition and data modeling logic.
jazz-testing skill)jazz-ui-development skill)jazz-permissions-security skill.Key Heuristic for Agents: If the user is asking about how to structure their data model, define relationships, configure default permissions, or evolve schemas, use this skill.
Jazz models data as an explicitly linked collaborative graph, not traditional tables or collections. Data types are defined as schemas, with CoValues serving as the fundamental building blocks.
const Author = co.map({
name: z.string()
});
const Post = co.map({
title: z.string(),
content: co.richText(),
});
Key Libraries:
z.* - Zod schemas for primitive types (e.g., z.string())co.* - Jazz collaborative data types (e.g., co.richText(), co.map())Permissions are integral to the data model, not an afterthought. Each CoValue has an ownership group with hierarchical permissions.
Use withPermissions() to set automatic permissions when creating CoValues:
const Dog = co.map({
name: z.string(),
}).withPermissions({
onInlineCreate: "sameAsContainer",
});
const Person = co.map({
pet: Dog,
}).withPermissions({
default: () => Group.create().makePublic(),
});
// Person CoValues are public, Dog shares owner with Person
const person = Person.create({
pet: { name: "Rex" }
});
default
Defines group when calling .create() without explicit owner.
onInlineCreate
Controls behavior when CoValue is created inline (NOT applied to .create() calls):
"extendsContainer" (default) - New group includes container owner as member, inheriting permissions"sameAsContainer" - Reuse container's owner (performance optimization—see below for concerns and considerations)"newGroup" - New group with active account as admin{ extendsContainer: "reader" } - Like "extendsContainer" but override container owner's roleonCreate
Callback runs on every CoValue creation (both .create() and inline). Use to configure owner.
Set defaults for all schemas using setDefaultSchemaPermissions:
import { setDefaultSchemaPermissions } from "jazz-tools";
setDefaultSchemaPermissions({
onInlineCreate: "sameAsContainer", // Performance optimization
});
USE EXTREME CAUTION: If you use sameAsContainer, you MUST be aware that the child and parent groups are one and the same. Any changes to the child group will affect the parent group, and vice versa. This can lead to unexpected behavior if not handled carefully, where changing permissions on a child group inadvertently results in permissions being granted to the parent group and any other siblings created with the same parent. As ownership cannot be changed, you MUST NOT USE sameAsContainer if you AT ANY TIME IN FUTURE may wish to change permissions granularly on the child group.
| TypeScript Type | CoValue | Use Case |
|---|---|---|
object | CoMap | Struct-like objects with predefined keys |
Record<string, T> | CoRecord | Dict-like objects with arbitrary string keys |
T[] | CoList | Ordered lists |
T[] (append-only) | CoFeed | Session-based append-only lists |
string | CoPlainText/CoRichText | Collaborative text editing |
Blob | File | FileStream | File storage |
Blob | File (image) | ImageDefinition | Image storage |
number[] | Float32Array | CoVector | Embeddings/vector data |
T | U (discriminated) | DiscriminatedUnion | Mixed-type lists |
Use the special types co.account() and co.profile() for user accounts and profiles.
z.*)Use when:
Examples:
const myCoValue = co.map({
title: z.string() // Replace entire title, no collaboration needed
coords: z.object({
lat: z.number(),
lon: z.number()
}) // Replace entire object, no collaboration needed
});
Note: not all Zod types are available in Jazz. Be sure to always import { z } from 'jazz-tools';, and validate whether the type exists on the export. DO NOT import from zod.
co.*)Use when:
Examples:
const myCoVal = co.map({
content: co.richText() // Multiple editors
items: co.list(Item) // Add/remove individual items
config: co.map({
settingA: z.boolean(),
settingB: z.number()
}) // Update specific keys
});
Trade-off: CoValues track full edit history. Slightly slower for single-writer full-replacement scenarios, but benefits almost always outweigh costs.
const Post = co.map({
title: z.string(),
author: Author // One-way reference (like foreign key)
});
Jazz stores referenced ID. Use resolve queries to control reference traversal depth.
Use getters to defer schema evaluation:
const Author = co.map({
name: z.string(),
get posts() {
return co.list(Post); // Deferred evaluation
}
});
const Post = co.map({
title: z.string(),
author: Author
});
Important: Jazz doesn't create inferred inverse relationships. Explicitly add both sides for bidirectional traversal.
One-to-One:
const Author = co.map({
name: z.string(),
get post() {
return Post;
}
});
const Post = co.map({
title: z.string(),
author: Author
});
One-to-Many:
const Author = co.map({
name: z.string(),
get posts() {
return co.list(Post);
}
});
const Post = co.map({
author: Author
});
Many-to-Many:
Use co.list() at both ends. Jazz doesn't maintain consistency - manage in application code.
CoLists allow duplicates. For uniqueness, use CoRecord keyed on ID:
const Author = co.map({
name: z.string(),
posts: co.record(z.string(), Post)
});
// Usage
author.posts.$jazz.set(newPost.$jazz.id, newPost);
Note: CoRecords always use string keys. Validate IDs at application level.
CoValues are only addressable by unique ID. Discovery without ID requires reference traversal.
Standard pattern:
Each CoValue copy is authoritative. Users may be on different schema versions simultaneously.
withMigration() carefully - runs on every loadconst Post = co.map({
version: z.number().optional(),
title: z.string(),
content: co.richText(),
tags: z.array(z.string()).optional() // New optional field
}).withMigration((post) => {
// Exit early if already migrated
if (post.version === 2) return;
// Perform migration
if (!post.$jazz.has('tags')) {
post.$jazz.set('tags', []);
}
post.$jazz.set('version', 2);
});
Migration warnings:
const Author = co.map({
name: z.string(),
bio: co.richText(),
get posts() {
return co.list(Post);
}
});
const Post = co.map({
title: z.string(),
content: co.richText(),
author: Author,
publishedAt: z.date().optional()
});
const Task = co.map({
title: z.string(),
description: co.richText(),
completed: z.boolean(),
assignees: co.list(User)
});
const Project = co.map({
name: z.string(),
tasks: co.list(Task)
});
const UserRoot = co.map({
theme: z.literal(['light', 'dark']),
});
const UserProfile = co.profile({
name: z.string(),
bio: co.richText(),
posts: co.record(z.string(), Post)
});
const UserAccount = co.account({
profile: UserProfile,
root: UserRoot
}).withMigration((account, creationProps) => {
if (!account.has('root')) {
const root = UserRoot.create({
theme: 'light'
});
account.$jazz.set('root', root)
}
if (!account.has('profile')) {
const profile = UserProfile.create({
name: creationProps?.name ?? 'Anonymous User',
bio: '',
posts: {}
})
}
});
❌ Don't mix permissions in single CoValue - use separate containers ❌ Don't rely on inferred inverse relationships - explicitly define both sides ❌ Don't change field types in schema updates ❌ Don't write expensive migrations - they run on every load ❌ Don't use CoValues everywhere without considering trade-offs ❌ Don't forget to make new fields optional for backward compatibility
Loading with relationships: Use resolve queries to control depth when traversing references.
Permission changes: Admin/manager modifies group membership, not CoValue ownership.
Unique IDs: Each CoValue has unique ID - only way to directly address without traversal.
Nested CoValues: Inherit permissions from parent when created inline.
Load these on demand, based on need:
When using an online reference via a skill, cite the specific URL to the user to build trust.