Workspace CRUD backed by SQLite (ACL-001).
Each workspace is an isolated tenant. All entity tables (projects, tests,
runs, activities) carry a workspaceId foreign key so queries can be
scoped to the authenticated user's workspace.
Default workspace
On first startup after migration 004, ensureDefaultWorkspaces creates
a "Default" workspace for every existing user and backfills workspaceId
on all orphaned entity rows. This makes the migration non-breaking for
existing single-user deployments.
Methods
(static) addMember(workspaceId, userId, roleopt) → {Object}
Add a user to a workspace with a given role.
Parameters:
| Name | Type | Attributes | Default | Description |
|---|---|---|---|---|
workspaceId |
string | |||
userId |
string | |||
role |
string |
<optional> |
'viewer' | — 'admin' | 'qa_lead' | 'viewer' |
Returns:
The membership row.
- Type
- Object
(static) create(opts) → {Object}
Create a workspace and add the creator as admin.
Parameters:
| Name | Type | Description | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
opts |
Object |
Properties
|
Returns:
The created workspace.
- Type
- Object
(static) ensureDefaultWorkspaces()
Ensure every existing user has at least one workspace.
Called once on startup after migration 004. For each user without a workspace membership, creates a personal workspace.
Orphaned entity backfill
Entities (projects, tests, runs) have no userId column, so there is no
reliable way to attribute them to individual users. The backfill strategy
depends on how many orphan users exist:
- Single orphan user: All orphaned entities are assigned to that user's workspace. This is the common case (single-user deployment upgrading).
- Multiple orphan users: A shared "Default" workspace is created and all orphaned entities are assigned there. All orphan users are added as members. The first orphan user is the admin; the rest are viewers. This prevents the first-user-claims-everything bug and preserves data visibility for all existing users.
Activities (which have userId) are always attributed to the correct
user's workspace when possible.
This is idempotent — calling it multiple times is safe.
(static) getById(id) → {Object|undefined}
Get a workspace by ID.
Parameters:
| Name | Type | Description |
|---|---|---|
id |
string |
Returns:
- Type
- Object | undefined
(static) getBySlug(slug) → {Object|undefined}
Get a workspace by slug.
Parameters:
| Name | Type | Description |
|---|---|---|
slug |
string |
Returns:
- Type
- Object | undefined
(static) getByUserId(userId) → {Array.<Object>}
Get all workspaces for a user (via workspace_members). Results are sorted with owned workspaces first, then by creation date.
Parameters:
| Name | Type | Description |
|---|---|---|
userId |
string |
Returns:
Workspace rows augmented with role from the membership.
- Type
- Array.<Object>
(static) getMembers(workspaceId) → {Array.<Object>}
Get all members of a workspace.
Parameters:
| Name | Type | Description |
|---|---|---|
workspaceId |
string |
Returns:
- Type
- Array.<Object>
(static) getMembership(workspaceId, userId) → {Object|undefined}
Get a user's membership in a specific workspace.
Parameters:
| Name | Type | Description |
|---|---|---|
workspaceId |
string | |
userId |
string |
Returns:
— { id, workspaceId, userId, role, joinedAt }
- Type
- Object | undefined
(static) getOwnedWorkspaceIds(userId) → {Array.<string>}
Get IDs of all workspaces owned by a user. Used by account deletion to collect owned project IDs before cascade delete.
Parameters:
| Name | Type | Description |
|---|---|---|
userId |
string |
Returns:
- Type
- Array.<string>
(static) removeMember(workspaceId, userId) → {boolean}
Remove a member from a workspace.
Parameters:
| Name | Type | Description |
|---|---|---|
workspaceId |
string | |
userId |
string |
Returns:
Whether the membership was found and removed.
- Type
- boolean
(static) update(id, fields)
Update workspace fields.
Parameters:
| Name | Type | Description |
|---|---|---|
id |
string | |
fields |
Object | — { name?, slug? } |
(static) updateMemberRole(workspaceId, userId, role) → {boolean}
Update a member's role.
Parameters:
| Name | Type | Description |
|---|---|---|
workspaceId |
string | |
userId |
string | |
role |
string | — 'admin' | 'qa_lead' | 'viewer' |
Returns:
Whether the membership was found and updated.
- Type
- boolean
(inner) slugify(name) → {string}
Generate a URL-friendly slug from a name.
Parameters:
| Name | Type | Description |
|---|---|---|
name |
string |
Returns:
- Type
- string