Members

Who's in a workspace, and what they can do.

A member is a Supabase user with a row in workspace_members joining them to a workspace with a role. One user can be a member of many workspaces; each membership has its own role.

Roles

There are four roles, ranked low → high. Each row grants the permissions of every row above it.

RoleSessionsMembersAPI keysAuditWorkspace settings
viewerlist / getlist (self)list (own)readread
member+ create / release / keepalive / save-profilelist (all)list / create / revoke (own)readread
admin+ force-stop+ invite / role-change / remove+ manage all keysreadread
ownerallallallread+ name / delete

Underlying scopes (auth/rbac.py): viewer → sessions:read; member → +sessions:write; admin → +admin; owner → +workspace:owner.

A workspace must have at least one owner. The last owner cannot be demoted; the API returns 409 Conflict. System-admin Ventus operators (is_system_admin=true Supabase users) can act on any workspace; every such action is tagged in the audit log with actor_is_system_admin=true.

Invite a member

Owners and admins invite by email from Members → Invite, or POST /v1/dashboard/ws/<slug>/invitations:

curl -X POST https://api.sessions.ventus.ai/api/v1/dashboard/ws/$WS_SLUG/invitations \
  -H "Authorization: Bearer $DASHBOARD_SESSION_JWT" \
  -d '{"email": "alice@example.com", "role": "member"}'

The invitee receives an email with a one-time accept link (https://dashboard.sessions.ventus.ai/auth/callback?token=vsd_inv_…). On click:

  1. The token is exchanged for membership in the workspace.
  2. If the email isn't yet a Supabase user, a magic-link sign-in flow

bootstraps them first.

  1. They land on /ws/<slug>/sessions already-signed-in.

Invitations expire after 7 days. The owner can revoke an unaccepted invitation from the same screen.

Change a role

Owner-only. From Members → ⋯ → Change role or PATCH /v1/dashboard/ws/<slug>/members/<user_id>:

curl -X PATCH https://api.sessions.ventus.ai/api/v1/dashboard/ws/$WS_SLUG/members/$USER_ID \
  -H "Authorization: Bearer $DASHBOARD_SESSION_JWT" \
  -d '{"role": "owner"}'

Demoting the last owner returns 409 Conflict. Removing yourself works if there's another owner.

Remove a member

Owner-only. DELETE /v1/dashboard/ws/<slug>/members/<user_id>. The member loses access immediately; sessions they started keep running and are still attributed to them in the audit log.

Audit-log attribution

The admin_audit_log table records actions performed via the dashboard (human, magic-link-authenticated). Every entry has actor_user_id (NOT NULL), the user's email at the time (actor_email), and an actor_is_system_admin flag.

API-key-driven actions (sessions create/release/keepalive, profiles, etc.) go to the session events / Redis bus, not to admin_audit_log — see the Audit log page for the split.

A removed member's past actions stay attributed to them by actor_user_id — we never re-attribute history. The Supabase user ID is durable across deletion.