OSCapstack CRM
Commercial operating system for real-estate credit origination, in production.
Full-stack development, end to end · built in 26 days
- RLS policies
- 146
- screens
- 42
01 Problem
The whole operation ran on spreadsheets. Leads arrived through three channels — Instagram, LinkedIn and WhatsApp — and were copied over by hand; nobody knew off the top of their head who had already been contacted, and the calendar, the proposal and the documents each lived somewhere else. The product had to become the brokerage's operating system, not a spreadsheet with a coat of paint.
02 Architecture
TypeScript monorepo with Fastify 5 on the API and Supabase/PostgreSQL as the database. Three isolated frontends: an admin panel in React, an external consultant panel (restricted access, scoped to their own data), and an Astro landing page for lead capture. It runs on 56 tables with 146 RLS policies, 219 endpoints and 42 screens. In production at os.capstack.capital.
03 Hard decisions
146 RLS policies across 56 tables
Authorization lives in the database, not the application: every table carries its own row-level security policies, so a bug in the API cannot leak another consultant's data. The cost is performance — every policy runs a context function per row — solved by rewriting the helpers as a scalar subquery so PostgreSQL's planner evaluates it once per query instead of once per row (InitPlan optimization).
Blue-green deploy on a VPS
Every deploy brings the new version up on a separate port from the one currently serving traffic. A 15-attempt health check decides whether it is healthy before any user is routed there. If it fails, the new container is removed and the old one keeps serving — the worst case of a bad deploy is the deploy not happening, never downtime.
WhatsApp dead man’s switch
A container can answer 200 on its HTTP health check while the WhatsApp instance behind it is disconnected — a failure no conventional HTTP probe sees. A cron job every 2 minutes checks the real connection state; if it dropped, it deliberately stops pinging healthchecks.io so the external service raises the alarm from the absence of a signal.
Safe weighted lottery under concurrency
Lead distribution weighs each consultant by clientes_ativos / peso, but two leads arriving at the same time cannot land on the same consultant because of a stale read. Selecting the customer and the consultant uses SELECT … FOR UPDATE inside a single transaction, serializing the read-and-write without locking the whole table.
04 Stack
- TypeScript
- Fastify 5
- Supabase
- PostgreSQL
- React
- Astro
- Playwright
- pgTAP
- Docker
05 What changed
The three channels now land in one place, and the lead is routed the moment it arrives — weighted round-robin or assigned to a specific consultant. The calendar syncs with Google Calendar, client documents are read by AI instead of checked by hand, the messaging layer answers the repetitive part on its own, and the performance report arrives with whatever the AI found out of pattern. Admin and salesperson each have their own app. And the owner builds his own funnel and his own automations: changing a rule of the process stopped depending on me.