Documentation
IntroductionArchitectureMonorepo & ToolingRunning Locally
Screen Flow MapUser JourneysPersonas & RolesWeb ScreensClient Portal
Convex OverviewData ModelFunctions ReferenceAuth & SessionsRoles & PermissionsJobs & Providers
Web Design SystemAdmin Console DesignPatterns & Conventions
Web AppiOS AppMarketing Website
Testing & HarnessCI Pipeline

Jobs & Providers

Jobs & Providers

Everything that runs without a user: scheduled crons, external service drivers, and the token-authenticated HTTP feed endpoints.

Crons (convex/crons.ts)

Visit status sweep — every 15 minutes

crons.sweep (internalMutation) drives the time-based edges of the visit state machine using each org's configurable windows:

  • planned → due_in once inside the org's early-check-in window (orgs.checkInEarlyMinutes).
  • in_house → overdue past the late-check-out grace (orgs.checkOutLateMinutes).

All transitions still funnel through the same transition() helper the interactive mutations use, so the state machine has exactly one enforcement point.

Reminder dispatch — every 30 minutes

crons.dispatchReminders (internalMutation) walks each org's active reminderConfigs:

  1. For upcoming_visit and vaccine_expiry kinds, find matching clients within offsetHours.
  2. Fill the config's template.
  3. Dedupe against reminderLogs.by_dedupe so a reminder never sends twice.
  4. Drop an in-app message on the client's thread (always).
  5. If the channel is email, schedule providers.email.send.

Note: the agreement_missing kind exists in the schema and settings UI but is not yet handled by the dispatch loop.

Payment providers

All money is integer cents; payments is an append-only charge/refund ledger with idempotency keys. The org's paymentProvider field selects the driver.

Order
subtotal + per-location tax (bps) · deposit · paidCents
take payment
Provider branch
paymentMethod.provider / org.paymentProvider
dev
instant simulated settle (local demo)
manual
cash / desk — orders.recordPayment
stripe
SetupIntent vault → off-session PaymentIntent · idempotent per order+paid
payments — append-only ledger
charge / refund rows · status · idempotencyKey · never updated
Refund caps
>30-day-old payment needs orders.refund_override (owner); caps at remaining refundable
Money flow — integer cents end to end, append-only ledger, provider selected per org
ProviderDriverStatus
devpaymentMethods.charge, portal.payOrderinstant simulated settle; local/dev default
manualorders.recordPaymentcash/desk payments
stripeconvex/providers/stripe.tsproduction driver
fluidpay—reserved in enums, no driver yet

Stripe driver (providers/stripe.ts)

  • Vaulting: createSetupIntent (action) → Stripe Elements confirms client-side → finalizeSetup persists brand/last4/exp + providerRef via saveMethod. Only display data and provider refs are stored — no PANs.
  • Charging: chargeSaved (staff; orders.manage) and portalChargeSaved (customer; ownership-checked) create off-session PaymentIntents, idempotent per order_{id}_{cumulativePaid}, recorded through recordCharge.
  • Talks to https://api.stripe.com/v1 directly; requires STRIPE_SECRET_KEY or throws PROVIDER_NOT_CONFIGURED.
  • Client selection logic in the web app: saved cards branch on method.provider === "stripe" → provider action, else the dev mutation.

Email driver (providers/email.ts)

send (internalAction) — Resend. POSTs https://api.resend.com/emails with a per-tenant from address (org display name + domain from EMAIL_FROM, default reminders@pawdentity.com). Without RESEND_API_KEY it logs and no-ops — the in-app thread message remains the delivery of record.

HTTP endpoints (convex/http.ts)

Calendar feeds for staff, authenticated by the per-user feedToken on the membership (?token=), no session required:

RouteReturns
GET /feeds/events.icsICS calendar of visits/events
GET /feeds/events.jsonsame data as JSON
GET /feeds/consumption.jsonper-room daily occupancy

Tokens are minted and rotated in Settings → General (settings.feedInfo / regenerateFeedToken); feedData (internalQuery) resolves the membership and builds the payload.

Environment variables

VariableConsumerBehavior when unset
STRIPE_SECRET_KEYstripe driverPROVIDER_NOT_CONFIGURED on Stripe paths; dev/manual still work
RESEND_API_KEYemail driverlogs and no-ops; in-app delivery only
EMAIL_FROMemail driverdefaults to reminders@pawdentity.com

The platform is fully functional with none of them set — that's the local/dev posture.