docs/plans/saas/00.challenges-and-roadmap.md
Refactor Bytebase from per-user isolation (one container per workspace) to a true SaaS model (single container, shared endpoint at console.bytebase.com).
Add a workspace column to root-level tables. Project-child tables reuse their existing project FK for workspace scoping -- the project table has workspace, so any query that filters by project is implicitly workspace-scoped.
Separate different workspaces so users only see their own data.
Single connection pool; workspace resolved from JWT, applied as query filter (WHERE workspace = $1 on root tables, WHERE project = $1 on project children).
Runners poll for pending tasks across ALL workspaces with a single query. Each task carries workspace for context when processing.
Current API uses flat resource names with no workspace prefix:
projects/{project}, instances/{instance}, environments/{environment}projects/{project}/issues/{issue}, instances/{instance}/databases/{database}Two options:
Option A: Keep flat names, resolve workspace from JWT (recommended)
The middleware resolves workspace from the JWT and injects it as a query parameter. Resource IDs like projects/default are unique per-workspace because project has UNIQUE(workspace, resource_id). Project-child queries use the project FK which is already workspace-scoped.
workspace in unique indexes on root tablesOption B: Explicit workspace prefix (workspaces/{workspace}/projects/{project})
AIP-compliant but requires changing every endpoint, proto definition, resource name parser, frontend API call, and external integration (Terraform provider, API clients, webhooks).
Recommendation: Option A. Workspace scoping is enforced at the DB level (root tables via workspace, child tables via project FK), making URL-level scoping redundant.
Current model: single workspace per installation. principal table holds END_USER, SERVICE_ACCOUNT, and WORKLOAD_IDENTITY in one table.
SaaS model requires:
user_id and workspaceSolution: Split the principal table into three separate tables:
principal -- keeps only END_USER rows. This IS the global account/identity table. No workspace needed (a user can belong to multiple workspaces). No data migration required -- just remove SERVICE_ACCOUNT/WORKLOAD_IDENTITY rows.service_account -- new table for SERVICE_ACCOUNT rows. Workspace-scoped via project FK (project-level) or workspace (workspace-level).workload_identity -- new table for WORKLOAD_IDENTITY rows. Same scoping as service_account.The store layer already treats these as separate entities (principal.go, principal_service_account.go, principal_workload_identity.go), all querying the same principal table with WHERE type = ?. The split makes the schema match the existing code structure.
Workspace membership is tracked by the policy table's IAM policy (resource_type = 'WORKSPACE'), so no separate membership table is needed.
RLS policies are per-table: each table must have its own workspace column for the policy USING (workspace = current_setting('app.workspace')) to evaluate. This means:
workspace columnproject FK for workspace scoping on child tablesRow-level isolation with project-FK scoping requires workspace on only 12 root tables, while child tables use their existing FK relationships.
workspace (12 tables)These are top-level entities with no parent FK that provides workspace scoping. They get a workspace column directly.
| Table | Notes |
|---|---|
project | Top-level resource, anchor for project-child scoping |
instance | Top-level resource |
service_account | New table (split from principal), workspace-scoped |
workload_identity | New table (split from principal), workspace-scoped |
setting | Workspace configuration |
policy | Workspace/env/project policies |
role | Custom roles |
idp | Identity providers |
review_config | Review configurations |
user_group | Groups |
export_archive | Data exports |
audit_log | Audit trail |
project FK (9 tables)These already have a project FK referencing project(resource_id). Since project has workspace, queries on these tables filter by WHERE project = $1 where the project is already validated as belonging to the current workspace. No workspace column needed.
| Table | FK |
|---|---|
plan | project text NOT NULL REFERENCES project(resource_id) |
issue | project text NOT NULL REFERENCES project(resource_id) |
db | project text NOT NULL REFERENCES project(resource_id) |
project_webhook | project text NOT NULL REFERENCES project(resource_id) |
worksheet | project text NOT NULL REFERENCES project(resource_id) |
db_group | project text NOT NULL REFERENCES project(resource_id) |
release | project text NOT NULL REFERENCES project(resource_id) |
access_grant | project text NOT NULL REFERENCES project(resource_id) |
query_history | project_id text NOT NULL (resource id, no FK constraint) |
These are children of Tier 2 tables. They have no project FK and no workspace. Workspace scoping is inherited through the FK chain -- they are always accessed via their parent, which is already workspace-scoped.
Plan/pipeline grandchildren (5):
| Table | FK chain to workspace |
|---|---|
plan_check_run | -> plan(id) -> plan.project -> project.workspace |
plan_webhook_delivery | -> plan(id) -> plan.project -> project.workspace |
task | -> plan(id) -> plan.project -> project.workspace |
task_run | -> task(id) -> plan(id) -> ... |
task_run_log | -> task_run(id) -> task(id) -> ... |
Issue grandchildren (1):
| Table | FK chain to workspace |
|---|---|
issue_comment | -> issue(id) -> issue.project -> project.workspace |
DB grandchildren (4):
| Table | FK chain to workspace |
|---|---|
db_schema | -> db (via instance+db_name) -> db.project -> project.workspace |
revision | -> db (via instance+db_name) -> db.project -> project.workspace |
sync_history | -> db (via instance+db_name) -> db.project -> project.workspace |
changelog | -> db (via instance+db_name) -> db.project -> project.workspace |
Worksheet grandchildren (1):
| Table | FK chain to workspace |
|---|---|
worksheet_organizer | -> worksheet(id) -> worksheet.project -> project.workspace |
workspace (5 tables)| Table | Reason |
|---|---|
sheet_blob | Content-addressed by SHA256, shared/deduped across workspaces |
replica_heartbeat | Infrastructure, not workspace-scoped |
instance_change_history | Bytebase internal migration version tracker |
workspace | It IS the workspace |
principal | Global identity layer (END_USER only, cross-workspace) |
workspace)| Table | Reason |
|---|---|
web_refresh_token | Tied to principal, not workspace |
oauth2_client | Global OAuth2 app registration |
oauth2_authorization_code | Login flow |
oauth2_refresh_token | Login flow |
| Tier | Tables | workspace column | Workspace scoping mechanism |
|---|---|---|---|
| Root | 12 | Yes (10 existing + 2 new) | Direct WHERE workspace = $1 |
| Project children | 9 | No | WHERE project = $1 (project already validated) |
| Grandchildren | 11 | No | Parent FK chain (parent validated by caller) |
| Global | 5 | No | Not workspace-scoped |
| Auth | 4 | No | Tied to principal (identity) layer |
| Total | 41 | 12 have column |
┌─────────────────────────────────────────────────────────────┐
│ Request Flow │
│ │
│ Client → Auth Interceptor → ACL Interceptor → Service │
│ │ │ │ │
│ Set workspace Validate Use │
│ in context workspace workspace │
│ from JWT ownership for queries │
│ │
├─────────────────────────────────────────────────────────────┤
│ Store Layer │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Root Tables │ │ Child Tables │ │ Global Tables │ │
│ │ workspace FK │ │ project/inst │ │ No workspace │ │
│ │ (required) │ │ FK (indirect)│ │ (principal, │ │
│ │ │ │ │ │ sheet_blob, etc) │ │
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ Background Runners │
│ │
│ Cross-workspace polling → Resolve workspace from entity │
│ → Use workspace for settings/policies │
└─────────────────────────────────────────────────────────────┘
| Step | File | Description |
|---|---|---|
| 01 | 01.prepare-for-saas.md | Preparatory refactors that can land independently: split principal table, fix workspace IAM policy resource field, migrate serial to bigserial |
| 02 | 02.database-migration.md | Create workspace table, add workspace to root tables, update unique indexes |
| 03 | 03.application-layer.md | Middleware, store/service changes to enforce workspace scoping on API requests |
| 04 | 04.runners-and-auth.md | Background runner workspace resolution, auth flow changes (login, workspace picker, JWT) |