What it is
Soliah Clinic is a CRM for health clinics, the kind that work across several fields: medical, dental, psychology, physiotherapy, nutrition. It is a personal project I ran solo, from design to infrastructure, and it became a study of how far you can go with Supabase and Cloudflare without standing up a server of your own.
The scope is deliberately wide: professionals and service catalog, scheduling, patients, finance, inventory, packages, treatments, medical records, goals, a sales CRM, and billing. The point was not to build one more form, it was to keep 24 modules coherent under the same architecture.
Architecture: Supabase, Cloudflare, and the front-to-back boundary
The system is a pnpm monorepo with three pieces: the frontend in React 19 with TanStack Router (deployed on Cloudflare Pages), the backend in Hono running on Cloudflare Workers, and a shared package with the Zod schemas both sides import. Data lives in Supabase: Auth, Postgres, and Storage.
Cloudflare Pages Cloudflare Workers
React 19 + TanStack Router Hono + Zod (apps/api)
│ │
├─ simple reads and writes ──→ Supabase (Auth · Postgres · Storage)
│ (protected by RLS) │ RLS + RBAC in the database
│ │
└─ orchestration, service_role ──→ Hono
invites, email, cron, webhooks
Supabase Auth ──webhook──→ /webhooks/supabase-auth-email (Hono → Resend)
packages/shared: Zod schemas used by both sides
The decision that organizes everything is a clear boundary rule about what talks to what. The frontend talks to Supabase directly for simple reads and writes, because security lives in the database through RLS. When an operation needs the service_role key, touches several tables at once, or integrates with an external service, it goes through Hono on the Worker. Because the Zod schemas are shared, there is a single contract between the two sides, and a whole class of drift bugs between what the frontend sends and what the backend expects simply disappears.
The lesson: defining that boundary before writing the first route is what kept half the logic from leaking into the frontend and the other half into scattered functions.
From Edge Function to Worker
The first version used Supabase Edge Functions for the privileged operations. As the project grew, keeping logic split between Edge Functions and the rest of the backend got confusing, so I recorded the decision in an ADR and migrated all of them to Hono routes on the Worker: create professional, invite, accept, and cancel invite. Only one function stayed on Supabase, and even then as a webhook: the authentication email dispatch, which Supabase calls on Hono, which in turn sends through Resend.
The lesson is about consolidation: it is worth more to have one backend to reason about than five scattered functions, each with its own deploy and its own conventions.
Multi-tenant with RLS and RBAC
Every table carries organization_id, and the RLS policies filter by organization right in the database. Permissions live there too: a has_permission(module, action) function reads the user’s role from the JWT and decides access, with a per-module data scope (all of it or only your own). Roles are modeled as data, with system templates (owner, admin, professional, receptionist) that each clinic clones and adjusts.
Putting tenancy and permission in the database, rather than in the routes, has a strong practical effect: no new route can forget to scope by organization, because the filter is not in the route, it is in the table.
Domain depth
What I am proudest of is the depth of each area, not the number of screens. Scheduling has recurring series, exceptions, blocks generated from vacations, and a waitlist. Finance has a chart of accounts, payables and receivables, cash sessions with blind closing, and a receivable created by a trigger when an appointment is marked as attended. Inventory works with batches, expiry-first suggestions (FEFO), and automatic write-off from a service’s bill of materials.
Medical records were the most careful part, because they involve sensitive data. There is versioned anamnesis with conditional alerts, clinical evolution in SOAP format, signed documents with a hash, an append-only access audit log, and an LGPD export of the patient as a single bundle. Treating LGPD as a first-class requirement, rather than an afterthought, changed the whole modeling of that part.
Status
It is a personal study, but taken seriously as a product: it is deployed on Cloudflare (Pages and Workers) with Supabase and Stripe wired in, and it was built in a few months, solo. It has no paying customers; the goal was to prove, end to end, a genuinely serverless architecture for a complex domain. The parts still missing (commission payouts, financial reports, calendar integration) are already specified, under the same spec-before-code discipline that guided the rest.