Case Study

One dashboard, seven sources: building a reporting layer you can trust

A commercial real estate portfolio's data lived in seven places that didn't always agree. The job was to put it on one screen. The harder job was proving the numbers on that screen were right.


The situation

A commercial real estate investment firm kept the data about its properties, leases and tenants in many places: several modules of its property management system (general ledger, commercial management, job cost), a space planning dataset, an insurance compliance source, its leasing deal pipeline, and a set of spreadsheets owned by finance, legal and investments. Leadership had a dashboard design. What they didn't have was a data layer that could feed it.

The project is a reporting base in Airtable that mirrors those sources and feeds a dashboard with Property, Lease and Tenant views, plus the acquisition and disposition pipelines. I lead the data architecture and the build, with the client's data owners making the calls on definitions.

Sources
7 synced bases
Plus finance, legal and investments spreadsheets
Pattern
Reporting base
Read-only mirrors, a few deliberately owned fields
Status
Phase 1 in build
Property, Lease and Tenant views, as of September 2026

Principle 1: mirror, don't edit

Mirrored data is read-only downstream. If a number is wrong, it gets fixed at the source, not patched in the dashboard. A patched copy is a second truth, and second truths drift.

The exception is deliberate and narrow: some facts have no system of record anywhere. Whether a lease is triple-net or gross isn't in the property management system or the finance workbook. Some possession dates aren't tracked in any system at all. Those fields are maintained inside the reporting base, inline on the interface, with per-person edit permission and everyone else read-only. The tenant legal and compliance layer became the largest of these, and it's designed to retire a standalone tracking spreadsheet outright.

Principle 2: define before you display

Most of the work wasn't building screens. It was walking every screen, field by field, with the people who own the data, and pinning down what each word means. A sample of what that turned up:

"Tenant" means four different things

A tenant can be a lease, an occupant across amendments, a parent operator, or a retail chain. The obvious key, the tenant ID, turned out to identify the parent operator: keying on it merged six separate locations of one national carrier into a single tenant. Matching on names merged 99 different tenants. The reporting layer keys tenants on the occupant identifier only, with no fallback.

The field names aren't the source's field names

The sync feeding the operational bases renamed fields and spelled out coded values. When a business rule arrives as "status C", the data says "Current". Filtering literally returns zero rows and looks like broken data. Every rule gets translated and checked before it's built.

Settle it against the data, not the meeting

Which of two name fields held the legal name and which held the DBA was an open question after a working session. Rather than wait on a follow-up, I checked all of the roughly 1,800 leases: 76% of one field carried entity suffixes like LLC or Inc, against 19% of the other, and the two differed on 68% of leases. Getting it backwards would have shown the wrong name on most records.

Some standard numbers have no source at all

The industry-standard square footage everyone expected to find in the space planning data turned out to measure interior usable area instead. The standard measurement has no system of record. The screen labels its denominator, so it can be swapped later without archaeology.

Every ruling is recorded with its date and the evidence behind it, so a question closed once stays closed.

The finding: why a third of active leases showed $0 rent

On screen, a large share of active leases showed no base rent. The first explanations were the reasonable ones: rent kept on a prior lease amendment, or charged under a category the rollup didn't recognize. A design was drafted to walk amendment chains back to the rent. Then the client's data team suspected a missing filter somewhere.

They were right, and it wasn't in the reporting layer. Three cheap questions, in order, each ruling out a whole class of cause:

1
Join failures? Every charge row matched a lease. Nothing was lost in the link step.
2
A filter on an obvious axis? Coverage was complete across buildings. Not that.
3
A filter on time? Across 5,130 charge rows, the oldest "last updated" timestamp sat six days inside a two-year cutoff from the day of the check. Zero rows predated it.
Real business data doesn't produce a clean floor. A query does. The upstream sync, built before this project and owned outside it, asked the property management system only for charges edited in the last two years. A lease that had been stable since before then (no amendment, no rent change) never had its charges arrive, and read $0 on every screen. Roughly a third of active leases carried no charge rows at all.

The amendment-chain build was shelved before it started. It would have been a sophisticated fix for a problem that lived one layer upstream, and every population it was sized against was inflated by the missing window.

Two more defects in the same sync

The upsert key included a field people can edit. The sync matched rows on a composite key that included a charge's start date. When a start date was corrected upstream, the next run matched nothing, created a second row, and orphaned the first, which froze with whatever it held, including its "currently in effect" flag. An audit found around 200 orphaned rows across more than 120 leases, and a handful of active leases displaying roughly double their real rent. The tell: inside a set of duplicates, the live row is the one whose "last updated" keeps moving.

// WRONG: the start date is editable in the source system
const key = `${buildingId}_${leaseId}_${startDate}_${category}_${frequency}`;

// RIGHT: the source system's own row identifier
const key = String(row.recurringChargeId);

A failed item was never retried. Progress was recorded as start position plus success count, so a single failure shifted the window: one item processed twice, the failed one skipped for good.

The repair order matters. Deleting the orphans first would have been permanent: a deleted row with an old update date never re-enters a two-year window. The full backfill is the undo button, so it has to exist before anything is deleted. The fix belongs to the sync's owner; my part was the diagnosis, the evidence, and a safe order of operations.

Rules I took away

These now live in my scripting guidelines and apply to every sync I write.

1
An upsert key must be immutable. Use the source system's own row identifier. If there isn't one, say so, and add a reconciliation step that reports rows the source no longer returns. Those are your orphans, and nothing else will find them.
2
A date window on an extract is a silent filter. If you need one, anchor it to the last successful run. A window rolling back from today re-excludes the same stale records on every run, forever.
3
Diagnose an incomplete sync from the destination. Joins first, then obvious axes, then the timestamp floor. Each check is one query and rules out a whole class.
4
Progress cursors track position, not success. Advance past everything attempted, log failures by id, and retry them explicitly.

Where it stands

Property, Lease and Tenant fields each traced to a named source
Client rulings recorded with dates and evidence
An editable legal layer designed to retire a spreadsheet
Root cause of the $0 rent population found before a workaround shipped
Orphan audit with a safe repair order
Four sync rules turned into reusable guidelines

What this demonstrates

A dashboard is only as good as the definitions behind it and the pipes in front of it. The screens are the visible part of this project. What makes them trustworthy is quieter: every field traced to a source, every definition agreed and dated, and the discipline to stop building when the numbers don't add up and go find out why.

Before you fix what's on the screen, find out what never arrived.