RBAC & Permissions Model¶
Starform uses a two-tier permission model with an optional environment protection flag. The design balances capability (beating Railway and Render on scoped access) with simplicity (avoiding enterprise-IAM complexity). The entire system is expressed in three database tables plus permission middleware in the Starbase API binary.
§15.1 Design Goals¶
- Beat Railway on flexibility: environment-level write protection (Railway has project-scoped members but no environment-level protection)
- Beat Render on cost: no per-seat pricing; unlimited team members on all paid plans
- Support agencies and small teams: project-scoped access so a freelancer can see one project without seeing others in the workspace
- Support finance roles without granting engineering access (Billing role)
- Keep the model small enough to ship at MVP without enterprise-IAM complexity
§15.2 Hierarchy¶
flowchart TB
classDef built fill:#3434DC22,stroke:#3434DC,color:#5B5EE8;
classDef third fill:transparent,stroke:#808080,color:#808080;
WS["Workspace"]
WSM["Workspace Members<br/>(4 roles)"]
BILL["Billing"]
GS["Global Settings"]
PROJ["Projects"]
PM["Project Members<br/>(3 roles, scoped)"]
PS["Project Settings"]
ENV["Environments<br/>(is_protected flag)"]
SVC["Services"]
DB["Databases"]
BUC["Buckets"]
VG["Variable Groups"]
WS --> WSM
WS --> BILL
WS --> GS
WS --> PROJ
PROJ --> PM
PROJ --> PS
PROJ --> ENV
ENV --> SVC
ENV --> DB
ENV --> BUC
ENV --> VG
class WS,PROJ,ENV built;
class WSM,BILL,GS,PM,PS,SVC,DB,BUC,VG third;
is_protected flag at the environment tier.§15.3 Workspace Roles¶
| Role | Permissions |
|---|---|
| Owner | Everything including billing, delete workspace, transfer ownership, manage all members |
| Admin | Manage workspace members, create/delete projects, manage workspace settings. No billing access. |
| Billing | View invoices, usage, spend breakdown per project, view payment methods (read-only). No project access. |
| Member | Must be explicitly added to projects. No default project access. No billing visibility. |
Rule: Workspace Owners and Admins have implicit access to all projects in the workspace with
Project Admin role. Workspace Members have no project access unless explicitly added via
project_members.
§15.4 Project Roles¶
| Role | Permissions |
|---|---|
| Admin | Full project control. Manage project members. Can protect/unprotect environments. Can deploy to all environments including protected ones. Can delete services, databases, and the project itself. |
| Developer | Can deploy to non-protected environments. Can edit env vars in non-protected environments. Can view logs, metrics, status across all environments. Cannot deploy or modify anything in protected environments. Cannot manage project members. |
| Viewer | Read-only across all environments. Can see services, status, logs, metrics. Cannot deploy, edit, or modify anything. Cannot see secret values in any environment. |
§15.5 Environment Protection¶
Any environment within a project can be marked as protected by a Project Admin. The canonical use case
is production — marked protected to prevent accidental changes by Developers.
When an environment is protected:
- ❌ Developers cannot deploy to it
- ❌ Developers cannot edit env vars or secrets in that environment
- ❌ Developers cannot delete resources in that environment
- ✅ Developers can still view logs, metrics, and non-secret configuration
- ✅ Developers can still view service status and deployment history
Who can deploy to protected environments: Workspace Owners, Workspace Admins, and Project Admins.
Default state: On project creation, production environment is created with is_protected = true.
staging and dev are created unprotected. Project Admins can toggle the flag on any environment.
§15.6 Permission Resolution¶
When a user requests access to a project resource, permissions are resolved in this order:
- Is the user a Workspace Owner? → Full access (Project Admin role, can deploy to protected environments)
- Is the user a Workspace Admin? → Full access (Project Admin role, can deploy to protected environments)
- Is the user a Workspace Billing-only role? → No project access. Can only view billing pages.
- Is the user a Workspace Member with a direct Project Membership? → Use the Project role (Admin / Developer / Viewer). Apply environment protection rules.
- Is the user a Workspace Member without Project Membership? → No access to this project.
- Is the user not a Workspace Member? → No access.
This resolution happens once per request in permission middleware. The result is attached to the
request context as a PermissionContext object:
type PermissionContext struct {
UserID string
WorkspaceID string
WorkspaceRole WorkspaceRole // Owner | Admin | Billing | Member | None
ProjectID string // Optional; set when request is project-scoped
ProjectRole ProjectRole // Admin | Developer | Viewer | None
EnvironmentID string // Optional; set when request is environment-scoped
EnvProtected bool // True if the targeted environment is protected
}
Handlers check against this context rather than querying the database on every permission check.
§15.7 Database Schema (RBAC tables)¶
users (
id UUID PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
name TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
workspaces (
id UUID PRIMARY KEY,
name TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
billing_email TEXT,
plan TEXT DEFAULT 'hobby',
created_at TIMESTAMPTZ DEFAULT NOW()
);
workspace_members (
workspace_id UUID REFERENCES workspaces(id) ON DELETE CASCADE,
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
role TEXT CHECK (role IN ('owner', 'admin', 'billing', 'member')),
created_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (workspace_id, user_id)
);
projects (
id UUID PRIMARY KEY,
workspace_id UUID REFERENCES workspaces(id) ON DELETE CASCADE,
name TEXT NOT NULL,
slug TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE (workspace_id, slug)
);
project_members (
project_id UUID REFERENCES projects(id) ON DELETE CASCADE,
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
role TEXT CHECK (role IN ('admin', 'developer', 'viewer')),
created_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (project_id, user_id)
);
environments (
id UUID PRIMARY KEY,
project_id UUID REFERENCES projects(id) ON DELETE CASCADE,
name TEXT NOT NULL,
is_protected BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE (project_id, name)
);
Key schema decisions:
project_membersonly has rows for non-workspace-admin users with project access. Workspace Owners and Admins have implicit access (enforced in application logic, not stored as rows).- Billing role is at workspace level only; there is no concept of a billing role within a project.
- Environment protection is a single boolean, not a per-user permission matrix. This keeps the model simple while covering 95% of real-world needs.
§15.8 API Enforcement¶
Every API endpoint in the Starbase API binary declares its
required permission level. Middleware resolves the PermissionContext once and enforces the check
before the handler runs.
Permission middleware flow:
- Extract user from session/JWT
- Load workspace membership for the request's target workspace
- If request is project-scoped, load project membership
- If request is environment-scoped, load environment protection status
- Build
PermissionContextand attach to request - Handler calls
ctx.RequireWorkspaceAdmin(),ctx.RequireProjectDeveloper(),ctx.RequireEnvironmentWrite()etc. — helpers that return 403 if the check fails
§15.9 UI Implications¶
Projects view (Workspace level):
- Each project card shows a team avatar stack indicating project members
- Workspace Owners/Admins see all projects; Workspace Members see only projects they're directly added to
- Billing role users see only a Billing page; no Projects view
Mission Control (Project level):
- Breadcrumbs establish hierarchy (Projects / Acme SaaS Platform / production)
- Protected environments show a lock icon in the environment switcher
- Developers attempting to deploy to a protected environment see an inline explanation and a "Request Admin approval" CTA (feature deferred, but UI prepared)
Settings:
- Workspace Settings (profile menu): Team members & roles, Billing, Workspace-wide API keys
- Project Settings (sidebar): Project members, Environment variables, Custom domains, Integrations
§15.10 What to Build for MVP vs. Defer¶
Build now
- Full two-tier schema (
workspace_members+project_memberstables) - Workspace roles: Owner, Admin, Billing, Member
- Project roles: Admin, Developer, Viewer
- Environment protection flag (single boolean)
- Permission middleware
- UI for workspace-level member management
- UI for project-level member management
- UI to toggle environment protection
Defer to v1.1
- Custom roles
- Per-user environment-level overrides ("Jane can deploy to prod but Bob can't")
- Team/group abstractions
- SSO/SAML integration (Enterprise tier)
- Audit logs
- "Request Admin approval" workflow for Developer deploys to protected environments
This foundation supports extension without restructuring: every deferred feature adds new tables or new columns, not new models.
§15.11 Competitive Positioning¶
| Capability | Railway | Render | Starform |
|---|---|---|---|
| Workspace roles | 3 (Admin/Member/Deployer) | 2 (Admin/Member) | 4 (Owner/Admin/Billing/Member) |
| Project-scoped membership | Yes (2 scopes) | No | Yes (3 roles) |
| Environment protection | No | Partial (destructive actions only) | Yes (full write protection) |
| Per-seat pricing | No | Yes ($19/user Pro, $29/user Org) | No |
| Billing-only role | No | Enterprise only | All plans |
This model ships with more capability than Railway and Render on day one, while remaining simpler than Vercel's Enterprise-gated team model.
Cross-references
Environment entity (RFC 1123, is_protected / is_ephemeral) → §24.1 ·
Var Groups inherit this protection model → §38.6 ·
enforced in the Starbase API binary ·
SSO (dashboard login, GitHub + Google) is distinct from the deferred customer auth primitive.
Canonical map: Canonical Sources.