Public sharing and invites
Public sharing
You can share CoValues publicly by setting the owner to a Group, and granting access to "everyone".
constconst group: Groupgroup =class GroupGroup.create();Group.create<Group>(this: CoValueClass<Group>, options?: { owner: Account; } | Account): Groupconst group: Groupgroup.Group.addMember(member: Everyone, role: "writer" | "reader" | "writeOnly"): void (+3 overloads)addMember("everyone", "writer");
You can also use makePublic(role) alias to grant access to everyone with a specific role (defaults to reader).
constconst group: Groupgroup =class GroupGroup.create();Group.create<Group>(this: CoValueClass<Group>, options?: { owner: Account; } | Account): Groupconst group: Groupgroup.Group.addMember(member: Everyone, role: "writer" | "reader" | "writeOnly"): void (+3 overloads)addMember("everyone", "writer");const group: Groupgroup.Group.makePublic(role?: "reader" | "writer"): GroupMake the group public, so that everyone can read it. Alias for `addMember("everyone", role)`.makePublic("writer"); // group.makePublic(); // Defaults to "reader" access
This is done in the chat example where anyone can join the chat, and send messages.
You can also add members by Account ID.
Invites
You can grant users access to a CoValue by sending them an invite link.
This is used in the todo example.
It generates a URL that looks like .../invite/[CoValue ID]/[inviteSecret]
In your app, you need to handle this route, and let the user accept the invitation, as done here.
You can accept an invitation programmatically by using the acceptInvite method on an account.
Pass the ID of the CoValue you're being invited to, the secret from the invite link, and the schema of the CoValue.
awaitconst account: Accountaccount.Account.acceptInvite<co.Map<{ name: z.z.ZodString; }, unknown, Account | Group>>(valueID: string, inviteSecret: InviteSecret, coValueClass?: co.Map<{ name: z.z.ZodString; }, unknown, Account | Group> | undefined): Promise<...>Accept an invite to a `CoValue` or `Group`.acceptInvite(const organizationId: ""organizationId,const inviteSecret: "inviteSecret_z"inviteSecret,Organization );const Organization: co.Map<{ name: z.z.ZodString; }, unknown, Account | Group>
Invite Secrets
The invite links generated by Jazz are convenient ways of handling invites.
In case you would prefer more direct control over the invite, you can create an invite to a Group using Group.createInvite(id, role) or group.$jazz.createInvite(role).
This will generate a string starting with inviteSecret_. You can then accept this invite using acceptInvite, with the group ID as the first argument, and the invite secret as the second.
const group = Group.create(); const readerInvite = group.$jazz.createInvite('reader'); // `inviteSecret_` await account.acceptInvite( group.$jazz.id, readerInvite );
Invites do not expire and cannot be revoked. If you choose to generate your own secrets in this way, take care that they are not shared in plain text over an insecure channel.
One particularly tempting mistake is passing the secret as a route parameter or a query. However, this will cause your secret to appear in server logs. You should only ever use fragment identifiers (i.e. parts after the hash in the URL) to share secrets, as these are not sent to the server (see the createInviteLink implementation).
Requesting Invites
To allow a non-group member to request an invitation to a group you can use the writeOnly role.
This means that users only have write access to a specific requests list (they can't read other requests).
However, Administrators can review and approve these requests.
Create the data models.
constJoinRequest =const JoinRequest: co.Map<{ account: <Shape extends BaseAccountShape>(shape?: Shape) => co.Account<Shape>; status: z.z.ZodLiteral<"pending" | "approved" | "rejected">; }, unknown, Account | Group>import coco.map({map<{ account: <Shape extends BaseAccountShape>(shape?: Shape) => co.Account<Shape>; status: z.z.ZodLiteral<"pending" | "approved" | "rejected">; }>(shape: { ...; }): co.Map<...> export mapaccount: <Shape extends BaseAccountShape>(shape?: Shape) => co.Account<Shape>account:import coco.const account: <Shape extends BaseAccountShape>(shape?: Shape) => co.Account<Shape> export accountDefines a collaborative account schema for Jazz applications. Creates an account schema that represents a user account with profile and root data. Accounts are the primary way to identify and manage users in Jazz applications.account,status: z.z.ZodLiteral<"pending" | "approved" | "rejected">status:import zz.literal(["pending", "approved", "rejected"]), }); constliteral<readonly ["pending", "approved", "rejected"]>(value: readonly ["pending", "approved", "rejected"], params?: string | z.z.core.$ZodLiteralParams): z.z.ZodLiteral<"pending" | "approved" | "rejected"> (+1 overload) export literalRequestsList =const RequestsList: co.List<co.Map<{ account: <Shape extends BaseAccountShape>(shape?: Shape) => co.Account<Shape>; status: z.z.ZodLiteral<"pending" | "approved" | "rejected">; }, unknown, Account | Group>>import coco.list(list<co.Map<{ account: <Shape extends BaseAccountShape>(shape?: Shape) => co.Account<Shape>; status: z.z.ZodLiteral<"pending" | "approved" | "rejected">; }, unknown, Account | Group>>(element: co.Map<...>): co.List<...> export listJoinRequest);const JoinRequest: co.Map<{ account: <Shape extends BaseAccountShape>(shape?: Shape) => co.Account<Shape>; status: z.z.ZodLiteral<"pending" | "approved" | "rejected">; }, unknown, Account | Group>
Set up the request system with appropriate access controls.
functioncreateRequestsToJoin() { constfunction createRequestsToJoin(): CoListInstance<co.Map<{ account: co.Account<BaseAccountShape>; status: z.z.ZodLiteral<"pending" | "approved" | "rejected">; }, unknown, Account | Group>>const requestsGroup: GrouprequestsGroup =class GroupGroup.create();Group.create<Group>(this: CoValueClass<Group>, options?: { owner: Account; } | Account): Groupconst requestsGroup: GrouprequestsGroup.Group.addMember(member: Everyone, role: "writer" | "reader" | "writeOnly"): void (+3 overloads)addMember("everyone", "writeOnly"); returnRequestsList.const RequestsList: co.List<co.Map<{ account: co.Account<BaseAccountShape>; status: z.z.ZodLiteral<"pending" | "approved" | "rejected">; }, unknown, Account | Group>>create([],CoListSchema<CoMapSchema<{ account: AccountSchema<BaseAccountShape>; status: ZodLiteral<"pending" | "approved" | "rejected">; }, unknown, Account | Group>>.create(items: readonly (({ readonly account: ({ readonly profile: ({ readonly name: string; readonly inbox?: string | undefined; readonly inboxInvite?: string | undefined; } & CoMap) | null; readonly root: ({ ...; } & CoMap) | null; } & Account) | null; readonly status: "pending" | ... 1 more ... | "rejected"; } & CoMap) | { ...; })[], options?: { owner: Group; unique?: CoValueUniqueness["uniqueness"]; } | Group): CoListInstance<...> (+1 overload)const requestsGroup: GrouprequestsGroup); } async functionsendJoinRequest(function sendJoinRequest(requestsList: co.loaded<typeof RequestsList>, account: Account): Promise<{ readonly account: { readonly profile: { readonly name: string; readonly inbox?: string | undefined; readonly inboxInvite?: string | undefined; } & CoMap; readonly root: { ...; } & CoMap; } & Account; readonly status: "pending" | ... 1 more ... | "rejected"; } & CoMap>requestsList:requestsList: CoList<({ readonly account: ({ readonly profile: ({ readonly name: string; readonly inbox?: string | undefined; readonly inboxInvite?: string | undefined; } & CoMap) | null; readonly root: ({ ...; } & CoMap) | null; } & Account) | null; readonly status: "pending" | ... 1 more ... | "rejected"; } & CoMap) | null>import coco.loaded<typeoftype loaded<T extends CoValueClassOrSchema, R extends ResolveQuery<T> = true> = R extends boolean | undefined ? NonNullable<InstanceOfSchemaCoValuesNullable<T>> : [NonNullable<InstanceOfSchemaCoValuesNullable<T>>] extends [...] ? Exclude<...> extends CoValue ? R extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean ... export loadedRequestsList>,const RequestsList: co.List<co.Map<{ account: co.Account<BaseAccountShape>; status: z.z.ZodLiteral<"pending" | "approved" | "rejected">; }, unknown, Account | Group>>account:account: { readonly profile: ({ readonly name: string; readonly inbox?: string | undefined; readonly inboxInvite?: string | undefined; } & CoMap) | null; readonly root: ({ ...; } & CoMap) | null; } & AccountAccount, ) { consttype Account = { readonly profile: ({ readonly name: string; readonly inbox?: string | undefined; readonly inboxInvite?: string | undefined; } & CoMap) | null; readonly root: ({ ...; } & CoMap) | null; } & Accountrequest =const request: { readonly account: { readonly profile: { readonly name: string; readonly inbox?: string | undefined; readonly inboxInvite?: string | undefined; } & CoMap; readonly root: { readonly [x: string]: any; } & CoMap; } & Account; readonly status: "pending" | ... 1 more ... | "rejected"; } & CoMapJoinRequest.const JoinRequest: co.Map<{ account: co.Account<BaseAccountShape>; status: z.z.ZodLiteral<"pending" | "approved" | "rejected">; }, unknown, Account | Group>create( {CoMapSchema<{ account: AccountSchema<BaseAccountShape>; status: ZodLiteral<"pending" | "approved" | "rejected">; }, unknown, Account | Group>.create(init: { account: { readonly profile: ({ readonly name: string; readonly inbox?: string | undefined; readonly inboxInvite?: string | undefined; } & CoMap) | null; readonly root: ({ readonly [x: string]: any; } & CoMap) | null; } & Account; status: "pending" | ... 1 more ... | "rejected"; }, options?: { owner?: Group; unique?: CoValueUniqueness["uniqueness"]; } | Group): { ...; } & CoMap (+1 overload)account,account: { readonly profile: ({ readonly name: string; readonly inbox?: string | undefined; readonly inboxInvite?: string | undefined; } & CoMap) | null; readonly root: ({ ...; } & CoMap) | null; } & Accountstatus: "pending" | "approved" | "rejected"status: "pending", },requestsList.requestsList: CoList<({ readonly account: ({ readonly profile: ({ readonly name: string; readonly inbox?: string | undefined; readonly inboxInvite?: string | undefined; } & CoMap) | null; readonly root: ({ ...; } & CoMap) | null; } & Account) | null; readonly status: "pending" | ... 1 more ... | "rejected"; } & CoMap) | null>$jazz.CoList<({ readonly account: ({ readonly profile: ({ readonly name: string; readonly inbox?: string | undefined; readonly inboxInvite?: string | undefined; } & CoMap) | null; readonly root: ({ ...; } & CoMap) | null; } & Account) | null; readonly status: "pending" | ... 1 more ... | "rejected"; } & CoMap) | null>.$jazz: CoListJazzApi<CoList<({ readonly account: ({ readonly profile: ({ readonly name: string; readonly inbox?: string | undefined; readonly inboxInvite?: string | undefined; } & CoMap) | null; readonly root: ({ ...; } & CoMap) | null; } & Account) | null; readonly status: "pending" | ... 1 more ... | "rejected"; } & CoMap) | null>>CoListJazzApi<CoList<({ readonly account: ({ readonly profile: ({ readonly name: string; readonly inbox?: string | undefined; readonly inboxInvite?: string | undefined; } & CoMap) | null; readonly root: ({ ...; } & CoMap) | null; } & Account) | null; readonly status: "pending" | ... 1 more ... | "rejected"; } & CoMap) | null>>.owner: Groupowner // Inherit the access controls of the requestsList );requestsList.requestsList: CoList<({ readonly account: ({ readonly profile: ({ readonly name: string; readonly inbox?: string | undefined; readonly inboxInvite?: string | undefined; } & CoMap) | null; readonly root: ({ ...; } & CoMap) | null; } & Account) | null; readonly status: "pending" | ... 1 more ... | "rejected"; } & CoMap) | null>$jazz.CoList<({ readonly account: ({ readonly profile: ({ readonly name: string; readonly inbox?: string | undefined; readonly inboxInvite?: string | undefined; } & CoMap) | null; readonly root: ({ ...; } & CoMap) | null; } & Account) | null; readonly status: "pending" | ... 1 more ... | "rejected"; } & CoMap) | null>.$jazz: CoListJazzApi<CoList<({ readonly account: ({ readonly profile: ({ readonly name: string; readonly inbox?: string | undefined; readonly inboxInvite?: string | undefined; } & CoMap) | null; readonly root: ({ ...; } & CoMap) | null; } & Account) | null; readonly status: "pending" | ... 1 more ... | "rejected"; } & CoMap) | null>>CoListJazzApi<CoList<({ readonly account: ({ readonly profile: ({ readonly name: string; readonly inbox?: string | undefined; readonly inboxInvite?: string | undefined; } & CoMap) | null; readonly root: ({ ...; } & CoMap) | null; } & Account) | null; readonly status: "pending" | ... 1 more ... | "rejected"; } & CoMap) | null>>.push(...items: (({ readonly account: ({ readonly profile: ({ readonly name: string; readonly inbox?: string | undefined; readonly inboxInvite?: string | undefined; } & CoMap) | null; readonly root: ({ ...; } & CoMap) | null; } & Account) | null; readonly status: "pending" | ... 1 more ... | "rejected"; } & CoMap) | CoMapInit<...> | null)[]): numberAppends new elements to the end of an array, and returns the new length of the array.push(request); returnconst request: { readonly account: { readonly profile: { readonly name: string; readonly inbox?: string | undefined; readonly inboxInvite?: string | undefined; } & CoMap; readonly root: { readonly [x: string]: any; } & CoMap; } & Account; readonly status: "pending" | ... 1 more ... | "rejected"; } & CoMaprequest; }const request: { readonly account: { readonly profile: { readonly name: string; readonly inbox?: string | undefined; readonly inboxInvite?: string | undefined; } & CoMap; readonly root: { readonly [x: string]: any; } & CoMap; } & Account; readonly status: "pending" | ... 1 more ... | "rejected"; } & CoMap
Using the write-only access users can submit requests that only administrators can review and approve.
async functionapproveJoinRequest(function approveJoinRequest(joinRequest: co.loaded<typeof JoinRequest, { account: true; }>, targetGroup: Group): Promise<boolean>joinRequest:joinRequest: CoMapLikeLoaded<{ readonly account: ({ readonly profile: ({ readonly name: string; readonly inbox?: string | undefined; readonly inboxInvite?: string | undefined; } & CoMap) | null; readonly root: ({ ...; } & CoMap) | null; } & Account) | null; readonly status: "pending" | ... 1 more ... | "rejected"; } & CoMap, { ...; }, 10, []>import coco.loaded<typeoftype loaded<T extends CoValueClassOrSchema, R extends ResolveQuery<T> = true> = R extends boolean | undefined ? NonNullable<InstanceOfSchemaCoValuesNullable<T>> : [NonNullable<InstanceOfSchemaCoValuesNullable<T>>] extends [...] ? Exclude<...> extends CoValue ? R extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean ... export loadedJoinRequest, {const JoinRequest: co.Map<{ account: co.Account<BaseAccountShape>; status: z.z.ZodLiteral<"pending" | "approved" | "rejected">; }, unknown, Account | Group>account: trueaccount: true }>,targetGroup: GrouptargetGroup:class GroupGroup, ) { constaccount = awaitconst account: ({ readonly profile: ({ readonly name: string; readonly inbox?: string | undefined; readonly inboxInvite?: string | undefined; } & CoMap) | null; readonly root: ({ ...; } & CoMap) | null; } & Account) | CoMapLikeLoaded<...> | nullconst Account: co.Account<BaseAccountShape>Account.load(AccountSchema<BaseAccountShape>.load: <ResolveQuery<co.Account<BaseAccountShape>>>(id: string, options?: { loadAs?: Account | AnonymousJazzAgent; resolve?: ({ ...; } & { ...; }) | ... 1 more ... | undefined; } | undefined) => Promise<...>joinRequest.joinRequest: CoMapLikeLoaded<{ readonly account: ({ readonly profile: ({ readonly name: string; readonly inbox?: string | undefined; readonly inboxInvite?: string | undefined; } & CoMap) | null; readonly root: ({ ...; } & CoMap) | null; } & Account) | null; readonly status: "pending" | ... 1 more ... | "rejected"; } & CoMap, { ...; }, 10, []>CoMap.$jazz: CoMapJazzApi<CoMapLikeLoaded<{ readonly account: ({ readonly profile: ({ readonly name: string; readonly inbox?: string | undefined; readonly inboxInvite?: string | undefined; } & CoMap) | null; readonly root: ({ ...; } & CoMap) | null; } & Account) | null; readonly status: "pending" | ... 1 more ... | "rejected"; } & CoMap, { ...; }, 10, []>>Jazz methods for CoMaps are inside this property. This allows CoMaps to be used as plain objects while still having access to Jazz methods, and also doesn't limit which key names can be used inside CoMaps.$jazz.CoMapJazzApi<CoMapLikeLoaded<{ readonly account: ({ readonly profile: ({ readonly name: string; readonly inbox?: string | undefined; readonly inboxInvite?: string | undefined; } & CoMap) | null; readonly root: ({ ...; } & CoMap) | null; } & Account) | null; readonly status: "pending" | ... 1 more ... | "rejected"; } & CoMap, { ...; }, 10, []>>.refs: { account: Ref<NonNullable<{ readonly profile: ({ readonly name: string; readonly inbox?: string | undefined; readonly inboxInvite?: string | undefined; } & CoMap) | null; readonly root: ({ ...; } & CoMap) | null; } & Account>>; }If property `prop` is a `coField.ref(...)`, you can use `coMap.$jazz.refs.prop` to access the `Ref` instead of the potentially loaded/null value. This allows you to always get the ID or load the value manually.refs.account.account: Ref<NonNullable<{ readonly profile: ({ readonly name: string; readonly inbox?: string | undefined; readonly inboxInvite?: string | undefined; } & CoMap) | null; readonly root: ({ ...; } & CoMap) | null; } & Account>>Ref<NonNullable<{ readonly profile: ({ readonly name: string; readonly inbox?: string | undefined; readonly inboxInvite?: string | undefined; } & CoMap) | null; readonly root: ({ ...; } & CoMap) | null; } & Account>>.id: stringid); if (account) {const account: ({ readonly profile: ({ readonly name: string; readonly inbox?: string | undefined; readonly inboxInvite?: string | undefined; } & CoMap) | null; readonly root: ({ ...; } & CoMap) | null; } & Account) | CoMapLikeLoaded<...> | nulltargetGroup: GrouptargetGroup.Group.addMember(member: Account, role: AccountRole): void (+3 overloads)addMember(account, "reader");const account: ({ readonly profile: ({ readonly name: string; readonly inbox?: string | undefined; readonly inboxInvite?: string | undefined; } & CoMap) | null; readonly root: ({ ...; } & CoMap) | null; } & Account) | CoMapLikeLoaded<...>joinRequest.joinRequest: CoMapLikeLoaded<{ readonly account: ({ readonly profile: ({ readonly name: string; readonly inbox?: string | undefined; readonly inboxInvite?: string | undefined; } & CoMap) | null; readonly root: ({ ...; } & CoMap) | null; } & Account) | null; readonly status: "pending" | ... 1 more ... | "rejected"; } & CoMap, { ...; }, 10, []>CoMap.$jazz: CoMapJazzApi<CoMapLikeLoaded<{ readonly account: ({ readonly profile: ({ readonly name: string; readonly inbox?: string | undefined; readonly inboxInvite?: string | undefined; } & CoMap) | null; readonly root: ({ ...; } & CoMap) | null; } & Account) | null; readonly status: "pending" | ... 1 more ... | "rejected"; } & CoMap, { ...; }, 10, []>>Jazz methods for CoMaps are inside this property. This allows CoMaps to be used as plain objects while still having access to Jazz methods, and also doesn't limit which key names can be used inside CoMaps.$jazz.CoMapJazzApi<CoMapLikeLoaded<{ readonly account: ({ readonly profile: ({ readonly name: string; readonly inbox?: string | undefined; readonly inboxInvite?: string | undefined; } & CoMap) | null; readonly root: ({ ...; } & CoMap) | null; } & Account) | null; readonly status: "pending" | ... 1 more ... | "rejected"; } & CoMap, { ...; }, 10, []>>.set<"status">(key: "status", value: "pending" | "approved" | "rejected"): voidSet a value on the CoMapset("status", "approved"); return true; } else { return false; } }