Skip to main content

Caregivers & Pet Sharing

Vetty supports two complementary ways for a primary owner to extend access to their pets:

MechanismAudienceChannelLifecycle
Pet share codeClinics (vetdoctor app)Short alphanumeric code or QRSingle-use or limited-redemption; can be revoked or expire on a date
Caregiver inviteFamily / friend co-ownersEmail-based invite tokenLong-lived; explicit access level (view / full); revocable

The two surfaces are independent — a pet can have any number of share codes and caregivers concurrently.

Pet share codes

When to use

When the owner walks into a clinic, hands the vet their phone, and wants the clinic to be able to add chart entries to this pet without a long onboarding form.

Owner flow (vetty)

  1. Owner navigates to Pets → [pet] → Share.
  2. App calls POST /api/v1/owner/me/shares with {pet_id, expires_at?, max_redemptions?}.
  3. Backend mints a code via PetShare::generateCode() (8-character human-friendly).
  4. App displays the code + a QR encoding the same.
  5. Owner can revoke at any time (POST /api/v1/owner/me/shares/{share}/revoke).

Clinic flow (vetdoctor)

  1. Clinic staff opens the QR scanner.
  2. App calls POST /api/v1/shares/redeem with {code, tenant_id, user_id}.
  3. Backend validates: not revoked, not expired, redemption count under cap. Increments redeemed_count.
  4. Pet is now linked to the clinic's patient list under the active tenant.

Public landing

Unauthenticated GET /api/v1/shares/{code} returns a minimal preview (pet name, owner-supplied notes) so the QR-scan landing page can render before the staff member installs the app.

Caregiver invites

When to use

A spouse who also looks after the dog, a dog walker who needs read-only access to the vaccination record, a family member who occasionally takes the cat to the vet.

Owner flow (vetty)

  1. Owner opens Pets → [pet] → Caregivers → Invite.
  2. App calls POST /api/v1/owner/me/pets/{pet}/caregivers with {invited_email, invited_name?, access_level: "view"|"full"}.
  3. Backend creates a pet_caregivers row with a fresh invite_token (48-char URL-safe).
  4. SendCaregiverInviteEmailJob is enqueued. The job:
    • Loads the row.
    • Renders the caregiver_invite template via TemplateRenderer::renderBySlug.
    • Builds the OutboundMessage (recipient = invited_email, accept-URL with _raw suffix to bypass HTML escaping).
    • Sends through the configured driver.
  5. Owner sees the row appear with status pending.

Resend / revoke

  • Resend: POST /api/v1/owner/me/pets/{pet}/caregivers/{caregiver}/resend — re-fires the same job without minting a new token.
  • Revoke: POST /api/v1/owner/me/pets/{pet}/caregivers/{caregiver}/revoke — sets revoked_at. The token short-circuits inside the job and returns 410 if redeemed afterwards.

Invitee flow

  1. Caregiver receives the email, clicks the accept_url (deep links into vetty if installed, otherwise lands on the web acceptance page).
  2. After login, app calls POST /api/v1/caregivers/accept with {token}.
  3. Backend matches the token, sets caregiver_user_id to the authenticated user, sets accepted_at, and nulls out the invite_token (one-shot).
  4. Caregiver now sees the pet in their Shared with me tab (GET /api/v1/owner/me/caregivers/shared-with-me).

Access levels

  • view — the caregiver can read the pet's profile, vaccinations, documents, health journal, and timeline. They cannot write anything.
  • full — adds write access on the health journal, document upload, and order requests.

Policy enforcement happens in the controllers that load the pet — they look up pet_caregivers for the authenticated user and gate writes accordingly.

Architectural notes

  • Tokens are stored in plaintext today (Str::random(48)). The audit recommends hashing with SHA-256 on store and comparing hashes on accept (AUDIT.md §7).
  • The caregiver_invite template is platform-default; tenants don't override it because caregivers belong to the owner's account, not a tenant.
  • The job uses $this->onQueue(...) in the constructor (not a queue() method) — see Notifications for the full driver story.