You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Workspaces are currently implicit — each user_id string gets its own siloed data. The WorkspacePool in server.rs caches per-user Workspace instances, but there's no shared-workspace concept beyond workspace_read_scopes: Vec<String> on UserIdentity, which is a flat string list with no DB backing, no membership semantics, and no write sharing.
This means:
No team collaboration — users can't share jobs, routines, or memory within a team workspace
Cross-user reads are token-config-only — workspace_read_scopes is set at token creation, not manageable at runtime
All data is scoped to a single user_id string — there's no way to assign a job or routine to a team/project rather than an individual
No workspace lifecycle — can't create, archive, or transfer workspaces
Depends on Issue #1 (DB-backed user management) — workspaces need real user records to reference.
Proposed Solution
New Database Tables
CREATETABLEworkspaces (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXTNOT NULL,
slug TEXTNOT NULL UNIQUE, -- URL-safe identifier
description TEXTNOT NULL DEFAULT '',
status TEXTNOT NULL DEFAULT 'active', -- active | archived
created_at TIMESTAMPTZNOT NULL DEFAULT now(),
updated_at TIMESTAMPTZNOT NULL DEFAULT now(),
created_by UUID NOT NULLREFERENCES users(id),
settings JSONB NOT NULL DEFAULT '{}'-- workspace-level config overrides
);
CREATETABLEworkspace_members (
workspace_id UUID NOT NULLREFERENCES workspaces(id) ON DELETE CASCADE,
user_id UUID NOT NULLREFERENCES users(id) ON DELETE CASCADE,
role TEXTNOT NULL DEFAULT 'member', -- owner | admin | member | viewer (enforced in Issue #3)
joined_at TIMESTAMPTZNOT NULL DEFAULT now(),
invited_by UUID REFERENCES users(id),
PRIMARY KEY (workspace_id, user_id)
);
CREATEINDEXidx_workspace_members_userON workspace_members(user_id);
Data Model Changes
Every table currently scoped by user_id TEXT gains an optional workspace_id UUID column:
workspace_id = NULL means personal/user-scoped (backward compatible)
workspace_id = <uuid> means workspace-scoped — visible to all members of that workspace
Default Personal Workspace
Every user gets an implicit "personal" workspace (not stored in the workspaces table). Queries with workspace_id IS NULL AND user_id = ? continue to work unchanged. This ensures zero migration disruption for existing data.
New Database Traits
Add a WorkspaceMgmtStore sub-trait (distinct from the existing WorkspaceStore which handles memory documents):
Handler logic becomes: if request has ?workspace=<slug>, verify membership, query by workspace_id. Otherwise, query by user_id (personal scope).
WorkspacePool Refactor
WorkspacePool in src/channels/web/server.rs currently caches by user_id: String. It must support workspace-scoped memory:
Cache key changes from user_id to (user_id, Option<workspace_id>)
Memory documents in a workspace are readable/writable by all members
Personal workspace memory remains user-scoped
Replace workspace_read_scopes
The current workspace_read_scopes: Vec<String> on UserIdentity is replaced by workspace membership. If a user is a viewer or member of workspace X, they can read its memory. The hybrid_search_multi method in WorkspaceStore already takes user_ids: &[String] — extend this to accept workspace IDs too.
New API Endpoints
POST /api/workspaces — create workspace
GET /api/workspaces — list my workspaces
GET /api/workspaces/{slug} — get workspace details
PATCH /api/workspaces/{slug} — update workspace
POST /api/workspaces/{slug}/archive — archive workspace
POST /api/workspaces/{slug}/members — add member
DELETE /api/workspaces/{slug}/members/{user_id} — remove member
PATCH /api/workspaces/{slug}/members/{user_id} — update member role
GET /api/workspaces/{slug}/members — list members
Existing endpoints (/api/jobs, /api/routines, etc.) accept an optional ?workspace=<slug> query param to switch context.
Architecture Notes
Workspace context propagation: The AuthenticatedUser extractor should be augmented or a new WorkspaceContext extractor added that resolves the optional ?workspace= param, verifies membership, and provides (UserIdentity, Option<WorkspaceRecord>) to handlers.
IncomingMessage already carries user_id: String through the agent pipeline. Add an optional workspace_id: Option<String> alongside it rather than replacing user_id. The agent loop, JobContext, and tool execution all need to propagate this.
The Workspace struct in src/workspace/ is the memory/document system — don't confuse it with the new workspace entity. Consider naming the new entity Team or Organization if naming collisions become painful, but workspace matches the user-facing concept better.
Slug uniqueness: enforce at DB level (UNIQUE constraint) and validate format (lowercase alphanumeric + hyphens, 3-48 chars) at API level.
Code Pointers
src/channels/web/server.rs — WorkspacePool, GatewayState — pool needs workspace-aware caching
src/channels/web/auth.rs — UserIdentity.workspace_read_scopes — being replaced
WorkspaceMgmtStore trait implemented for both backends
API endpoints for workspace CRUD and membership management
Membership verification before any workspace-scoped data access
?workspace=<slug> query param on existing /api/jobs, /api/routines, /api/memory/*, /api/settings endpoints
Personal workspace (no workspace_id) continues to work identically to current behavior
workspace_read_scopes field deprecated in favor of membership-based access
WorkspacePool caches by (user_id, workspace_id) tuple
Memory documents in a workspace visible to all workspace members
SSE broadcasts for workspace-scoped events delivered to all workspace members
Creator automatically added as owner role on workspace creation
Archived workspaces return 410 Gone on data access
Unit tests for WorkspaceMgmtStore (both backends)
Integration tests for workspace create → add member → query scoped data → remove member → verify no access
cargo clippy zero warnings, cargo fmt clean
Pitfalls & Landmines
Naming collision: the existing Workspace struct in src/workspace/ is the persistent memory system, not a team/org entity. Be very careful with naming. The new DB entity should use distinct type names like WorkspaceEntity or TeamWorkspace in Rust to avoid confusion.
Dual-backend: libSQL has no UUID type — use TEXT. No arrays — use JSON. See src/db/libsql_migrations.rs patterns.
Nullable workspace_id migration: existing rows have workspace_id = NULL which means "personal". Queries must handle WHERE workspace_id IS NULL AND user_id = ? OR WHERE workspace_id = ? — don't accidentally return personal data when querying a workspace or vice versa.
SSE broadcast fan-out: workspace-scoped events need to go to all online members. SseManager.broadcast_for_user() currently takes a single user_id. You'll need a broadcast_for_workspace() that resolves members and broadcasts to each. Be careful about N+1 DB queries — cache workspace membership.
workspace_read_scopes backward compatibility: the env-var GATEWAY_USER_TOKENS path still supports workspace_read_scopes. Keep it working during the deprecation period, but map scopes to workspace memberships at startup if both systems are active.
Don't change the existing WorkspaceStore trait in src/db/mod.rs — it handles memory documents and has methods like hybrid_search_multi. The new workspace management is a separate trait.
Problem / Motivation
Workspaces are currently implicit — each
user_idstring gets its own siloed data. TheWorkspacePoolinserver.rscaches per-userWorkspaceinstances, but there's no shared-workspace concept beyondworkspace_read_scopes: Vec<String>onUserIdentity, which is a flat string list with no DB backing, no membership semantics, and no write sharing.This means:
workspace_read_scopesis set at token creation, not manageable at runtimeuser_idstring — there's no way to assign a job or routine to a team/project rather than an individualDepends on Issue #1 (DB-backed user management) — workspaces need real user records to reference.
Proposed Solution
New Database Tables
Data Model Changes
Every table currently scoped by
user_id TEXTgains an optionalworkspace_id UUIDcolumn:workspace_id = NULLmeans personal/user-scoped (backward compatible)workspace_id = <uuid>means workspace-scoped — visible to all members of that workspaceDefault Personal Workspace
Every user gets an implicit "personal" workspace (not stored in the
workspacestable). Queries withworkspace_id IS NULL AND user_id = ?continue to work unchanged. This ensures zero migration disruption for existing data.New Database Traits
Add a
WorkspaceMgmtStoresub-trait (distinct from the existingWorkspaceStorewhich handles memory documents):Query Scoping Changes
Existing
_for_usermethods inJobStore,SandboxStore,SettingsStore,RoutineStoreneed parallel_for_workspacevariants:Handler logic becomes: if request has
?workspace=<slug>, verify membership, query byworkspace_id. Otherwise, query byuser_id(personal scope).WorkspacePool Refactor
WorkspacePoolinsrc/channels/web/server.rscurrently caches byuser_id: String. It must support workspace-scoped memory:user_idto(user_id, Option<workspace_id>)Replace
workspace_read_scopesThe current
workspace_read_scopes: Vec<String>onUserIdentityis replaced by workspace membership. If a user is aviewerormemberof workspace X, they can read its memory. Thehybrid_search_multimethod inWorkspaceStorealready takesuser_ids: &[String]— extend this to accept workspace IDs too.New API Endpoints
Existing endpoints (
/api/jobs,/api/routines, etc.) accept an optional?workspace=<slug>query param to switch context.Architecture Notes
AuthenticatedUserextractor should be augmented or a newWorkspaceContextextractor added that resolves the optional?workspace=param, verifies membership, and provides(UserIdentity, Option<WorkspaceRecord>)to handlers.IncomingMessagealready carriesuser_id: Stringthrough the agent pipeline. Add an optionalworkspace_id: Option<String>alongside it rather than replacinguser_id. The agent loop,JobContext, and tool execution all need to propagate this.Workspacestruct insrc/workspace/is the memory/document system — don't confuse it with the new workspace entity. Consider naming the new entityTeamorOrganizationif naming collisions become painful, butworkspacematches the user-facing concept better.UNIQUEconstraint) and validate format (lowercase alphanumeric + hyphens, 3-48 chars) at API level.Code Pointers
src/channels/web/server.rs—WorkspacePool,GatewayState— pool needs workspace-aware cachingsrc/channels/web/auth.rs—UserIdentity.workspace_read_scopes— being replacedsrc/db/mod.rs—Databasesupertrait,JobStore,SandboxStore,SettingsStore— addWorkspaceMgmtStoreand_for_workspacequery variantssrc/workspace/— memory systemWorkspacestruct — needs workspace_id-aware document scopingsrc/agent/—JobContext— needs optionalworkspace_idfieldsrc/channels/channel.rs—IncomingMessage— needs optionalworkspace_idsrc/tools/builtin/memory.rs—WorkspaceResolver/CachedWorkspaceResolver— cache key must include workspace contextsrc/channels/web/handlers/jobs.rs— example of handler needing?workspace=supportAcceptance Criteria
workspacesandworkspace_memberstables in both PostgreSQL and libSQLworkspace_idcolumn added toconversations,agent_jobs,memory_documents,routines,settings(nullable, backward compatible)WorkspaceMgmtStoretrait implemented for both backends?workspace=<slug>query param on existing/api/jobs,/api/routines,/api/memory/*,/api/settingsendpointsworkspace_read_scopesfield deprecated in favor of membership-based accessWorkspacePoolcaches by(user_id, workspace_id)tupleownerrole on workspace creationWorkspaceMgmtStore(both backends)cargo clippyzero warnings,cargo fmtcleanPitfalls & Landmines
Workspacestruct insrc/workspace/is the persistent memory system, not a team/org entity. Be very careful with naming. The new DB entity should use distinct type names likeWorkspaceEntityorTeamWorkspacein Rust to avoid confusion.UUIDtype — useTEXT. No arrays — use JSON. Seesrc/db/libsql_migrations.rspatterns.workspace_idmigration: existing rows haveworkspace_id = NULLwhich means "personal". Queries must handleWHERE workspace_id IS NULL AND user_id = ?ORWHERE workspace_id = ?— don't accidentally return personal data when querying a workspace or vice versa.SseManager.broadcast_for_user()currently takes a singleuser_id. You'll need abroadcast_for_workspace()that resolves members and broadcasts to each. Be careful about N+1 DB queries — cache workspace membership.workspace_read_scopesbackward compatibility: the env-varGATEWAY_USER_TOKENSpath still supportsworkspace_read_scopes. Keep it working during the deprecation period, but map scopes to workspace memberships at startup if both systems are active.WorkspaceStoretrait insrc/db/mod.rs— it handles memory documents and has methods likehybrid_search_multi. The new workspace management is a separate trait.Non-Goals
workspace_members.rolebut not enforced yet. That's Issue Onboarding: show Telegram in channel selection and auto-install bundled channel #3.Part 2 of 4 — depends on #1605 (User Management). See also: RBAC (#3), Admin Panel (#4)