Teams & Scopes
xbrain's entire data isolation model is built on teams. Every piece of data — conversations, extracted facts, Drive files, agent outputs — belongs to exactly one team and is permanently tagged with that team's scope identifier. This is not an optional feature: it is enforced at every layer of the stack.
Team Isolation
In xbrain, every piece of data belongs to exactly one team. The X-Team-Scope
HTTP header is mandatory on every authenticated API call. Team A cannot read, write, or search
Team B's data — enforced at the API layer, not just the UI.
This isolation is absolute. Even if two users share the same Google account or the same LibreChat instance, their data remains separated if they belong to different teams. There is no cross-team query, no cross-team aggregation, and no way to bypass the scope check via the API.
Design Principle
Security by isolation, not by obscurity. The X-Team-Scope check is enforced
server-side on every write and every search. Removing it from the UI would not grant access
to another team's data.
The team_scope Field
Every memory item carries a team_scope field. If the item's
team_scope doesn't match the X-Team-Scope header sent with the
request, the write returns HTTP 400. This prevents data from being written to the wrong
team's namespace, even by accident.
bash# This works — team_scope matches header curl -X POST https://api.grooveos.app/v1/memory/upsert \ -H "X-Team-Scope: excalibur" \ -H "Authorization: Bearer $JWT" \ -H "Content-Type: application/json" \ -d '{ "item": { "team_scope": "excalibur", "content": "Q2 planning is confirmed for May 15th", "truth_level": "WORKING", "source": "librechat:conv_abc123" } }' # This fails — team_scope mismatch curl -X POST https://api.grooveos.app/v1/memory/upsert \ -H "X-Team-Scope: excalibur" \ -H "Authorization: Bearer $JWT" \ -H "Content-Type: application/json" \ -d '{ "item": { "team_scope": "other-team", "content": "..." } }' # → 400 Bad Request: item.team_scope must match X-Team-Scope header
The X-Team-Scope header is validated against the authenticated user's team
memberships. A user cannot set an arbitrary team scope — the value must correspond to a team
they are a member of.
Project Scopes
Within a team, data can be further scoped by project (project_scope). This
allows isolation of e.g. fundraising data from engineering data within the same team. Project
scopes are optional — if omitted, a memory item is visible to all team members.
The full tagging contract requires at minimum: team_scope,
truth_level, source, and visibility. The
project_scope and confidence fields are strongly recommended.
Visibility Options
| Visibility | Who Can See It | Typical Use Case |
|---|---|---|
team |
All authenticated members of the team | Shared facts, architecture decisions, team agreements |
project |
Only members with access to that project_scope |
Fundraising docs (separate from engineering data), client-specific data |
private |
Only the owner (source_user_id) |
Personal notes, draft thoughts not yet shared with the team |
json — memory item with full tagging contract{ "team_scope": "excalibur", "project_scope": "fundraising", "visibility": "project", "truth_level": "VALIDATED", "confidence": 0.95, "source": "librechat:conv_def456", "source_user_id": "google:108765432109876543", "validation_status": "peer_reviewed", "content": "Series A target: $3M at $15M pre-money valuation" }
User Management
Since Phase 10 the primary sign-in path is GitHub. Phase 12 then migrated
that path from the legacy OAuth App to a GitHub App (Client ID
Iv23liVnZvIN0Lo6isof), which supports multi-callback (web + Chrome extension),
short-lived installation tokens, refresh tokens (~6 month rolling sessions), and
install/uninstall webhooks. Google OAuth is preserved as a legacy fallback for
backward-compatible LibreChat / Open WebUI sign-ins.
- source_user_id — OIDC
subclaim. Format:github:{login}for GitHub-authenticated users (primary),google:108765432109876543for legacy Google OAuth users. - Superadmins — configured via the
ADMIN_USER_SUBSenv var (comma-separated subs). Superadmins gate access to the/v1/admin/brain/*dashboard endpoints introduced in Phase 11. - Team membership — managed via the memory-api admin endpoints, or auto-granted on the basis of GitHub Org membership (Phase 10).
GitHub App install flow
After signing in, if the user's primary GitHub org has not yet installed the xbrain App,
POST /v1/auth/github/signin returns
{install_required: true, install_url, org_login} instead of an
xbt_ token. The web app renders a banner with an
Install xbrain on <org> button. An org admin completes the GitHub install
screen (xbrain only requests Read on org members + account email + profile — no repo
permissions), at which point auto-grant team membership kicks in on the next sign-in.
Identity precedence
If a user has authenticated previously via Google and then re-authenticates via the GitHub
App, the two identities are merged on the same users.id via the
users.github_id field. Subsequent calls under either Bearer type resolve to
the same internal principal.
Single Identity Across Frontends
The xbrain GitHub App backs both the web app
(https://grooveos.app/account/teams/) and the Chrome extension
(https://anigikcnmldoklcmogffmgcojdhhficb.chromiumapp.org/) via two
callback URLs registered on the same App. A single sign-in on either surface produces
the same identity. LibreChat / Open WebUI on Google OAuth continue to map onto the same
users row via merged identity.
Creating Teams
Teams are created by an admin via the memory-api admin endpoints. Once created, a team has a
team_scope string identifier that is used in all API calls and memory items. The
identifier should be short, lowercase, and URL-safe (e.g. excalibur,
engineering, growth).
bash — create a new teamcurl -X POST https://api.grooveos.app/v1/admin/teams \ -H "Authorization: Bearer $ADMIN_JWT" \ -H "Content-Type: application/json" \ -d '{ "name": "Engineering Team", "scope": "engineering" }' # Response: # { # "team_id": "team_abc123", # "scope": "engineering", # "name": "Engineering Team", # "created_at": "2026-05-06T10:00:00Z" # }
After creating the team, add users to it via the team membership endpoint:
bash — add a user to a teamcurl -X POST https://api.grooveos.app/v1/admin/teams/engineering/members \ -H "Authorization: Bearer $ADMIN_JWT" \ -H "Content-Type: application/json" \ -d '{ "user_id": "google:108765432109876543", "role": "member" }'
Drive Folder Mapping per Team
Each team can have one or more Google Drive folders mapped to it. When a file is updated in a
mapped folder, it's automatically synced to xbrain and tagged with that team's scope.
Drive mappings tie a specific Google Drive folder to a team_scope and optionally
a project_scope.
bash — map a Drive folder to a teamcurl -X POST https://api.grooveos.app/v1/admin/drive/mappings \ -H "Authorization: Bearer $ADMIN_JWT" \ -H "Content-Type: application/json" \ -d '{ "team_scope": "excalibur", "project_scope": "fundraising", "folder_id": "1ABC...xyz", "folder_name": "Fundraising Docs" }' # After this mapping, any file added or updated in the Drive folder # is automatically picked up by drive-sync and stored in memory-api # with team_scope="excalibur" and project_scope="fundraising".
Multiple Folders per Team
Multiple Drive folders can be mapped to the same team with different
project_scope values. For example, the excalibur team might have
a fundraising folder and an engineering folder, each synced with
their respective project scopes. See the
Drive Sync documentation for details on webhook setup and
sync frequency.
Drive Mapping Properties
| Field | Required | Description |
|---|---|---|
team_scope |
Yes | The team that owns the synced content |
project_scope |
No | Optional project subdivision within the team |
folder_id |
Yes | Google Drive folder ID (from the URL) |
folder_name |
No | Human-readable name for the mapping (display only) |
watch_enabled |
No | Whether to set up a Drive push notification webhook (default: true) |