Skip to content
Case study · 03 / 09← All work

Nexora

A white-label CRM for real estate agencies, running today for Liliana Cappelli Propiedades.

Role
Product, architecture and full-stack build
Stack
Next.js 16, React 19, TypeScript, Supabase, TanStack Query, Tailwind CSS 4
Agency website
Agency websiteAccess rules live in Postgres
(01) The problem

An agency works across properties, contacts, deals, contracts and WhatsApp. Liliana Cappelli Propiedades, in Villa Gesell, also needed a public website under its own name. Each agent should see only their own portfolio, while the owner role sees everything.

My roleI built the product end to end and deployed it for its first agency.
(02) ResultsCounted in the code
72routes across dashboard, public site and print views
62versioned SQL migrations
3roles enforced by the database, not the UI
(03) Architecture

Three decisions that shaped it.

  1. 01

    Access rules live in Postgres

    Why. Tenant isolation and each agent's portfolio are row-level security policies. The client never filters by tenant. A bug in a screen cannot leak another agency's data.

    Trade-off. Policies are harder to debug than app code. When a deal is reassigned, the old agent stops getting realtime events, so the board refetches on window focus.

  2. 02

    White label by environment variables

    Why. The same code serves every agency. One variable makes the root domain serve a single agency's website and moves the dashboard to an app subdomain. Another hides the NEXORA name everywhere.

    Trade-off. The proxy has to resolve route collisions, since /propiedades exists on both the site and the dashboard. Two hostnames per agency mean more DNS to set up.

  3. 03

    Public site cached for 30 days, purged by tag

    Why. The site used to regenerate every few minutes even when nothing changed, which burned server CPU. Pages now live for 30 days. The actions that change a property or the brand call revalidateTag.

    Trade-off. Every write path must remember to invalidate. A missed one shows stale data for weeks, and three such gaps had to be found and closed.

(04) GalleryKeep scrolling →
Agency website
Agency website · Liliana Cappelli's public home. Colors, sections and contact data come from the tenant's brand settings.
Property listings
Property listings · Served from cache and refreshed only when a property changes.
Deal pipeline
Deal pipeline · Drag and drop with an optimistic update. A move writes one row, using a fractional sort order.
Contacts
Contacts · A virtualized table with keyset pagination, so it stays fast with thousands of rows.
(05) What I'd do next
01Run the unit tests and the SQL role tests in CI. Today CI only lints, typechecks and builds.
02Add Playwright for the core path: a website inquiry becomes a contact, then a deal on the board.
03Move from unstable_cache to "use cache" with cacheTag, so caching and invalidation sit next to the data.
Next project · 04 / 09
HPC →Four web apps and a native app for Hospital Privado de Comunidad
Case study · 03 / 09

Nexora

A white-label CRM for real estate agencies, running today for Liliana Cappelli Propiedades.

Role
Product, architecture and full-stack build
Stack
Next.js 16, React 19, TypeScript, Supabase, TanStack Query, Tailwind CSS 4
Nexora
(01) The problem

An agency works across properties, contacts, deals, contracts and WhatsApp. Liliana Cappelli Propiedades, in Villa Gesell, also needed a public website under its own name. Each agent should see only their own portfolio, while the owner role sees everything.

My roleI built the product end to end and deployed it for its first agency.
(02) Results
72routes across dashboard, public site and print views
62versioned SQL migrations
3roles enforced by the database, not the UI
(03) Architecture

Three decisions that shaped it.

01

Access rules live in Postgres

Why. Tenant isolation and each agent's portfolio are row-level security policies. The client never filters by tenant. A bug in a screen cannot leak another agency's data.

Trade-off. Policies are harder to debug than app code. When a deal is reassigned, the old agent stops getting realtime events, so the board refetches on window focus.

02

White label by environment variables

Why. The same code serves every agency. One variable makes the root domain serve a single agency's website and moves the dashboard to an app subdomain. Another hides the NEXORA name everywhere.

Trade-off. The proxy has to resolve route collisions, since /propiedades exists on both the site and the dashboard. Two hostnames per agency mean more DNS to set up.

03

Public site cached for 30 days, purged by tag

Why. The site used to regenerate every few minutes even when nothing changed, which burned server CPU. Pages now live for 30 days. The actions that change a property or the brand call revalidateTag.

Trade-off. Every write path must remember to invalidate. A missed one shows stale data for weeks, and three such gaps had to be found and closed.

(04) Gallery
Agency website
Agency website · Liliana Cappelli's public home. Colors, sections and contact data come from the tenant's brand settings.
Property listings
Property listings · Served from cache and refreshed only when a property changes.
Deal pipeline
Deal pipeline · Drag and drop with an optimistic update. A move writes one row, using a fractional sort order.
Contacts
Contacts · A virtualized table with keyset pagination, so it stays fast with thousands of rows.
(05) What I'd do next
01Run the unit tests and the SQL role tests in CI. Today CI only lints, typechecks and builds.
02Add Playwright for the core path: a website inquiry becomes a contact, then a deal on the board.
03Move from unstable_cache to "use cache" with cacheTag, so caching and invalidation sit next to the data.
Next projectHPC →
Nahuel SantillánEN