docs
/
AppEngine API

Workspaces

Team workspaces and conversations — members, a journal of messages, files, tasks, meetings and agendas, with a personal Home, Inbox, My Tasks and My Calendar.

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 gets 404 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 403 with reason: 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 — expiresAt on a workspace or a member is compared with now on every read. A member whose status is invited, inactive or expired has 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:

GET/workspace/homeJWT
GET/workspace/home/inboxJWT
GET/workspace/home/tasksJWT
GET/workspace/home/calendarJWT
GET/workspace/home/filesJWT
GET/workspace/home/searchJWT
  • home — me, workspaces, conversations (each with unread), 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 carries kind and unread.
  • 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

GET/workspaceJWT
POST/workspaceJWT
GET/workspace/:idJWT
PATCH/workspace/:idJWT
POST/workspace/:id/archiveJWT
POST/workspace/:id/restoreJWT

GET /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

GET/workspace/:id/membersJWT
POST/workspace/:id/membersJWT
PATCH/workspace/:id/members/:emailJWT
DELETE/workspace/:id/members/:emailJWT
POST/workspace/:id/joinJWT
POST/workspace/:id/leaveJWT
POST/workspace/:id/notifyJWT
  • Add (any member) — { members: [{ email, name?, accessType?, external?, expiresAt? }], redirectUrl }. accessType is admin, member or guest. 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 as invited with no access until it is accepted. redirectUrl is where that outsider lands to finish signing up.
  • Patch — admins change accessType and expiresAt. Anyone may change their own notify (all · mentions · none) and mutedUntil; POST :id/notify is 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

GET/workspace/:id/itemsJWT
POST/workspace/:id/itemJWT
GET/workspace/item/:iidJWT
PATCH/workspace/item/:iidJWT
DELETE/workspace/item/:iidJWT
GET/workspace/:id/roomsJWT
POST/workspace/:id/pin/:iidJWT
DELETE/workspace/:id/pin/:iidJWT
POST/workspace/:id/readJWT

POST :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 task record owned by the workspace and links it in the item's data[]; assignees are notified. Conversations refuse tasks.
  • Meeting — type: meeting becomes an event item backed by a reservation on the org's meeting reservation definition. Without that definition the call answers 422.
  • Rooms are labels on message items (room: { id, label }), not records; GET :id/rooms lists them with counts.
  • Replies — parentItem makes a reply, one level deep only.
  • Mentions — @email in the text, plus any listed in mentions; 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

GET/workspace/:id/tasksJWT
GET/workspace/:id/tasks/:tidJWT
PATCH/workspace/:id/tasks/:tidJWT
GET/workspace/:id/meetings/:midJWT
PATCH/workspace/:id/meetings/:midJWT
GET/workspace/:id/filesJWT
GET/workspace/:id/calendarJWT
GET/workspace/:id/agendaJWT
GET/workspace/:id/analyticsJWT
GET/workspace/:id/searchJWT
  • tasks — the board: ?status=, ?assignTo=, ?agenda=<id>|none. Each task carries its journal item's files and room, its agenda, and overdue.
  • PATCH tasks/:tid (members) — { title, description, status, dueDate, assignTo[], agenda }. Each change is appended to the task's history and 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 with progress (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

POST/workspace/digest/runJWT
POST/workspace/migrateJWT

digest/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.