Aletheia

31. Team mode

Status: implemented and tested, 2026-10-26 (T-112). 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 (T-104/T-108); 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: T-115 is not started, 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.