← Back to projects

Multi-tenant SaaS for veterinary clinics, built with AI-directed architecture. Pre-revenue; production-ready; maintenance mode.

Role: Product & Software Architect (independent project) Period: Aug 2026 – present Status: production

TypeScriptNestJSNext.jsPostgreSQLPrismaPlaywright AI: Claude CodeAI: ElevenLabsAI: Playwright
137

Data tables modeled: the whole clinic operation, not just appointments

One product covers agenda, records, inventory, billing and more, so a clinic does not need several tools

Counted on the schema, 2026-10-13

96

Versioned database changes (migrations)

Every change to the data structure is tracked and repeatable, so the product can keep evolving

Counted on disk, 2026-10-13

107

Automated end-to-end checks that run real product flows

Changes are tested against real workflows before they can reach a clinic

Counted on disk, 2026-10-13

722

API operations that other systems can call

Integrations (accounting, messaging, partners) have a ready surface to connect to

Counted in the API source, 2026-10-13

Architecture

ARCHITECTURE / VETNOVA 01 Web client Next.js, 5 languages 02 REST API NestJS, 722 routes 03 Tenant context set on every request 04 Row-level security non-owner DB role 05 Smoke tests 107 scripts 06 Cron jobs 28 jobs, with locks 07 PostgreSQL Prisma, 137 models RLS fails closed: one forgotten WHERE cannot leak another clinic.
VetNova architecture: client, API, tenant context, row-level security, PostgreSQL.

Context

VetNova is a multi-tenant SaaS for veterinary clinics: appointments, clinical records, prescriptions, inventory, point of sale, Mexican tax e-invoicing (CFDI 4.0), an owner portal, WhatsApp and email notifications, a public API and five interface languages. The first commit is from 14 August 2026. It has been in production since 7 September and has been frozen since 21 September: the goal now is marketing and support, not new features. It is pre-revenue: production-ready, with no paying customer yet. What stands between the product and its first revenue is legal and fiscal setup, not engineering.

I started from a market study of Mexico City veterinary clinics (August 2026). It argued against a generic clinical record and for a product built around retention and financial visibility, so the scope follows that.

My role

  • Decided: product scope, the multi-tenant data model, the isolation strategy, what is deliberately left out (job queues, a native mobile app in favor of an installable web app), and when to stop building.
  • Verified: behavior against the database and the 107 smoke scripts, screen-by-screen QA rounds, and every count in the documentation against the disk.
  • Delegated to AI: implementation. Claude Code writes the code under my specifications, rules and review of every change; I can read, debug and explain any part of it.

Research

Three pieces of research shaped the build. The market study set the scope. A review of the stack against the actual team (one person directing an AI agent) favored a boring, widely hired stack: TypeScript, NestJS, Prisma, Next.js and PostgreSQL in a pnpm monorepo. And for each external dependency I tested the alternatives before choosing: I compared three text-to-speech engines by ear and by licence, and I measured the real cost of the paid one instead of trusting its public documentation (a three-minute tutorial cost 613 credits, not the 1,227 the documentation implied).

Decisions

DecisionAlternatives rejectedWhy
Row-Level Security (RLS) in PostgreSQL, set per request, with the app connecting as a non-owner roleTenant filters in application code; one database per tenantOne forgotten where leaks another clinic’s data; RLS fails closed. A database per tenant does not scale on serverless Postgres.
No Redis and no job queue; scheduled jobs run in-process (28 cron jobs in 25 files, with locks)BullMQ on RedisOne more piece of infrastructure to operate with no volume to justify it. The cost was measured and accepted: WhatsApp sends are a single attempt with a short timeout.
Hand-written SQL migrations, applied with deploy only; migrate dev is never runprisma migrate dev; editing the two broken migrationsThe schema and the real database differ by 97 cosmetic items in 33 tables, so migrate dev would write a migration of renames. Editing applied migrations breaks their checksums.
Paid ElevenLabs voice for tutorials, free Piper for iterationKokoro or Piper only; XTTS-v2; Edge TTSThe free engines have a better Mexican accent but flat prosody. ElevenLabs returns per-character timings, which gives exact subtitles. XTTS-v2 forbids commercial use; Edge TTS uses an unlicensed endpoint.
Turn Google Ads off after ten daysLeave it running; keep only Search59.3% of clicks were Display, which Google enables by default; only 18 of 54 clicks came from real search intent. A channel is judged by intent clicks, not clicks.
Do not route Claude Code to other model providersCommunity proxies through ANTHROPIC_BASE_URLAll context, including customer data that appears in a smoke test, would reach a provider missing from the privacy notice, and skills and hooks break silently.

Validation & QA

Quality is gated by 107 end-to-end smoke scripts plus classification, integrity and assistant-bank checks, and by written QA rounds with a findings table per round (three rounds for the first release candidate). Visual QA runs with Playwright at several widths, in light and dark themes.

Three warnings that never expire sit at the top of the status file, and each came from a real failure. Before calling a smoke test red, check that the API is serving today’s code. A green smoke does not prove tax correctness: the e-invoicing provider’s test mode accepts what the tax authority would reject. And a route that no smoke calls is not tested even if the suite is green: the multi-invoice endpoint answered 409 to everything until a smoke finally hit it.

I also recount. The status file said 118 pages; the disk says 117, so 117 is what I publish.

Results

  • 137 data models, 96 migrations, 722 REST route decorators and 117 pages, in five languages (8,597 translation keys in 158 namespaces).
  • 125 tables carry a tenant identifier and are covered by RLS.
  • In production since 7 September 2026; frozen since 21 September; pre-revenue.
  • A cheap, fast lesson on paid acquisition: ten days of Google Ads produced no sign-ups and one finding worth keeping about how to read channel data.

What it solves and where else it applies

A veterinary clinic runs appointments, clinical records, prescriptions, inventory, sales and tax invoices. VetNova puts all of that in one product, and the database itself keeps each clinic’s data apart from every other clinic’s, so one forgotten filter in the code cannot leak records.

The same patterns apply in other places:

  • Other appointment-and-records businesses: dental, physiotherapy or human-health clinics need the same mix of agenda, records, inventory and billing.
  • Any B2B software sold to many customers: database-level isolation per customer fits CRMs, school systems or HR tools where one customer must never see another’s data.
  • Businesses that must issue tax invoices in Mexico: the e-invoicing (CFDI 4.0) integration pattern applies to retail, services or any billing system.

What I’d do differently

There is no continuous integration on this repository: the gates are local, which is exactly why the three warnings above exist. My next project (REYBER) has CI on every push from day one, and I would retrofit it here. I would also check the default network settings of an ad campaign before launching it, not after a week of misleading numbers. And I would exercise the full payment circuit before declaring the product ready: the live payment account has never processed a charge end to end, and I have documented that risk instead of hiding it.

Evidence

Full version: private repository, available on request.