Operator Console
The Operator Console is the SaaS-platform-provider admin surface — distinct from every tenant-facing surface in Vetty. It's where the Anthropic-equivalent staff who run the multi-tenant Vetty SaaS itself onboard new clinics, monitor platform health, and support the users across every tenant.
Two panels share the same authentication and user model but have different navigation groupings:
| Panel | Mounted at | Focus |
|---|---|---|
| Operator | /operator | Day-to-day tenant lifecycle, cross-tenant analytics, WhatsApp templates, module toggles. |
| SystemAdmin | /systemadmin | Deep troubleshooting: raw resources, subscription invoices, audit log, SMS pool, service settings. |
Both authenticate against the system_admins table (not users) via
a dedicated systemadmin guard, so tenant users can never reach
/operator or /systemadmin and vice versa.
Authentication
Login flow:
- Operator hits
/operator/login(Filament page — served through the backend API host, not the admin SPA). - Filament validates credentials against the
system_adminstable. - On successful login, a Laravel session cookie is set (scoped to the admin host — no cross-domain leak).
- Every subsequent request against
/operator/*runsSystemAdmin::canAccessPanel(Panel $panel). If it returns false, Filament serves 403.
The canAccessPanel gate enforces two things:
is_activemust be true. Suspending an operator (via the same Filament resource that lists them) setsis_active=false; on the next request the panel returns 403 even if their session cookie is still valid. No token invalidation dance required.- Panel id must be
operatororsystemadmin. Both panels share the same user model, so an operator can hop between the two — the separation is a UX distinction, not a security boundary.
last_login_at and last_login_ip get written by an Illuminate\Auth\Events\Login
listener so an on-call operator can see recent access at a glance in
the SystemAdmin panel's SystemAdmin resource.
Bootstrapping the first operator
Fresh production deploys have an empty system_admins table by
default. deploy.sh handles this automatically:
- Runs
SystemAdminSeeder, which readsOPERATOR_INITIAL_EMAILandOPERATOR_INITIAL_PASSWORDfromdocker/.env. If set, one operator is created with those credentials +is_active=true. - If those env vars weren't set, deploy.sh emits a warning telling
the operator to run
php artisan operator:createinteractively.
operator:create prompts for email / name / phone / password (min 12
chars), hides the password on entry, and creates or updates the row.
The --rotate-password flag forces a fresh password on an existing
account — useful for on-call rotation.
Features
Cross-tenant analytics dashboard
The Operator panel's landing page shows three widgets:
- OperatorOverview stats — active tenants, trials ending in 7 days, suspended tenants, last-30-day revenue (approximate MRR), WhatsApp templates pending Meta approval.
- RevenueTrendChart — 6-month line chart of paid
subscription_invoicesbucketed bypaid_atmonth. Portable SQL works on MySQL, Postgres, and SQLite. - TopTenantsByActivity — top 10 tenants by appointment count this calendar week, with plan + renewal-date badges. Useful for spotting disengagement before churn.
All widgets poll on background timers (60 s for stats, 5 min for chart/table). Numbers refresh without the operator hitting reload.
Cross-tenant user lookup
Under the Operator panel's "Support" nav group, the User lookup
page searches the entire users table by email/name/phone. For each
match it shows:
- Tenant memberships (staff role names) or "Owner:
<tenant>" for pet owners without a staff role. - Pet count, open ticket count, last login, signup date.
Filters: owners-only, staff-only, never-logged-in.
The page is read-only by design. Modifications happen through the tenant SPA so the standard audit trail captures the change author correctly.
Tenant lifecycle actions
The shared TenantResource's row + bulk actions cover:
- Suspend — flips
tenants.statustosuspended. Writes anaudit_logsentry. On the tenant's next request the subscription suspension middleware locks their SPA + API. - Activate — reverse of suspend.
- Impersonate — mints a 60-minute Sanctum personal-access token
bound to the tenant's primary
tenant_adminuser with animpersonation:*ability. Token + audit entry go into the notification; operator can use it via curl/Postman for support work. Falls back to a friendly "no eligible user" notification if the tenant has no active tenant_admin yet. - Bulk suspend / activate — mass actions.
Every action writes to audit_logs with the SystemAdmin's identity
in metadata (since actor_user_id is a User FK and operators
aren't Users).
WhatsApp template management
The Operator panel's WhatsApp Templates resource wraps CRUD + Meta approval status. Templates flow into the reminder pipeline when their status becomes APPROVED.
Per-tenant module toggles
Custom Filament page under the Operator panel. Pick a tenant → toggle
which of the 19 optional modules (Phase 1 clinical core, Phase 2
lab/surgery/boarding/telemedicine/AI, Phase 3 marketplace/loyalty)
are enabled. Persists to tenants.enabled_modules JSON.
Post-deploy smoke test
docker/smoke-operator.sh runs automatically at the end of every
deploy.sh and verifies:
system_adminstable has at least one row./operator/loginand/systemadmin/loginreturn 200.- No
notification.readmiddleware is still present on any route (a class of regression that broke the platform once). - Route cache is fresh (protects against a stale cache file masking the fix in point 3).
- Filament resources autoload cleanly (
package:discoversucceeds). notification.sendpermission is present in the catalog (proxy for "RolePermissionSeeder ran").
Non-fatal on failure — the smoke reports issues but doesn't roll back the deploy.
Security posture
- Isolated user provider. Tenant users cannot authenticate against either operator panel and vice versa; the two guards + tables are physically separate.
- Session-based auth. No JWTs, no API tokens for the panels themselves. Password reset flows through the standard Filament reset-link mechanism (no separate SMS-OTP flow yet).
- Suspension is instant.
canAccessPanelre-checksis_activeon every request. - Traefik + strict CSP in front. The
vetty-headers@filemiddleware applies HSTS + strict-Referrer + a CSP that allows Filament's necessary'unsafe-inline'+'unsafe-eval'(Alpine.js requirement) but locks everything else. - Optional IP allowlist. For extra-cautious deploys, a Traefik
ipAllowListmiddleware can be dropped onto the/operator+/systemadminrouters to restrict to office/VPN IPs. Off by default because on-call operators typically work from home networks with rotating IPs.