Aletheia

31. Team mode

Status: implemented and tested, 2026-10-26. This page describes what the code does. It does not claim SSO, SCIM, compliance certificates or an SLA; none of those are built.

Team mode is a small layer on top of Aletheia Sync (29-sync.md). It stores named projects, members, shared entries, a board and a brief in team.db, a separate SQLite file next to sync.db. Every route and CLI command reads that store; the store makes the access decision.

31.1 Model

TableHolds
teamone row per team, scoped to a sync account
team_projectthe named projects that belong to a team
team_membera member: one sync device token plus a role (owner, editor, viewer)
team_sharethe projects a non-owner member may read and write
team_entryshared memory (memory, fact, note) and board entries (pin, decision) with author and time
team_auditappend-only team actions; ids and counts, never entry text

A member is a device token from the sync account; the raw token is shown once when the member is created. A team is isolated by account_id. Joining a team shares nothing: a member sees only the projects in team_share. For an owner, the project list is the team's projects.

31.2 Roles

Actionownereditorviewer
read a shared projectyesassigned projectsassigned projects
write shared memory or a board entryyesassigned projectsno (403)
add projects, members, shares; revoke; read the member listyesnono
leave the teamyesyesyes

authorize() in src/hub/team.mjs is the single check. The HTTP routes call it, and so does every CLI and test call; a route cannot be the only gate. A revoked member gets 401 member_revoked; a token revoked in the sync store gets 401 token_revoked; the wrong project gets 403 project_forbidden.

31.4 Routes and commands

/v1/team/* needs a sync bearer token. /api/hub/team/* is the guarded local UI route and acts as the local operator. The full table is in 03-http-api.md. The CLI mirrors it under aletheia team ...; --token-id <id> acts as that member, and the default is the local operator.

31.5 Threat list and what is not covered

ThreatCovered
A viewer writesyes: store check, tested
A member reads a project they do not shareyes: store check on every read, tested
A member reads another teamyes: team id and account id are part of every query, tested
A revoked member keeps accessyes: next call fails, tested
A token revoked elsewhere keeps team accessyes: the token is re-checked, tested
A member leaves and its shared rows stay visibleyes: rows are tombstoned and shares removed, tested
One team's search returns another team's rowsyes: team_id is in the query, tested
A local UI user reads every teamyes by design: the /api/hub session is the operator. It is not a member-only path
A stolen device token reads its assigned projectsNOT covered: the token is the identity. Revoke it
E2EE team memoryNOT covered: a shared brief and shared search need server-readable text. Team mode runs on a plaintext sync account
A removed member's offline replicaNOT covered: the device keeps its local copy. Revoke the token and wipe the device
SSO, SCIM, compliance, audit exportNOT built and no certificate is claimed

31.6 Failure modes

FailureBehaviour
Member revoked during a pullthe next team call fails; the sync pull answers 401 on the next call
Project removed while a member is readingthe share remains but the project is no longer in team_project; reads answer 403
Duplicate team name in one account400; names are unique per account
Last owner revoked400; a team always keeps one active owner
A member leavestheir shared rows are tombstoned and their shares are deleted; the member row is revoked
Brief too longcut at maxChars (default 2000) and ends with …

31.7 Evidence

test/team-mode.test.mjs covers the store checks, cross-project and cross-team reads, token and member revocation, the board author/time, the brief's source ids, search ACLs, leave tombstones and the HTTP route behaviour. The task file carries the real command output. Nothing here was measured on two real machines.