Example: a group's events page
Everything from the previous guides in one flow. Set up a group calendar, list events with who's going, RSVP, and post an event as the group.
This walks through the events pages of a group’s website. Each step links to the guide that explains it.
The pieces
- Space:
at://<group>/space/fyi.opensocial.events/self, readable by members. - Events:
community.lexicon.calendar.eventrecords, written by the group into its own repo in that space. - RSVPs:
community.lexicon.calendar.rsvprecords, written by each member into their own repo in the same space, on their own PDS. - Scope:
atproto,include:fyi.opensocial.basePermissions?aud=…,include:fyi.opensocial.calendarPermissions, plus anrpc:…?aud=…entry for each host method the page calls:requestJoin,getJoinRequest,cancelJoinRequest,getGroupAuth, andcreateSpaceif it sets up the calendar.listGroupsneeds none. See Signing in.
1. Make sure the calendar exists
Read the group’s space index: fyi.opensocial.space records in its members space (Reading a space). If none has type: "fyi.opensocial.events", an admin creates it with createSpace (Creating a space):
{
"group": "did:plc:…",
"type": "fyi.opensocial.events",
"skey": "self",
"name": "Events",
"readableBy": ["member"],
"credentialScopes": [{ "role": "admin", "scopes": ["community.lexicon.calendar.event"] }]
}
readableBy: ["member"] makes the calendar members-only. credentialScopes lets admins post events as the group. Members can write RSVPs through their own permission set.
2. List events and who’s going
With a credential for the events space:
listReposon the group’s host gives every writer: the group, plus each member who has RSVP’d.- From the host,
listRecordsfor the group’s repo, collectioncommunity.lexicon.calendar.event, gives the events. - From each member’s PDS,
listRecordsfor their repo, collectioncommunity.lexicon.calendar.rsvp, gives their RSVPs. - Group RSVPs by
subject.uri, which is the event’s URI.
const space = `at://${group}/space/fyi.opensocial.events/self`;
const records = await readSpace(session, space, [
"community.lexicon.calendar.event",
"community.lexicon.calendar.rsvp",
]);
const events = records.filter((r) => r.collection === "community.lexicon.calendar.event");
const going = (eventUri: string) =>
records.filter(
(r) =>
r.collection === "community.lexicon.calendar.rsvp" &&
r.value.subject.uri === eventUri &&
r.value.status === "community.lexicon.calendar.rsvp#going",
);
A reader who isn’t a member gets UserNotAuthorized at the credential step. Show a “members only” state, not an error:
getJoinRequest(?group=) tells you whether they’ve already asked. Ifrequestis present, show “request pending”.- Otherwise offer “Ask to join”, which calls
requestJoinwith{ group, message }. It returnsadmittedfor an open group orpendingfor one that reviews requests. cancelJoinRequestwithdraws a pending request.
3. RSVP
A member writes their RSVP from their own PDS. The rkey is the event’s rkey, so changing their answer overwrites it (A member’s own records). The calendar lexicons declare tid keys; reusing the event’s rkey departs from that on purpose, so one member has at most one RSVP per event. Seeded events may have readable rkeys like cougar-loop, so don’t assume every key is a TID:
await new Client(session).call(com.atproto.space.putRecord, {
space,
repo: session.did,
collection: "community.lexicon.calendar.rsvp",
rkey: event.rkey,
record: {
$type: "community.lexicon.calendar.rsvp",
subject: { uri: event.uri, cid: event.cid },
status: "community.lexicon.calendar.rsvp#going",
createdAt: new Date().toISOString(),
},
});
4. Post an event as the group
An admin posts the event, and the group authors it. Get a getGroupAuth token for the events space, then write into the group’s repo on the host (Writing as the group):
const grant = await getGroupAuth(session, group, space); // { accessJwt, pds, … }
await createRecordWithDpop(grant, {
space,
repo: group,
collection: "community.lexicon.calendar.event",
record: {
$type: "community.lexicon.calendar.event",
name: "Saturday coffee ride",
startsAt: "2026-10-03T15:00:00.000Z",
mode: "community.lexicon.calendar.event#inperson",
status: "community.lexicon.calendar.event#scheduled",
createdAt: new Date().toISOString(),
},
});
getGroupAuth returns Forbidden for someone whose role has no credentialScopes in this space. To know in advance who may post, and show the “new event” button only to them:
- Read the viewer’s roles: their
fyi.opensocial.membershiprecord in themembersspace (repo = the group, rkey = the viewer’s DID) hasroles. - Read the events space’s
accessrecord:fyi.opensocial.access, rkeyself, in the group’s repo in that space. - The viewer may post if any entry in
credentialScopesnames one of their roles and listscommunity.lexicon.calendar.event, or"*", which means every collection in the space.
5. Signed-out visitors
A signed-out visitor can’t read the events space. Show the group’s name, description and rules from listGroups and a sign-in button.