jazz-permissions-security

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Jazz 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
    jazz-schema-design
    skill)
  • 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.
RoleCapabilityBest For
adminRead, Write, Delete, Invite Members, Revoke Access, Change RolesTeam Owners, Creators
managerRead, Write, Add/Remove readers/writersDelegated management
writerRead, WriteCollaborators, Team Members
readerRead OnlyObservers, Public Links
writeOnlyWrite 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);
}
分配角色时,你可以添加AccountsGroups或「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
extendsContainer
).
You MUST NOT use an
Account
as a CoValue owner.
必须显式传递正确的归属方,以确保数据可见性。
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
)。
绝对不能
Account
作为CoValue的归属方。

Pattern 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
writeOnly
role for request lists—users can submit requests but not read others.
ts
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;
}
为请求列表设置
writeOnly
角色——用户可以提交请求,但无法查看其他人的请求。
ts
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 role
ts
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"

「用户无法查看数据」

  1. Verify Ownership: Is the CoValue owned by the expected Group? Check
    $jazz.owner
    .
  2. Verify Membership: Is the target user a member of that Group?
  3. Check References: Does the user have a way to discover the ID (e.g. through a reference in their account root)?
  1. 验证归属权: CoValue是否由预期的Group所有?检查
    $jazz.owner
  2. 验证成员身份: 目标用户是否是该Group的成员?
  3. 检查引用: 用户是否有途径获取该ID(例如通过其账户根节点中的引用)?

"Data is read-only"

「数据为只读状态」

  1. Check Role: Use
    red.$jazz.owner.getRoleOf(me.$jazz.id)
    to check the actual role.
    reader
    cannot write.
  1. 检查角色: 使用
    red.$jazz.owner.getRoleOf(me.$jazz.id)
    查看实际角色。
    reader
    角色无法写入。

Cascading Issues

级联问题

  1. 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.
  2. Unsupported Roles: Remember
    writeOnly
    does not cascade.
  1. 组成员身份: 如果组A是组B的成员,请检查用户是否在组A中拥有允许其在组B中执行操作的角色。
  2. 不支持的角色: 请注意
    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:
makePublic()
or
addMember("everyone", "reader")
makes Groups readable by anyone with the Group ID.
Invite links:
createInviteLink()
generates shareable URLs. Accept with
useAcceptInvite()
(React),
InviteListener
(Svelte) or
account.acceptInvite()
.
Invite security: Never pass secrets as route parameters/query strings. Only use fragment identifiers.
Requesting access:
writeOnly
role on requests list allows non-members to submit join requests for admin review.
Cascading: Groups can be members of other groups. Roles inherit (admin, manager, writer, reader), but
writeOnly
doesn't. Most permissive role wins. Use
getParentGroups()
to inspect hierarchy.
归属权: 管理员/管理者可以修改组成员身份,但无法修改CoValue的归属权,归属权一旦确定不可更改。
权限继承: 内联创建的嵌套CoValues会从父级继承权限(该行为可在架构层面修改)。
访问控制: 只有Group的成员才能访问该Group拥有的CoValues。仅通过引用无法获得访问权限。
公开访问: 使用
makePublic()
addMember("everyone", "reader")
可让任何知道Group ID的人读取该组内容。
邀请链接:
createInviteLink()
生成可共享的URL。可通过
useAcceptInvite()
(React)、
InviteListener
(Svelte)或
account.acceptInvite()
接受邀请。
邀请安全: 切勿将密钥作为路由参数/查询字符串传递,仅使用片段标识符。
申请访问: 为请求列表设置
writeOnly
角色,允许非成员提交加入请求供管理员审核。
级联: Groups可以作为其他组的成员。角色(admin、manager、writer、reader)会被继承,但
writeOnly
除外。权限最高的角色优先。可使用
getParentGroups()
查看层级结构。

References

参考链接