Skip to main content

Teams & RBAC

There are no individual accounts in the Cloud CRM. Every paying customer is a team labeled after the company — "Smith Roofing," "ABC HVAC," "Mountain Pool & Spa." Users are members of one or more teams, and inside each team their access is governed by role. This model exists because every other model we've watched in this industry breaks the same way: a tech leaves, the company can't recover their data; an owner is hospitalized, the bookkeeper can't pay vendors; a dispatcher quits, customer notes are stuck in their personal account. Teams-first eliminates that whole class of failure.

What a team is

The team owns
• The dedicated per-tenant D1 database
• The R2 namespace for files and photos
• The billing relationship with Stripe
• The phone number(s) for the AI receptionist
• The company-branded customer portal
• The custom field definitions and views
The team has
• One Owner (sometimes two for partnerships)
• Any number of Admins
• Any number of role-typed members
• Optional viewer-only users (accountant, investor)
• A roster of pending invitations
• An audit log of every membership change
Users belong to teams
• A user can be on multiple teams
• Each membership carries its own role
• Switching teams re-issues the active JWT
• User identity (login, MFA) lives in the control plane
• Team-specific data never crosses team boundaries
• Removing a user from a team is one click

The standard roles

Seven roles ship out of the box. Each is a pre-built composition of granular permissions; tenants can stay with the defaults or build custom roles by mixing permissions.

RoleForDefining permissions
OwnerFounder · CEOFull control. Manages billing, transfers ownership, can delete the team. Only Owner can promote another Owner.
AdminOffice manager · operations leadEverything except billing and team deletion. Manages users, roles, field definitions, integrations, automations.
DispatcherScheduler · operations supportAssigns jobs to techs, edits the calendar, views the crew map, sees customer financials. Cannot change pricing or invoice.
EstimatorSales · estimatorCreates leads, drafts and sends estimates, marks deals won/lost. Sees pipeline and own commission data. No access to other reps' deals by default.
BookkeeperAccountant · billing clerkManages invoices, payments, expenses, payroll exports. Cannot edit field-operations data — no schedule changes, no crew assignments.
TechCrew member · field techSees only own jobs. Updates job status, uploads photos, scans receipts, captures customer signature. No financial visibility.
ViewerOutside accountant · investor · spouseRead-only across the team's records. No write permission anywhere. Useful for periodic third-party reviews without account proliferation.

The permission matrix

Roles aren't magic; each one is a saved set of granular permissions. Here's the default matrix. The Custom Role builder lets Owners create new roles by checking and unchecking these exact cells.

CapabilityOwnerAdminDisp.Estim.Book.TechViewer
View own assigned jobs
View all jobs · all customers
Create contacts / leads
Edit job schedule
Assign tech to a job
View pricing / margins
Draft & send estimates
Create & send invoices
Record payments
Scan & categorize expenses
Approve / void expenses
Upload photos to a job
See AI receptionist transcripts
Add or edit field definitions
Build automations / workflows
Manage integrations (Stripe, QuickBooks)
Invite / remove team members
Change billing / payment method
Transfer team ownership
Delete the team

Field-level permissions

Beyond capability gates ("can see invoices yes/no"), individual field definitions can be restricted by role. This is what lets Tech see a job's address and customer name without seeing the quoted price or commission. The field_definitions table carries a visible_to and editable_by array of role IDs.

customer.full_name
visible: All roles
editable: Owner, Admin, Dispatcher
customer.phone
visible: All roles
editable: Owner, Admin, Dispatcher
project.address
visible: All roles
editable: Owner, Admin, Dispatcher
project.assigned_tech
visible: All roles
editable: Owner, Admin, Dispatcher
project.quoted_amount
visible: Owner, Admin, Dispatcher, Estimator, Bookkeeper
editable: Owner, Admin, Estimator
project.actual_cost
visible: Owner, Admin, Bookkeeper
editable: Owner, Admin, Bookkeeper
project.margin
visible: Owner, Admin
editable: derived — read-only
project.commission_paid
visible: Owner, Admin, Bookkeeper
editable: Owner, Admin, Bookkeeper
tech.hourly_rate
visible: Owner, Admin
editable: Owner, Admin
customer.private_notes
visible: Owner, Admin, Dispatcher
editable: Owner, Admin, Dispatcher

Custom roles

Owners and Admins can create roles that don't fit the seven defaults. A few examples we see in the field:

Lead Tech
Based on Tech
+ View all jobs (not just own), + Override schedule for own crew
Sales Manager
Based on Estimator
+ View all reps' pipelines, + Reassign leads, + See team-level conversion stats
Apprentice
Based on Tech
− Upload photos requires Lead Tech approval, − Cannot complete a job (must hand off)
Outside Bookkeeper
Based on Bookkeeper
− Cannot see Owner's personal expense category
Sub-Contractor
Based on Tech
− No access to other companies' jobs, − Can see only own invoiced amounts, not company margin
Investor
Based on Viewer
− Restricted to financial dashboards only, no operational data, time-boxed access window

Onboarding and offboarding

The whole point of team-based access is that adding and removing people is one click. Here's the lifecycle of a team member.

InviteAdmin enters email and chooses role. System emails an invitation link. Until accepted, the row is in pending_invitations on the control plane — no access yet, no JWT issued.
AcceptUser clicks the link, signs in (or signs up if new), and confirms the team join. Row moves from pending_invitations to org_memberships. JWT now contains their role for this team.
ActiveNormal use. Every action is authorized against the role's permissions. Field-level checks happen at the API layer; UI hides anything they shouldn't see anyway.
Role changeAdmin updates membership.role. The user's next request gets a fresh JWT with the new role. No re-login required.
SuspendAdmin marks suspended=true. All active sessions for that user on that team are invalidated within 60 seconds. Data stays put — useful when an employee is on extended leave but might return.
RemoveAdmin deletes the membership row. JWT can no longer authenticate against this team. The user account on the control plane survives — they might be on other teams — but their access to this team's D1 is gone immediately.
Data successionOn removal, records the user owned can be auto-transferred to another team member (e.g., their manager) or to the team itself. The org's data is never orphaned by an employee leaving.

How billing follows the team

Stripe subscriptions are attached to the team, not to a user. The Owner is the default billing contact; an Admin can be authorized as a secondary. If the Owner leaves the company, an existing Admin can be promoted to Owner without changing the Stripe subscription, the data, the integrations, or any user-facing detail. The team continues; the cast of characters around it changes.

The seat math: billing is per active seat per month. Tech seats are typically cheaper than office seats because they consume less compute (no AI runs from the field UI by default). Viewer seats are usually free up to a small cap so outside accountants and investors don't impose a billing tax. The exact pricing lives in the Billing settings doc.

Continue to Views for how the same underlying records get rendered in five different ways depending on the consumer's role and preference.