Skip to content

← Work

Property Operations Platform

Organizations that manage residential properties run on facts that go stale quickly: who lives in which room, which rooms are free, which safety certificates exist and which have lapsed, and what support each resident has received. This is a configurable multi-tenant operations platform built for a client to hold all of that in one place, with each customer organization working in its own isolated data.

I was the sole developer. I owned the application, the data model, the access model, the operational workflows, the analytics, and delivery.

At a glance

  • Role: Sole developer and engineering owner across the application, data model, access model, workflows, analytics, and delivery.
  • Stage: Delivered against the agreed milestones and in use by an initial organization.
  • Core constraint: Serve multiple customer organizations from one codebase, with isolated data and configurable workflows.
  • Strongest evidence: Transactional occupancy rules, capability-based permissions across seven roles, explicit records for missing compliance documents, and server-side analytics.

Context

The product had to serve multiple customer organizations from one codebase, each with its own properties, rooms, residents, staff, compliance records, and dashboards, and none of them able to reach another's data. It also had to sit between two product shapes — property management on one side, status-oriented operational logging on the other — with the emphasis adjustable toward either depending on what an organization needed.

That configurability is where the difficulty lives. A product like this fails in the data model long before it fails in the interface: if the domain is not modeled so that organization, property, room, resident, staff responsibility, compliance requirement, and support record each have their own identity and their own rules, every new customer requirement becomes a conditional rather than a configuration.

Two senses of the word tenant run through the system as well — the customer organization the data belongs to, and the resident living in a room. Both are first-class, and they are separate concerns at every level: isolation and access control operate on the first, occupancy and support workflows on the second.

My role

I built the Next.js application, designed the Firebase data model and the services over it, implemented the organization-scoped access model, wrote the Cloud Functions, built the operational workflows spanning properties, rooms, residents, compliance, and support records, and built the analytics layer and delivery pipeline.

The client owned product strategy and domain requirements. Engineering direction and implementation were mine end to end, planned against internal milestones rather than an external deadline.

Architecture

The application is a Next.js App Router front end over Firebase. Every read and write is rooted at an organization: Firestore paths begin with the organization, queries are further scoped to the properties a user is assigned to, and capability checks sit in front of the operations a role is permitted to perform. Forms, tables, and server state run through Zod validation and TanStack Query, Form, and Table, with Zustand holding interface state.

Organization identity is established on the server rather than carried by the client. A verified Firebase ID token is exchanged for an HTTP-only session cookie, so the session the application trusts is one it issued from a token it checked, not a claim the browser supplies. Sensitive administrative operations run through protected Cloud Functions instead of directly from the browser, which keeps the operations that can change other people's data off the client entirely.

Files, attachments, and signatures live in Firebase Storage, and support records are exported as signed PDFs and date-filtered ZIP archives. Analytics are handled by a separate Firebase Functions HTTP API rather than inside the application, for reasons covered below.

Stack

Application — Next.js App Router · React · TypeScript · Zod

State and UI — TanStack Query · TanStack Form and Table · Zustand

Data and services — Firebase Authentication · Firestore · Storage · Cloud Functions

Delivery — Vercel · Firebase and Google Cloud

Engineering challenges

Occupancy that has to stay true across assignment, moves, and vacancy

The problem. Moving a resident from one room to another is a single action in the interface and several writes underneath: the resident's property and room references, the destination room's occupancy, the previous room's occupancy, and the property's collection of resident references. Perform those as independent writes and the system can end up with a room held by someone who left, a resident pointing at a room they no longer live in, or two residents assigned to the same room.

Why it was hard. Occupancy is not a field, it is an invariant spanning several records, and no single write can be made responsible for it. The obvious guard — check that the destination room is free, then write — is a read followed by a write with a gap in between, and two managers placing residents at the same time can both pass the same check. The failure is also worse than a typical data error, because room state feeds dashboards, occupancy analytics, and decisions about where to place someone next; wrong occupancy sends a real person to an occupied room.

The decision. Encode the invariant in the service layer, inside a transaction, rather than in the screen that happens to call it.

Implementation. Assignment, room moves, and vacancy are implemented as Firestore transactions. Each one reads the resident, the destination room, the previous room, and the property state before writing anything; rejects an assignment when the destination room is already occupied; updates the resident's property and room references; marks the destination occupied and clears the previous room on a move or vacancy; and synchronizes the property's resident-reference collection in the same transaction. Every path that changes occupancy goes through those methods, so there is no route to the data that bypasses the rule.

Outcome. Occupancy became a property of the write path rather than of the interface that triggered it. The three workflows that can corrupt room state each commit as a unit or not at all, and an assignment to an occupied room is refused at the point of the write.

Roles that were not a hierarchy

The problem. The platform supports seven operational roles, including organization administrators, property managers, support workers, auditors, and housing-benefit staff. A permission model built as levels would have been wrong on day one: an auditor needs broad visibility and almost no write access, housing-benefit staff need a narrow financial slice, and a support worker needs deep access to the residents they support and nothing past them. None of those sit above or below each other.

Why it was hard. Ranked roles are appealing because the check is a comparison. Real operational responsibilities overlap in patterns that do not line up on one axis, so forcing them into levels means either over-granting access to make a workflow possible, or accumulating special cases spread across the application, where each new screen re-derives who is allowed to be on it and the rules stop agreeing with each other.

The decision. Model capabilities rather than ranks. A role is a set of capabilities, and features ask whether a capability is held.

Implementation. Centralized capability checks describe responsibilities — resident record editing, signup-pack work, support activity, audits, housing-benefit workflows — independently of role names, and the interface guards against those checks rather than against role identity. Capability is combined with property assignment, so holding a responsibility does not imply holding it everywhere: queries are scoped to the properties a user is attached to. Administrative operations that change organization-wide state are performed by protected Cloud Functions rather than from the client.

Outcome. Adding a role or shifting a responsibility became a change to a capability set instead of a sweep through conditionals, and the rules live in one place rather than being reconstructed screen by screen.

The compliance risk is the document that does not exist

The problem. Properties carry mandatory certificates and documents. The straightforward implementation lists what has been uploaded, which means the single most important record — the certificate nobody has provided — is invisible. Absence renders as empty space, and empty space reads as nothing to do.

Why it was hard. A missing item has no record to attach state to, so it cannot be filtered, sorted, counted, assigned, or reported on. Expiry compounds it: a certificate that exists but lapsed is operationally identical to one never provided, while structurally being a completely different case. Staff need both to appear as the same kind of problem without having to know which is which.

The decision. Make absence explicit. If a document is mandatory, the requirement exists as data whether or not the file does.

Implementation. The platform generates placeholders for every mandatory item a property has not provided, so a missing certificate is a first-class record carrying its own status rather than a gap in a list. Expiry status is derived and surfaced alongside it, and compliance-gap analytics count what is missing and what is expiring across the organization instead of only summarizing what was filed.

Outcome. Compliance views answer the question staff actually have — what is not covered right now — rather than describing what happens to be on file. Gaps became something the system can report on.

Organization-wide analytics that could not be computed in the browser

The problem. Occupancy, demographics, compliance gaps, and support coverage are organization-wide questions. Answering them in the client means pulling a large share of an organization's data into the browser on every dashboard visit and aggregating it there, so the dashboards get slowest and most expensive for exactly the customers with the most data.

Why it was hard. Firestore is built for scoped reads, not ad-hoc aggregation across collections, so the numbers have to be produced somewhere other than the query. But staff also need current figures right after a change — someone moves a resident and then goes to look at occupancy — which is precisely when a purely scheduled snapshot is stale.

The decision. Move aggregation out of the browser and support both cadences: scheduled snapshots for normal use, and authenticated recalculation for when the current number matters.

Implementation. A separate Firebase Functions HTTP API owns analytics. A scheduled job precomputes snapshots per organization, and an authenticated endpoint recalculates on demand. Occupancy, demographic, compliance, and support-coverage dashboards read those computed results, so the client renders numbers rather than producing them.

Outcome. Dashboard reads no longer grow with the size of an organization's data, because the browser reads a result instead of the records behind it, and aggregation logic lives in one service rather than being reimplemented per view.

Outcome

The platform was delivered against the agreed milestones and put into use by an initial organization. What shipped covers the full operational loop: organization-scoped properties and rooms, resident records spanning residency, referral, financial, medical, and supporting information, assignment and move and vacancy workflows, mandatory compliance documents with expiry and missing-item visibility, support logs and well-being and room checks with signatures and attachments, date-filtered PDF and ZIP export, and the analytics built on top of all of it.

The client's feedback was about focus. They described the platform as easy to use and effective at keeping necessary information in plain sight — which, in operations software, is the product doing its job, since the usual failure is information that technically exists but nobody ever sees.

The principle underneath the build is that correctness belongs in the write path. Occupancy rules live inside transactions, responsibilities live in capability checks, mandatory documents exist as records whether or not they have been provided, and analytics are computed where the data is. The interface reflects the state of the system rather than being the place that state is decided.