Skip to main content

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:

PanelMounted atFocus
Operator/operatorDay-to-day tenant lifecycle, cross-tenant analytics, WhatsApp templates, module toggles.
SystemAdmin/systemadminDeep 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:

  1. Operator hits /operator/login (Filament page — served through the backend API host, not the admin SPA).
  2. Filament validates credentials against the system_admins table.
  3. On successful login, a Laravel session cookie is set (scoped to the admin host — no cross-domain leak).
  4. Every subsequent request against /operator/* runs SystemAdmin::canAccessPanel(Panel $panel). If it returns false, Filament serves 403.

The canAccessPanel gate enforces two things:

  • is_active must be true. Suspending an operator (via the same Filament resource that lists them) sets is_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 operator or systemadmin. 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:

  1. Runs SystemAdminSeeder, which reads OPERATOR_INITIAL_EMAIL and OPERATOR_INITIAL_PASSWORD from docker/.env. If set, one operator is created with those credentials + is_active=true.
  2. If those env vars weren't set, deploy.sh emits a warning telling the operator to run php artisan operator:create interactively.

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_invoices bucketed by paid_at month. 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.status to suspended. Writes an audit_logs entry. 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_admin user with an impersonation:* 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:

  1. system_admins table has at least one row.
  2. /operator/login and /systemadmin/login return 200.
  3. No notification.read middleware is still present on any route (a class of regression that broke the platform once).
  4. Route cache is fresh (protects against a stale cache file masking the fix in point 3).
  5. Filament resources autoload cleanly (package:discover succeeds).
  6. notification.send permission 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. canAccessPanel re-checks is_active on every request.
  • Traefik + strict CSP in front. The vetty-headers@file middleware 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 ipAllowList middleware can be dropped onto the /operator + /systemadmin routers to restrict to office/VPN IPs. Off by default because on-call operators typically work from home networks with rotating IPs.