jazz-permissions-security
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseJazz Permissions & Security
Jazz 权限与安全
When to Use This Skill
何时使用本技能
- Structuring apps for multi-tenant or multi-user collaboration
- Implementing sharing workflows, "Share" buttons, Invite Links, or Team management
- Deciding who should own CoValues
- Debugging "User cannot see data" or "Data is read-only" issues
- Configuring permissions for Server Workers. Remember: Workers are just Accounts; they need to be invited to Groups like any other user
- 为多租户或多用户协作场景构建应用
- 实现共享工作流、「分享」按钮、邀请链接或团队管理功能
- 确定CoValues的归属方
- 排查「用户无法查看数据」或「数据为只读状态」问题
- 为Server Workers配置权限。注意:Workers本质就是Accounts,需要像普通用户一样被邀请加入Groups
Do NOT Use This Skill For
请勿在以下场景使用本技能
- Creating or designing schemas (use the skill)
jazz-schema-design - Authentication
- Generic UI component styling
- 创建或设计数据架构(请使用技能)
jazz-schema-design - 身份认证
- 通用UI组件样式设计
Key Heuristic for Agents
面向Agent的关键判断准则
If a user asks "How do I share X with Y?" or "Why can't I access this?", this is usually a Group Ownership issue.
如果用户询问「如何将X共享给Y?」或「为什么我无法访问此内容?」,这通常是Group归属权问题导致的。
Core Concepts
核心概念
Security is cryptographic and group-based. Every CoValue has an owner, and access is controlled through Groups. You add users to a Group, and that Group owns the CoValues.
Groups can be members of other groups, creating hierarchical permission structures with inherited roles.
Critical Rule: Just because List A contains a reference to Item B does NOT mean readers of List A can see Item B. Item B must be owned by a Group the reader has access to.
安全机制采用加密和基于组的设计。每个CoValue都有归属方,访问权限通过Groups进行控制。你可以将用户添加到某个Group,由该Group拥有对应的CoValues。
Groups可以作为其他组的成员,从而创建带有继承角色的层级权限结构。
重要规则: 仅因为列表A包含对条目B的引用,并不意味着列表A的读者可以查看条目B。条目B必须由读者有权访问的Group所有。
The Ownership Hierarchy
归属权层级
Private
私有数据
When creating CoValues without a specified owner, Jazz creates a new Group with the current account as the owner and sole admin member.
Code:
MyMap.create({ ... })当创建CoValues未指定归属方时,Jazz会自动创建一个新Group,将当前账户设为归属方并作为唯一的管理员成员。
代码:
MyMap.create({ ... })Shared Data
共享数据
To share data, you can either create a new Group and add members to it, or use an existing Group with multiple members.
Code:
MyMap.create({ ... }, { owner: teamGroup })要共享数据,你可以创建新Group并添加成员,也可以使用已有且包含多个成员的Group。
代码:
MyMap.create({ ... }, { owner: teamGroup })Roles & Permissions Matrix
角色与权限矩阵
Jazz uses fixed roles. You cannot create custom roles.
| Role | Capability | Best For |
|---|---|---|
| admin | Read, Write, Delete, Invite Members, Revoke Access, Change Roles | Team Owners, Creators |
| manager | Read, Write, Add/Remove readers/writers | Delegated management |
| writer | Read, Write | Collaborators, Team Members |
| reader | Read Only | Observers, Public Links |
| writeOnly | Write Only (Blind submissions) | Voting, Dropboxes |
All users can downgrade themselves or leave a group. Admins cannot be removed/downgraded except by themselves. Managers cannot remove/downgrade each other, but can remove/downgrade lower roles.
Note: Only admins can delete CoValues.
Jazz使用固定角色,不支持自定义角色。
| 角色 | 权限 | 适用场景 |
|---|---|---|
| admin | 读写删除、邀请成员、撤销访问、修改角色 | 团队所有者、创建者 |
| manager | 读写、添加/移除读者/作者 | 授权管理者 |
| writer | 读写 | 协作者、团队成员 |
| reader | 只读 | 观察者、公开链接用户 |
| writeOnly | 仅可写入(盲提交) | 投票、提交箱场景 |
所有用户都可以降级自身角色或退出组。管理员只能由自己移除/降级,管理者之间无法互相移除/降级,但可以移除/降级更低权限的角色。
注意: 只有管理员可以删除CoValues。
Managing Groups
管理Groups
When assigning roles, you can add Accounts, Groups, or "everyone". When adding a group, the most permissive role wins for members with multiple entitlements.
ts
const group = co.group().create();
const bob = await co.account().load(bobsId);
if (bob.$isLoaded) {
group.addMember(bob, "writer");
group.addMember(bob, "reader"); // Change role
group.removeMember(bob);
}分配角色时,你可以添加Accounts、Groups或「everyone」。当添加组作为成员时,对于拥有多个权限的成员,将采用权限最高的角色。
ts
const group = co.group().create();
const bob = await co.account().load(bobsId);
if (bob.$isLoaded) {
group.addMember(bob, "writer");
group.addMember(bob, "reader"); // 修改角色
group.removeMember(bob);
}Validating Permissions
验证权限
ts
const red = MyCoMap.create({ color: "red" });
const me = co.account().getMe();
if (me.canAdmin(red)) {
console.log("I can add users of any role");
} else if (me.canManage(red)) {
console.log("I can share value with others");
} else if (me.canWrite(red)) {
console.log("I can edit value");
} else if (me.canRead(red)) {
console.log("I can view value");
}
// Or get role directly
red.$jazz.owner.getRoleOf(me.$jazz.id); // "admin"ts
const red = MyCoMap.create({ color: "red" });
const me = co.account().getMe();
if (me.canAdmin(red)) {
console.log("我可以添加任意角色的用户");
} else if (me.canManage(red)) {
console.log("我可以将该内容共享给他人");
} else if (me.canWrite(red)) {
console.log("我可以编辑该内容");
} else if (me.canRead(red)) {
console.log("我可以查看该内容");
}
// 或直接获取角色
red.$jazz.owner.getRoleOf(me.$jazz.id); // "admin"Fundamental Patterns
基础模式
Pattern 1: Creating Shared Data
模式1:创建共享数据
Must pass the correct owner explicitly to ensure visibility.
ts
// ❌ WRONG: Defaults to private, other members won't see it
const task = Task.create({ title: "Fix bug" });
project.tasks.push(task);
// ✅ RIGHT: Explicitly set owner
const task = Task.create(
{ title: "Fix bug" },
{ owner: project.$jazz.owner }
);
project.tasks.push(task);
// ✅ ALSO RIGHT: Create new group for independent permissions
const taskGroup = co.group().create();
taskGroup.addMember(project.$jazz.owner, 'writer');
const task = Task.create({ title: "Fix bug" }, { owner: taskGroup });Note: Inline creation (passing JSON) automatically handles group inheritance based on schema configuration (default is ).
extendsContainerYou MUST NOT use an as a CoValue owner.
Account必须显式传递正确的归属方,以确保数据可见性。
ts
// ❌ 错误:默认为私有数据,其他成员无法查看
const task = Task.create({ title: "修复漏洞" });
project.tasks.push(task);
// ✅ 正确:显式设置归属方
const task = Task.create(
{ title: "修复漏洞" },
{ owner: project.$jazz.owner }
);
project.tasks.push(task);
// ✅ 同样正确:创建新组以实现独立权限
const taskGroup = co.group().create();
taskGroup.addMember(project.$jazz.owner, 'writer');
const task = Task.create({ title: "修复漏洞" }, { owner: taskGroup });注意: 内联创建(传递JSON)会根据架构配置自动处理组继承(默认是)。
extendsContainer你绝对不能将作为CoValue的归属方。
AccountPattern 2: The Invite Flow
模式2:邀请流程
Creating Invite Links
创建邀请链接
React:
ts
import { createInviteLink } from "jazz-tools/react";
const inviteLink = createInviteLink(organization, "writer");Svelte:
ts
import { createInviteLink } from "jazz-tools/svelte";
const inviteLink = createInviteLink(organization, "writer");Generates URL:
.../#/invite/[CoValue ID]/[inviteSecret]React:
ts
import { createInviteLink } from "jazz-tools/react";
const inviteLink = createInviteLink(organization, "writer");Svelte:
ts
import { createInviteLink } from "jazz-tools/svelte";
const inviteLink = createInviteLink(organization, "writer");生成的URL格式:
.../#/invite/[CoValue ID]/[inviteSecret]Accepting Invites
接受邀请
React:
tsx
import { useAcceptInvite } from "jazz-tools/react";
useAcceptInvite({
invitedObjectSchema: Organization,
onAccept: async (organizationID) => {
const organization = await Organization.load(organizationID);
if (!organization.$isLoaded) throw new Error("Could not load");
me.root.organizations.$jazz.push(organization);
},
});Svelte:
svelte
<script lang="ts">
import { InviteListener } from "jazz-tools/svelte";
new InviteListener({
invitedObjectSchema: Organization,
onAccept: async (organizationID) => {
const organization = await Organization.load(organizationID);
if (!organization.$isLoaded) throw new Error("Could not load");
me.current.root.organizations.$jazz.push(organization);
},
});
</script>Programmatic:
ts
await account.acceptInvite(organizationId, inviteSecret, Organization);React:
tsx
import { useAcceptInvite } from "jazz-tools/react";
useAcceptInvite({
invitedObjectSchema: Organization,
onAccept: async (organizationID) => {
const organization = await Organization.load(organizationID);
if (!organization.$isLoaded) throw new Error("加载失败");
me.root.organizations.$jazz.push(organization);
},
});Svelte:
svelte
<script lang="ts">
import { InviteListener } from "jazz-tools/svelte";
new InviteListener({
invitedObjectSchema: Organization,
onAccept: async (organizationID) => {
const organization = await Organization.load(organizationID);
if (!organization.$isLoaded) throw new Error("加载失败");
me.current.root.organizations.$jazz.push(organization);
},
});
</script>程序化方式:
ts
await account.acceptInvite(organizationId, inviteSecret, Organization);Invite Secrets
邀请密钥
ts
const groupToInviteTo = Group.create();
const readerInvite = groupToInviteTo.$jazz.createInvite("reader");
await account.acceptInvite(group.$jazz.id, readerInvite);⚠️ Security: Invites do not expire and cannot be revoked. Never pass secrets as route parameters or query strings—only use fragment identifiers (hash in URL).
ts
const groupToInviteTo = Group.create();
const readerInvite = groupToInviteTo.$jazz.createInvite("reader");
await account.acceptInvite(group.$jazz.id, readerInvite);⚠️ 安全提示: 邀请不会过期且无法撤销。切勿将密钥作为路由参数或查询字符串传递——仅使用片段标识符(URL中的哈希部分)。
Pattern 3: Public Data
模式3:公开数据
"Public" means "readable by anyone who knows the CoValue ID".
ts
const group = Group.create();
group.addMember("everyone", "writer");
// Or use alias
group.makePublic("writer"); // Defaults to "reader"「公开」意味着「任何知道CoValue ID的人都可以读取」。
ts
const group = Group.create();
group.addMember("everyone", "writer");
// 或使用别名
group.makePublic("writer"); // 默认角色为"reader"Pattern 4: Requesting Invites
模式4:申请邀请
Use role for request lists—users can submit requests but not read others.
writeOnlyts
const JoinRequest = co.map({
account: co.account(),
status: z.literal(["pending", "approved", "rejected"]),
});
function createRequestsToJoin() {
const requestsGroup = Group.create();
requestsGroup.addMember("everyone", "writeOnly");
return RequestsList.create([], requestsGroup);
}
async function sendJoinRequest(requestsList, account) {
const request = JoinRequest.create(
{ account, status: "pending" },
requestsList.$jazz.owner
);
requestsList.$jazz.push(request);
}
async function approveJoinRequest(joinRequest, targetGroup) {
const account = await co.account().load(joinRequest.$jazz.refs.account.id);
if (account.$isLoaded) {
targetGroup.addMember(account, "reader");
joinRequest.$jazz.set("status", "approved");
return true;
}
return false;
}为请求列表设置角色——用户可以提交请求,但无法查看其他人的请求。
writeOnlyts
const JoinRequest = co.map({
account: co.account(),
status: z.literal(["pending", "approved", "rejected"]),
});
function createRequestsToJoin() {
const requestsGroup = Group.create();
requestsGroup.addMember("everyone", "writeOnly");
return RequestsList.create([], requestsGroup);
}
async function sendJoinRequest(requestsList, account) {
const request = JoinRequest.create(
{ account, status: "pending" },
requestsList.$jazz.owner
);
requestsList.$jazz.push(request);
}
async function approveJoinRequest(joinRequest, targetGroup) {
const account = await co.account().load(joinRequest.$jazz.refs.account.id);
if (account.$isLoaded) {
targetGroup.addMember(account, "reader");
joinRequest.$jazz.set("status", "approved");
return true;
}
return false;
}Pattern 5: Cascading Permissions (Groups as Members)
模式5:级联权限(组作为成员)
Groups can be added as members of other groups, creating hierarchies.
ts
const playlistGroup = Group.create();
const trackGroup = Group.create();
trackGroup.addMember(playlistGroup);When you add groups as members:
- Permissions are granted indirectly
- Roles are inherited (except )
writeOnly - Revoking access from member group removes access to container group
Warning: Deep nesting can cause performance issues.
Groups可以作为其他Groups的成员,从而创建层级结构。
ts
const playlistGroup = Group.create();
const trackGroup = Group.create();
trackGroup.addMember(playlistGroup);当你将组添加为成员时:
- 权限会间接授予
- 角色会被继承(除外)
writeOnly - 从成员组撤销访问权限会同时移除其对容器组的访问权限
注意: 过深的嵌套可能会导致性能问题。
Role Inheritance Rules
角色继承规则
Most Permissive Role Wins:
ts
const addedGroup = Group.create();
addedGroup.addMember(bob, "reader");
const containingGroup = Group.create();
containingGroup.addMember(bob, "writer");
containingGroup.addMember(addedGroup);
// Bob stays writer (higher than inherited reader)权限最高的角色优先:
ts
const addedGroup = Group.create();
addedGroup.addMember(bob, "reader");
const containingGroup = Group.create();
containingGroup.addMember(bob, "writer");
containingGroup.addMember(addedGroup);
// Bob仍然是writer角色(权限高于继承的reader)Overriding Roles
覆盖角色
ts
const organizationGroup = Group.create();
organizationGroup.addMember(bob, "admin");
const billingGroup = Group.create();
billingGroup.addMember(organizationGroup, "reader");
// All org members get reader access to billing, regardless of org rolets
const organizationGroup = Group.create();
organizationGroup.addMember(bob, "admin");
const billingGroup = Group.create();
billingGroup.addMember(organizationGroup, "reader");
// 所有组织成员对账单组仅有reader权限,无论其在组织中的角色Other Operations
其他操作
ts
// Remove group
containingGroup.removeMember(addedGroup);
// Get parent groups
containingGroup.getParentGroups(); // [addedGroup]ts
// 移除组
containingGroup.removeMember(addedGroup);
// 获取父组
containingGroup.getParentGroups(); // [addedGroup]Inline CoValue Creation
内联CoValue创建
Jazz automatically manages group ownership for nested CoValues:
ts
const board = Board.create({
title: "My board",
columns: [["Task 1.1", "Task 1.2"], ["Task 2.1", "Task 2.2"]],
});Each column and task gets a new group that inherits from the referencing CoValue's owner.
Jazz会自动管理嵌套CoValues的组归属权:
ts
const board = Board.create({
title: "我的看板",
columns: [["任务1.1", "任务1.2"], ["任务2.1", "任务2.2"]],
});每个列和任务都会创建一个新组,并从引用的CoValue归属方继承权限。
Example: Team Hierarchy
示例:团队层级
ts
const companyGroup = Group.create();
companyGroup.addMember(CEO, "admin");
const teamGroup = Group.create();
teamGroup.addMember(companyGroup);
teamGroup.addMember(teamLead, "admin");
teamGroup.addMember(developer, "writer");
const projectGroup = Group.create();
projectGroup.addMember(teamGroup);
projectGroup.addMember(client, "reader");ts
const companyGroup = Group.create();
companyGroup.addMember(CEO, "admin");
const teamGroup = Group.create();
teamGroup.addMember(companyGroup);
teamGroup.addMember(teamLead, "admin");
teamGroup.addMember(developer, "writer");
const projectGroup = Group.create();
projectGroup.addMember(teamGroup);
projectGroup.addMember(client, "reader");Troubleshooting
故障排查
"User cannot see data"
「用户无法查看数据」
- Verify Ownership: Is the CoValue owned by the expected Group? Check .
$jazz.owner - Verify Membership: Is the target user a member of that Group?
- Check References: Does the user have a way to discover the ID (e.g. through a reference in their account root)?
- 验证归属权: CoValue是否由预期的Group所有?检查。
$jazz.owner - 验证成员身份: 目标用户是否是该Group的成员?
- 检查引用: 用户是否有途径获取该ID(例如通过其账户根节点中的引用)?
"Data is read-only"
「数据为只读状态」
- Check Role: Use to check the actual role.
red.$jazz.owner.getRoleOf(me.$jazz.id)cannot write.reader
- 检查角色: 使用查看实际角色。
red.$jazz.owner.getRoleOf(me.$jazz.id)角色无法写入。reader
Cascading Issues
级联问题
- Group Membership: If Group A is a member of Group B, check that the user is a member of Group A with a role that permits the desired action in Group B.
- Unsupported Roles: Remember does not cascade.
writeOnly
- 组成员身份: 如果组A是组B的成员,请检查用户是否在组A中拥有允许其在组B中执行操作的角色。
- 不支持的角色: 请注意角色不会被级联继承。
writeOnly
Quick Reference
快速参考
Ownership: Admin/manager modifies group membership, not CoValue ownership, which cannot be modified.
Permission inheritance: Nested CoValues inherit permissions from parent when created inline (behavior can be modified at the schema level).
Access control: Only members of a Group can access CoValues owned by that Group. References alone don't grant access.
Public access: or makes Groups readable by anyone with the Group ID.
makePublic()addMember("everyone", "reader")Invite links: generates shareable URLs. Accept with (React), (Svelte) or .
createInviteLink()useAcceptInvite()InviteListeneraccount.acceptInvite()Invite security: Never pass secrets as route parameters/query strings. Only use fragment identifiers.
Requesting access: role on requests list allows non-members to submit join requests for admin review.
writeOnlyCascading: Groups can be members of other groups. Roles inherit (admin, manager, writer, reader), but doesn't. Most permissive role wins. Use to inspect hierarchy.
writeOnlygetParentGroups()归属权: 管理员/管理者可以修改组成员身份,但无法修改CoValue的归属权,归属权一旦确定不可更改。
权限继承: 内联创建的嵌套CoValues会从父级继承权限(该行为可在架构层面修改)。
访问控制: 只有Group的成员才能访问该Group拥有的CoValues。仅通过引用无法获得访问权限。
公开访问: 使用或可让任何知道Group ID的人读取该组内容。
makePublic()addMember("everyone", "reader")邀请链接: 生成可共享的URL。可通过(React)、(Svelte)或接受邀请。
createInviteLink()useAcceptInvite()InviteListeneraccount.acceptInvite()邀请安全: 切勿将密钥作为路由参数/查询字符串传递,仅使用片段标识符。
申请访问: 为请求列表设置角色,允许非成员提交加入请求供管理员审核。
writeOnly级联: Groups可以作为其他组的成员。角色(admin、manager、writer、reader)会被继承,但除外。权限最高的角色优先。可使用查看层级结构。
writeOnlygetParentGroups()References
参考链接
- Permissions Overview: https://jazz.tools/docs/permissions-and-sharing/overview.md
- Invite Links: https://jazz.tools/docs/permissions-and-sharing/sharing.md
- Cascading Rules: https://jazz.tools/docs/permissions-and-sharing/cascading-permissions.md
- Chat Example: https://github.com/garden-co/jazz/tree/main/examples/chat
- Todo Example: https://github.com/garden-co/jazz/tree/main/examples/todo
When using an online reference via a skill, cite the specific URL to the user to build trust.
- 权限概览:https://jazz.tools/docs/permissions-and-sharing/overview.md
- 邀请链接:https://jazz.tools/docs/permissions-and-sharing/sharing.md
- 级联规则:https://jazz.tools/docs/permissions-and-sharing/cascading-permissions.md
- 聊天示例:https://github.com/garden-co/jazz/tree/main/examples/chat
- 待办示例:https://github.com/garden-co/jazz/tree/main/examples/todo
当通过技能引用在线资源时,请向用户提供具体URL以提升可信度。