Back to Selected Work

Custom site + admin CMS for a Zona Norte real-estate brokerage · live

De Brasi — Real Estate Platform

A self-managed property catalog that replaced a slow, dated site and the brokerage's reliance on third-party listing portals — a public site buyers browse and an admin panel where non-technical staff load properties themselves.

Problem

The brokerage was running an old website — poor design, slow, barely functional — and leaned on third-party listing portals for most of their actual traffic. They needed their own fast, self-managed property catalog: a public site buyers could browse and contact through, plus an admin panel where non-technical staff load properties and read incoming inquiries themselves.

My Role

Sole developer, ~2 months, in production at debrasi.com.ar. The UI shell was scaffolded with v0 (component library + Tailwind); everything below the UI layer is mine: the data model, the entire application/domain layer, all API routes, authentication, the media pipeline, geocoding integration, background jobs, and the maintenance tooling. All the non-trivial architecture decisions were mine.

Architecture & Key Decisions

  • Media as a first-class, decoupled entity. Instead of hanging image URLs off the property row, images/plans/videos live in a dedicated Media model with a polymorphic owner and an explicit state machine (UPLOADING → READY → ORPHAN → DELETING → DELETED), plus soft-deletes. This is what makes the rest of the media handling safe to reason about.
  • Direct-to-storage uploads with a deferred commit. The browser uploads straight to object storage via presigned URLs into a temp prefix; a Media row is created as UPLOADING. Only on commit does the server process the file (EXIF-aware rotation, multi-width WebP variants, blur placeholder), write final keys, and promote to READY. Commits are idempotent.
  • Solving the hard part: orphaned files. The gap between "user uploaded a file" and "user saved the property" is where storage leaks happen. I closed it from three sides: a TTL sweep for abandoned uploads, a split delete path (a cheap DB mark vs. the actual out-of-band storage deletion), and a reconciliation script that lists the whole bucket, diffs it against known DB keys, and deletes true orphans — but only inside project-owned prefixes, dry-run by default, batched. Never touch a key you can't account for.
  • Domain modeling for the business. Property status is an 8-state workflow (draft → active → reserved → sold/rented…), dual currency (USD/ARS), operation type, geolocation with high-precision decimals, and ordered collections (gallery, plans, specs, amenities) with drag-and-drop reordering. Indexes chosen deliberately, not left to defaults.
  • Geocoding with graceful degradation. Address entry is backed by Google Places (autocomplete + details for coordinates). If the API key is missing or the call fails, it returns empty results with a 200 instead of breaking the form — the admin can always still type an address.
  • Thin routes, thick lib. API routes stay small and delegate to a hand-written domain layer with its own validation (unique slugs with accent folding and collision counters, per-collection limits, duplicate detection). Cron endpoints are secret-guarded.

Trade-Offs

  • Single-admin auth instead of a full user system. The brokerage has one login, so auth is a signed httpOnly cookie rather than a full user/role system. I'd move to DB-backed users if the team grows.
  • Cron-driven cleanup instead of a real job queue. Storage deletions run on a schedule rather than through a message broker — simpler to operate at this scale; a queue is the upgrade path if volume demands it.
  • Hand-rolled server validation instead of a schema library at the API boundary. This gave me exact Spanish error copy and business-specific rules, at the cost of more code to maintain.

Stack

  • Next.js
  • React
  • TypeScript
  • PostgreSQL
  • Prisma
  • Cloudflare R2
  • sharp
  • Google Places
  • Tailwind

debrasi.com.ar