WorkspaceModule serves /workspace/* (40 routes): Slack-style workspaces and conversations for the people of an org. The caller is always a person — a signed-in user or customer, resolved the same way as CurrentCustomerOrUser elsewhere. A site's app token alone is no one. Live updates go out on the /workspace socket namespace.
Datatypes: workspace (the workspace or conversation, with its members[] inside it), workspace_item (the journal), plus task and reservation records owned by the workspace.
Who sees what
One service, WorkspaceAccessService, decides every read, list, search, notification and analytics call:
- Private (
isPrivate: true) — you have to be invited or added; everyone else gets404 Workspace not found, so it looks like it does not exist. - Public — anyone in the org can read; only members post. Posting without joining answers
403withreason: join-required. - Members see everything inside. Items have no visibility of their own.
- External people — any customer account, a guest user, or a member flagged
external— see only what they are members of and never browse, and cannot create workspaces. - Admin — the author, or a member with
accessType: admin. Editing, archiving and managing other members need admin. - Expiry —
expiresAton a workspace or a member is compared with now on every read. A member whosestatusisinvited,inactiveorexpiredhas no access.
All 40 routes are jwt; none use @PublicRoute, @StaffOnly or @Roles. Rules are enforced inside the service as above.
Home
The personal views across everything the caller has joined:
/workspace/homeJWT/workspace/home/inboxJWT/workspace/home/tasksJWT/workspace/home/calendarJWT/workspace/home/filesJWT/workspace/home/searchJWThome—me,workspaces,conversations(each withunread),unreadTotal, open tasks assigned to me, upcoming meetings, recent activity.home/inbox— mentions, direct messages and replies to my items;?kind=mentions|direct|replies. Each row carrieskindandunread.home/tasks— tasks assigned to me or created by me;?status=takes a comma list.home/calendar—{ events, meetings };?from=&to=filter meetings by start time.home/files— items that are files or carry files;?workspace=,?by=,?limit=(max 500).home/search?q=— items and tasks matching the text.
These use only workspaces the caller has joined, not every public one they could browse.
Workspaces and conversations
/workspaceJWT/workspaceJWT/workspace/:idJWT/workspace/:idJWT/workspace/:id/archiveJWT/workspace/:id/restoreJWTGET /workspace lists what the caller can read; ?type=workspace|conversation, ?joined=true for joined only. A conversation is the same record without tasks.
POST takes { type, title, description, icon, isPrivate, expiresAt, members, redirectUrl }. With type: conversation and direct: true it is a direct conversation — always private, one per set of people: asking again returns the existing one with existing: true. The creator becomes admin.
PATCH (admin) accepts title, description, icon, isPrivate, expiresAt, pinnedItems, intakeForm. Archive and restore are admin-only.
Members
/workspace/:id/membersJWT/workspace/:id/membersJWT/workspace/:id/members/:emailJWT/workspace/:id/members/:emailJWT/workspace/:id/joinJWT/workspace/:id/leaveJWT/workspace/:id/notifyJWT- Add (any member) —
{ members: [{ email, name?, accessType?, external?, expiresAt? }], redirectUrl }.accessTypeisadmin,memberorguest. Members must be email addresses of a user or customer of the org — never a group. An address not yet in the org gets an invitation (valid 168 hours) and sits asinvitedwith no access until it is accepted.redirectUrlis where that outsider lands to finish signing up. - Patch — admins change
accessTypeandexpiresAt. Anyone may change their ownnotify(all·mentions·none) andmutedUntil;POST :id/notifyis that shortcut. The person is emailed whenever their access changes. - Remove — admin, or yourself (
leave). The owner cannot be removed by someone else. - Join — public workspaces only; a private one answers
403.
Every membership change also writes a member item to the journal.
The journal
/workspace/:id/itemsJWT/workspace/:id/itemJWT/workspace/item/:iidJWT/workspace/item/:iidJWT/workspace/item/:iidJWT/workspace/:id/roomsJWT/workspace/:id/pin/:iidJWT/workspace/:id/pin/:iidJWT/workspace/:id/readJWTPOST :id/item (members only) creates any item type in one call: message, file, task, event, agenda, analytics, data, team, block. member items are written only by the member routes.
- Task — creates a
taskrecord owned by the workspace and links it in the item'sdata[]; assignees are notified. Conversations refuse tasks. - Meeting —
type: meetingbecomes aneventitem backed by areservationon the org'smeetingreservation definition. Without that definition the call answers422. - Rooms are labels on message items (
room: { id, label }), not records;GET :id/roomslists them with counts. - Replies —
parentItemmakes a reply, one level deep only. - Mentions —
@emailin the text, plus any listed inmentions; members mentioned are notified.
GET :id/items with no filter is the Activity view; ?type=, ?room=, ?thread=<itemId>, ?before=, ?limit= (max 200). Items past their expiresAt are hidden unless ?includeExpired=true.
Edit and delete are for the item's author or a workspace admin. Delete is a tombstone: the card stays, its message, summary and files are blanked. read sets your lastReadAt, which drives unread counts.
Tasks, meetings and views
/workspace/:id/tasksJWT/workspace/:id/tasks/:tidJWT/workspace/:id/tasks/:tidJWT/workspace/:id/meetings/:midJWT/workspace/:id/meetings/:midJWT/workspace/:id/filesJWT/workspace/:id/calendarJWT/workspace/:id/agendaJWT/workspace/:id/analyticsJWT/workspace/:id/searchJWTtasks— the board:?status=,?assignTo=,?agenda=<id>|none. Each task carries its journal item's files and room, its agenda, andoverdue.PATCH tasks/:tid(members) —{ title, description, status, dueDate, assignTo[], agenda }. Each change is appended to the task'shistoryand mirrored on its journal card; newly assigned people are notified.PATCH meetings/:mid(members) —{ title, startTime, endTime, timezone, meetingLink, meetingInfo, invites[], cancel: true }. The meeting must end after it starts.agenda— agenda items are the workspace's projects, each withprogress(total,done,open,overdue) from its tasks.analytics— task counts by status and assignee, item counts by type and week, members active in the last 30 days.
Maintenance
/workspace/digest/runJWT/workspace/migrateJWTdigest/run sends the "while you were away" emails now; ?graceMinutes=0 includes everything unread. migrate fills in missing type / status on old workspaces and items and is idempotent. Neither route checks for a workspace or org admin in the code.
Two scheduled jobs run for every org: the digest every 15 minutes (unread activity older than 30 minutes; skips people who are online, muted or set to none), and the expiry sweep every 10 minutes (marks expired members, archives expired workspaces, notifies).
Live updates
Socket.IO namespace /workspace, connected with auth: { token, orgId }. The token must be a user or customer of that org. On connect the socket joins a room for every workspace the person can read.
Events out: item.created, item.updated, item.deleted, member.added, member.updated, member.removed, member.left, workspace.created, workspace.updated, workspace.archived, workspace.restored, typing, read. Send subscribe, typing and read with { workspaceId }. A locked account is sent account_locked and disconnected.