Team Collaboration
Manage operators, roles, and permissions in the MVS Telecom Operator Console.
The MVS Telecom Operator Console runs as a single MSP operator Organization. Your team of operators (owner, admins, and techs) all work inside that one org, and every read and write is scoped to it. Collaboration is built on Better Auth's multi-tenancy primitives — Member, Invitation, and OrganizationRole — layered with the platform's RBAC permission model.
The Operator Organization
Rather than many isolated team accounts, MVS operates as one Organization. Customers, ApiKeys, RoutingPolicies, MrrSnapshots, and OpsDigests carry organizationId directly; everything else is org-scoped transitively (most often through the customer that a job, number, or device belongs to). The active org lives on the Better Auth Session (activeOrganizationId), and downstream code resolves it identically whether the caller is an interactive operator or a programmatic API key.
Membership and identity are modeled by Better Auth:
User/Session/Account— identity and login.Organization— the MVS operator org.Member— a user's membership in the org, with a role.Invitation— a pending invite to join the org.OrganizationRole— the role assigned to a member.
There is no separate "team workspace" abstraction. The Console shell (navy rail + frosted topbar + ⌘K palette) is the workspace, and nav-items.ts is the single source for the sidebar and palette.
Inviting Operators
Send Invitations
Invitations are issued through Better Auth's organization API (admins/owners only). A member-level invite gives the new operator the tech-tier permissions described below.
import { auth } from '@kit/auth';
await auth.api.createInvitation({
body: {
email: 'operator@mvs.example',
role: 'member',
},
headers, // request headers carrying the session
});
Invitation Flow
- An owner or admin sends an invitation by email.
- The recipient receives an email with an invitation link.
- The recipient accepts the invitation (handled by the
(invitation)/route group). - A
Memberrow is created and the operator gains access to the org.
Roles and Permissions
Authorization has two layers. Roles are a numeric Better Auth hierarchy (higher = more authority); permissions are the finer-grained provisioning feature-permissions checked by the action middleware. See Security & RBAC for the full model.
Default Roles
Roles are defined in packages/rbac/src/core/defaults.ts:
Owner (level 100)
- Full org control
- Manage billing and the operator's own SaaS subscription
- Invite and remove members
- All provisioning permissions
Admin (level 50)
- Manage members and billing
- All provisioning permissions, including
approve_spendandcompensate
Member / Tech (level 10)
read,create, andexecuteprovisioning- Deliberately cannot
update/delete,approve_spend, orcompensate
Unknown roles resolve to level 0 (no permissions).
The provisioning permission model
RBAC is customized in packages/rbac/src/rbac.config.ts, which adds a single provisioning resource with these capabilities:
| Permission | Meaning in the platform |
|---|---|
read | View customers, jobs, tasks, inventory, billing, config/status |
create | Create customers/intakes, start onboarding, trigger sync |
update | Edit provisioning settings |
delete | Destructive teardown; also gates API-key issuance/revocation |
execute | Run/retry jobs, assign/release numbers & devices, provision extensions |
approve_spend | The spend gate — approve/deny carrier purchases |
compensate | Resolve compensation (rollback/cleanup) tasks |
This separates the operator who runs provisioning from the operator who authorizes money and who resolves failed rollbacks.
Checking Permissions
Portal mutations gate on these permissions via the action middleware (@kit/action-middleware), not ad-hoc helpers. For example, billing and provisioning actions are wrapped with withFeaturePermission:
import { withFeaturePermission } from '@kit/action-middleware';
// gate a write on provisioning:execute
const action = withFeaturePermission({ provisioning: ['execute'] })(/* ... */);
// gate a read
const readAction = withFeaturePermission({ provisioning: ['read'] })(/* ... */);
// the spend gate
const buyAction = withFeaturePermission({ provisioning: ['approve_spend'] })(/* ... */);
The public REST API (/api/v1) enforces the equivalent via per-key scopes on the custom ApiKey model (sha256 hash + prefix, org-scoped).
Member Management
Listing, role changes, and removal go through Better Auth's organization API, scoped to the active org.
import { auth } from '@kit/auth';
// list members of the active org
const members = await auth.api.listMembers({ headers });
// update a member's role
await auth.api.updateMemberRole({
body: { memberId, role: 'admin' },
headers,
});
// remove a member
await auth.api.removeMember({ body: { memberIdOrEmail }, headers });
Organization Settings
Org and account settings live under the /settings screen. The operator's own SaaS plan is the Better Auth/Stripe Subscription model — this is how MVS pays for the Console, and is not how telecom service is billed to customers (that lives in the RatePlan/Invoice billing domain surfaced at /billing).
Audit Trail
The platform has no generic team "activity log" table. Operator and system actions are recorded in the append-only AuditEvent model (FK-less, with costCents, payloadHash, and metadata.organizationId stamped on global ops actions) and surfaced read-only at the /audit screen.
import { writeAuditEvent } from '@kit/provisioning-workflows';
await writeAuditEvent({
action: 'number.assign',
actorType: 'operator',
customerId,
costCents,
metadata: { organizationId },
});
/audit reads these events org-scoped two ways (the customer belongs to the org, or customerId is null and metadata.organizationId matches), filterable by action substring, actor type, and a day window (default 30, max 365, cap 200 rows).
Notifications
There is no notification table in MVS. The topbar bell is derived live from the operations attention feed — loadOperations() — so notifications always reflect real, current state:
- Unread =
attentionJobs+ opencompensationTasks(compensationHumanTasks). - The bell shows a dot only when unread > 0.
- "View all" links to the /operations screen, which renders the unified "needs attention" feed (exception jobs in
failed/compensating/compensation_blocked/awaiting_human, plus open compensation tasks).
This means collaboration awareness comes from the same exceptions-and-health console operators already act on — not a parallel notifications store that can drift.
Best Practices
- Single operator org — keep the team in one Organization; lean on org-scoping for isolation.
- Principle of least privilege — techs (
member) run jobs and read everything; keepapprove_spendandcompensatewith admins/owners. - Use the middleware — gate every mutation with
withFeaturePermission; never hand-roll permission checks. - Audit trail — record money-spending and destructive actions via
AuditEvent; review them on/audit. - Easy onboarding — invite operators by email through the Better Auth invitation flow.
- Notifications follow reality — the bell is the operations attention feed, so triage from
/operationsrather than expecting a separate inbox.