User Roles
Roles in Vetty come from two places:
users.account_type— distinguishes clinic staff from pet owners.tenant_user.role— refines what a staff member can do in a particular clinic.
System admins live in a different table (system_admin) and have their own guard.
Matrix
| Role | Where defined | Can do |
|---|---|---|
| System Admin | system_admin table | Everything in the Filament panel: create tenants, change plans, manage cross-tenant data |
| Clinic Admin | tenant_user.role = 'clinic_admin' | Manage all data inside their clinic — staff, patients, appointments, billing, inventory, reminders |
| Veterinarian | tenant_user.role = 'veterinarian' | See appointment queue, update medical notes, sign off prescriptions; read-only on billing. Can also use the vetdoctor mobile app to see today's appointments and scan owner QR codes to adopt pets into the clinic |
| Clinic Staff / Receptionist | tenant_user.role = 'staff' | Check-in, scheduling, invoicing, payment capture, inventory adjustments. Same vetdoctor app access as vets |
| Pet Owner | users.account_type = 'owner' | See their pets, appointments and invoices via /owner/me/* endpoints — in the React SPA and in the vetty mobile app (which adds vaccination logging, QR share, and nearby-provider search) |
How roles are enforced today
- Route middleware (
ResolveTenantMembership) confirms the user belongs to the tenant. It does not yet readtenant_user.role— so any member has full clinic access in practice. - The Filament panel is walled off by the
systemadminguard; onlySystemAdminrows can log in there. - The
/api/v1/owner/me/*endpoints filter strictly byowner_user_id, so owners can only read their own rows regardless of what the UI hides.
How roles are expected to be enforced next
Recommended evolution — Laravel Policies:
// app/Policies/AppointmentPolicy.php
public function update(User $user, Appointment $appointment): bool
{
$role = $user->tenants()
->where('tenants.id', $appointment->tenant_id)
->first()?->pivot->role;
return in_array($role, ['clinic_admin', 'veterinarian', 'staff']);
}
Then controllers do $this->authorize('update', $appointment); before mutating.
Role assignment
- Clinic Admin — created automatically when
POST /api/v1/tenantsruns. - Additional staff — added via Filament (or a future API endpoint) by inserting rows into
usersandtenant_userwith the desired role. - Pet Owner — self-registers via
POST /auth/owner/registerspecifying the clinic'stenant_id. A pairedClientrow is created.
Multiple clinics per user
A staff user can belong to several clinics (each with its own role). users.current_tenant_id picks the default on login; the SPA's TenantSwitcher lets them hop.
Pet owners typically belong to a single clinic, but the data model doesn't forbid multiple. If an owner adopts a pet from a different clinic, a new users↔clients link at that tenant is created.
On vetdoctor, the tenant selected at login is pinned for the whole session — multi-tenant switching is a web-only feature today. A vet with rounds across several clinics uses the web app to change tenant.
Where each role can sign in
| Role | React SPA | Filament admin | vetty app | vetdoctor app |
|---|---|---|---|---|
| System Admin | — | ✅ | — | — |
| Clinic Admin | ✅ | — | — | ✅ |
| Veterinarian | ✅ | — | — | ✅ |
| Clinic Staff | ✅ | — | — | ✅ |
| Pet Owner | ✅ (owner mode) | — | ✅ | — |