Project Case Study
Atlas
Enterprise Property Operations Product · Private
A real-estate operations platform where the commercial model lives inside the transactions: plan limits, add-ons and capability tiers enforced under row locks rather than checked on a billing page.
Overview
Atlas is a real-estate operations platform: properties, owners, residents, staff, vehicles, occupancies and the money that moves between them. It runs one isolated deployment per client, where the database itself is the client boundary rather than a column in a WHERE clause.
Its distinguishing engineering is that the commercial model is not a layer on top of the product, it is inside the transactions. Plan limits, add-ons and capability tiers reach into row locks, guards and error payloads rather than into conditionals on a billing page.
Atlas is also the oldest and largest codebase in a family of related products, and the origin of most of their shared vocabulary. That makes it a useful case study in a specific way: it holds the first working version of a great many things, and a first version is rarely the one you keep.
Dentra, the clinic platform, is the sibling that took those patterns into production and hardened them. Its case study covers the deployment, storage and observability engineering that Atlas has not needed and does not yet have.
- Lines of backend TypeScript
- 40k
- Backend modules
- 18
- Backend test suites
- 28
- Schema migrations
- 17
- Applications in the workspace
- 4
Deep Dive · 20 min read
The Atlas Engineering Notebook
The long-form record: entitlement enforcement under row locks, capability tiers and non-destructive downgrade, the operator control plane, and an honest account of a design that was abandoned.
The System at a Glance
Before the story, the shape. Four applications, one isolated deployment per client, and a single operator reaching all of them from outside.
ONE DEPLOYMENT PER CLIENT, addressed remotely by one operator
browser ─▶ Next.js app ─┐
visitor ─▶ marketing ├─▶ API container ─▶ Postgres
site │ │ own database
atlasctl ─▶ internal API ─┘ ▼
local filesystem
(no provider boundary yet)
WORKSPACE
apps/ backend (18 modules) · web · marketing · atlasctl
packages/ types · permissions · ui · utils · configs
SCALE a client's portfolio runs to hundreds or low thousands
of properties, so the pressure is on correctness under
concurrent edits rather than on query volume.Phase 01
Establishing the Vocabulary
Atlas was built first, and much of what it produced was not property-specific at all. An upload pipeline arrived as six services in a single day, covering storage, image processing, content validation, cleanup, orchestration and the decorator that wires it to a route. Audit logging was the second schema written in the entire project, before most of the domain existed.
Two decisions in that audit schema aged particularly well. The actor is nullable, so a system action is representable without inventing a fake user to attribute it to. And the action and entity type are free text rather than database enums, so auditing a new operation never requires a migration. Every product built afterwards converged on that second choice independently, which is the strongest evidence available that a shape is right.
The frontend vocabulary followed: a config-driven data table, dynamic forms rendered from field definitions, a form wrapper whose actual contribution is sending only the fields that genuinely changed, and a filter toolkit that keeps filter state in the URL so a filtered view is a shareable link.
None of this was designed as a framework. It was written to ship a product, and it became shared vocabulary only because later products copied it. That order matters, and it is the reason the patterns are sound while several of the implementations are not the ones worth keeping.
written here first, inherited by everything after
────────────────────────────────────────────────────
upload pipeline storage · processing · validation
cleanup · orchestration · decorator
audit by diff actor nullable, action free text
config-driven table one config object per module
dynamic forms change detection: send only what changed
URL-synced filters a filtered view is a shareable link
internal API key-gated operator control plane
plan + usage limits, add-ons, enforcementPhase 02
Modelling People and Property
The obvious way to model a property system is to give owners and residents their own tables. Atlas does not, and the reason is that the same human is frequently both. A resident who buys their unit, an owner who moves into one of their own properties, a staff member who also rents: separate tables mean the same person exists twice, with two phone numbers to keep in step and two sets of identity documents.
So there is one person record, and owner, resident and staff are roles that person plays. A person may optionally be linked to a login account, which makes the distinction between someone the system knows about and someone who can sign in an attribute rather than a separate entity.
Identity documents hang off the person rather than the role, with a type and a side, because a national identity card has a front and a back while a passport page does not. That is enforced when a document is attached rather than left to the caller to get right. The national identity number itself is stored encrypted, with an explicit instruction in the schema never to expose it on a public route.
Above the property hierarchy sits an organisational one. A client is an agency, and an agency has branches: named offices with their own address and phone, to which buildings and staff are assigned. It is a small table, and it is what makes a multi-office agency one deployment rather than several, which matters because the alternative was selling an instance per office and reconciling them by hand.
Underneath, two tables record how the relationships between people and property changed over time. Ownership history and residency history each hold an interval, a start and an optional end, and each carries a reference to the transaction that caused it: the deal that transferred the property, or the deal and lease that placed the resident. That provenance link is the part worth copying. A history table recording what changed answers who owned this in March. One that records why answers the question which always follows, which is on what basis, and that is the one a dispute actually turns on.
The containment hierarchy is the easy half of the domain: complexes hold buildings, buildings hold floors, floors hold units. The hard half is the occupancy lifecycle attached to the leaves, which is where the model stops being a tree and becomes a state machine. Moving a resident in creates an occupancy and marks the unit occupied in one operation, so the two facts cannot disagree. Moving out closes the active occupancy and vacates the unit together. The active occupancy is looked up from the unit rather than supplied by the caller, because a caller passing an identifier could close the wrong one, and a move-out dated before its own move-in is rejected rather than stored, because a negative tenancy corrupts every report that touches it.
┌──────────────┐
│ PERSON │ one record, encrypted national id
│ (optional │ documents attach here, with side
│ login) │
└──────┬───────┘
│ plays
┌──────────┼──────────┐
▼ ▼ ▼
OWNER RESIDENT STAFF roles, not tables
COMPLEX ─▶ BUILDING ─▶ FLOOR ─▶ UNIT
│
▼
OCCUPANCY move-in / move-out
validated against
the record it closesPhase 03
Plans, Tiers and Add-ons
Atlas ships three named tiers, standard, premium and enterprise, and none of them is a value in the code. Plans are a table seeded per deployment, carrying the ceilings for properties, staff, custom roles and storage. A null ceiling means unlimited rather than zero, which is the encoding that keeps the upper tiers from needing a special case at every call site.
Each plan also carries a flag for the capability tier, and this is where two vocabularies meet that are easy to keep apart by accident. Internally the capability is called Level 2. Commercially it is what Premium unlocks: standard is the flat product with finite ceilings, while premium and enterprise both carry the flag and the unlimited ceilings that come with it. So Level 2 and Premium are not two features, they are the engineering name and the sales name for the same line in the product. The rest of this case study writes it as Level 2 (Premium) on first mention in each section, because the code says one and the invoice says the other.
Product limits and pricing are deliberately separate tables. What a plan includes changes when the product changes; what it costs changes with currency, billing interval and time. Fusing them means a price change rewrites the definition of the product, and a historical invoice can no longer be explained. The pricing table carries its own constraint, stated in the schema: a one-time price must not have an interval and a recurring one must.
Add-ons are published as a catalog of seven items across three categories. Capacity raises a ceiling and carries a quantity: property capacity, staff capacity, storage capacity. Permissions unlocks authorisation behaviour with no quantity at all: custom permissions, and role management, which bundles custom role slots with the ability to manage roles and assign permissions. Platform covers the two that change what the product is rather than how much of it you get: the Level 2 upgrade that opens the complex, building and apartment hierarchy, and premium support.
Every label in that catalog is a translation key rather than a string. Title, description, category, the request prompt and each listed capability are all resolved through the localisation layer, because the product ships in three languages and a commercial catalog written in English would be the one screen a client could not read in their own.
The catalog is separate from what the system can actually grant, and the separation is deliberate. Each catalog entry names the underlying add-on types it activates, and two of the seven currently name none: they are listed as requestable and are fulfilled by a conversation rather than by a database write. The rule is stated in the operator documentation, that a planned add-on stays disabled until it has both a correct enum mapping and defined business behaviour. A catalog that can advertise slightly ahead of enforcement is useful; one that silently pretends to grant something is not.
Each granted add-on carries its own billing type, interval and payment status, and that status includes waived alongside pending and paid, because an operator granting something without charge is a real commercial act that deserves recording rather than being faked as paid.
The effective limit for any resource is therefore resolved from three layers rather than read from one column. The plan supplies a default, the client row may carry an override for a negotiated deal, and every currently-active add-on adds its quantity on top. Nothing stores the resolved total, because a stored total drifts out of step with the add-ons that produced it the first time one expires.
Plan changes are history rather than a mutation. The client row records when the current plan was activated, when it last changed, and which plan preceded it, with a separate history table behind that. A client that upgrades mid-cycle has a period that belongs partly to each plan, which is a fact a single row cannot represent.
PLANS seeded data, not code
standard · premium · enterprise
maxProperties · maxStaff · maxCustomRoles · storageGb
null = unlimited hasLevel2 = capability tier
priced in a separate table, so a price change never
rewrites what the product includes
CATALOG what a client can see and request
├ capacity property · staff · storage + quantity
├ permissions custom permissions · role mgmt no qty
└ platform level 2 upgrade · premium support
│ every label is a translation key, not a string
│ activeAddonTypes[]
▼
GRANTABLE what the system can actually enforce
extra_properties · extra_staff · extra_storage
custom_permissions · permission_management
role_management · custom_roles
two catalog entries map to nothing yet, on purpose:
advertised, fulfilled by conversation, never faked
EFFECTIVE LIMIT = plan default
+ client override negotiated deal
+ active add-ons summed quantityPhase 04
Enforcing Limits Under Concurrency
Atlas is sold on plans with limits, extended by add-ons, and gated by capability tiers. The naive implementation is a boolean check that hides a button. That version breaks the moment two staff members create a record in the same second.
Resolving the effective limit is only half the problem. The check has to run inside the same transaction as the insert it guards, under a row lock on the client, because otherwise two concurrent creations both read the same count, both conclude there is room, and both write. The client ends up over a paid limit with no error recorded anywhere, which is the failure mode that makes an entitlement system decorative rather than wrong.
Exceeding a limit returns a decision rather than a refusal: a structured error carrying a machine-readable code, the effective ceiling and the current count. The interface can say you are at fifty of fifty and name the add-on that raises it, instead of showing a generic failure and leaving the client to guess.
Database contention is retried narrowly and on purpose. Deadlocks retry immediately, because the database has already resolved the conflict by choosing a victim and the operation will now likely succeed. Lock timeouts retry with exponential backoff, because contention needs time to clear. Everything else fails immediately rather than being retried into a worse state.
create request
│
▼
BEGIN transaction
│
▼
SELECT ... FOR UPDATE lock this client's row
│
▼
effective limit = plan allowance + active add-ons
│ (resolved, never stored)
▼
count current ──▶ over? ──▶ 422 { code, limit, current }
│ a decision, not a boolean
▼
INSERT ──▶ COMMITPhase 05
Tiers, and Downgrades That Preserve Data
Atlas sells depth as well as capacity. Basic property records are available to every client; the full hierarchy of complexes, buildings, floors and units, with occupancies and resident vehicles attached, is Level 2 (Premium), the higher tier.
Tiered capability raises a question most systems answer badly: what happens when a client stops paying for it? The guard here checks two things, and the second is the interesting one. It asks whether the client holds the tier, and whether that tier is archived. Archived is a distinct state from absent: the capability was held, is no longer paid for, and the data it produced still exists.
So removal never deletes. It marks the add-on inactive and stamps an end time. Custom roles are not reset, permission assignments are not stripped, and records created while the capability was active remain readable. Only future actions are blocked by the newly effective limits. Re-subscribing is therefore a no-op on the data side rather than a support ticket and a restore.
The tier is not the only thing sold this way, and the second case is more unusual: the authorisation model itself is a paid capability. Every client gets the built-in system roles. Creating custom roles, and assigning permissions to them, requires an add-on, and the number a client may hold is a limit resolved like any other, with the remaining allowance calculated against what they already have. Selling the ability to reshape your own permissions is a decision most products do not make, and it means the role service consults the entitlement model before it will let a role exist at all.
That policy is written into the operator documentation rather than left implicit in code, which matters more than it sounds. A downgrade rule that exists only as an implementation detail will eventually be tidied away by someone cleaning up rows that look orphaned.
NEVER HELD HELD ARCHIVED
┌───────────┐ ┌───────────┐ ┌───────────┐
│ no tier │─────▶│ active │─────▶│ inactive │
│ │ buy │ │ stop │ endsAt set│
└───────────┘ └───────────┘ └───────────┘
│ │ │
create: ✗ create: ✓ create: ✗
read: ✗ read: ✓ read: ✓
data: - data: ✓ data: ✓ kept
absent and revoked are different states; collapsing
them turns a downgrade into permanent data lossPhase 06
The Internal Operator API
If every client is an isolated deployment, routine commercial work happens against many separate systems. Doing that by hand means shell access to production databases, which is the worst available answer to a routine task.
Atlas built an internal operator API, disabled by default, key-gated on every route, with optional IP allow-listing on top. It accepts no client identifier anywhere, in a route or in a body, because the deployment's database is already the client boundary and a parameter naming a client would only be a way to get that boundary wrong. The strongest input validation available is a field that does not exist.
Writes carry idempotency keys, so a retried request returns the original result rather than granting a second add-on. Operator tooling runs over unreliable connections and gets re-run by people who are not sure whether the first attempt landed. Every operation is audited with before state, after state, a reason and an actor recorded as an operator rather than as a user, which is the only way an audit trail stays honest about work done outside the application.
The API key is compared in constant time rather than with string equality, so a wrong key cannot be narrowed down by timing the rejection. Routes accept both hyphenated and unhyphenated spellings of add-ons, which is a small compatibility courtesy to whatever an operator typed last time.
The capability tier gets three operations rather than two: enable, archive and restore. Restore existing as a first-class endpoint is what makes the non-destructive downgrade in the previous phase real rather than aspirational, because it proves the archived state was always meant to be reversible.
atlasctl profiles hold a key NAME
clients list · add · edit · remove · use · current
addons available · list · add · remove (--dry-run)
usage · status · history (--json)
│
│ x-internal-api-key compared in constant time
│ x-idempotency-key retry returns the first result
│ x-request-id correlates CLI call to logs
▼
INTERNAL API disabled by default · IP-restrictable
GET addons · addons/available · usage
POST addons · addons/:id/remove · settings
POST level-2/enable · level-2/archive · level-2/restore
│
├──▶ CLIENT A deployment ──▶ own database
├──▶ CLIENT B deployment ──▶ own database
└──▶ CLIENT C deployment ──▶ own database
no route or body accepts a client id, so the database
IS the boundary and there is nothing to address wrongly
every call audited: before · after · reason · operatorPhase 07
atlasctl, the Operator's Client
Everything in the three phases above is inert without a way to operate it. Plans, add-ons, capacity limits, capability tiers and a downgrade policy are a description of a commercial arrangement, and a commercial arrangement is agreed somewhere the software cannot see: a phone call, an invoice, a bank transfer. Somebody then has to make the system reflect it. That job is what atlasctl exists to do, and it is the difference between a billing model and a billing model you can actually sell against.
The alternatives are worth naming, because they are what it replaced. One is connecting to a customer's production database and writing the rows by hand, which puts a person with a SQL prompt inside the data of the business paying them. The other, and the one actually documented before this existed, was assembling the internal API calls in Postman: the key pasted into a header, the deployment chosen by editing a URL, the idempotency key invented on the spot or forgotten, and no record afterwards of who did what.
Both work once. The problem is that neither degrades gracefully with volume, and volume is the direction this goes. Every client is a separate deployment with its own configuration, its own plan, its own accumulated add-ons and its own property estate, and a single grant is not one value but a small commercial record: which add-on, what quantity, what amount, in which currency, recurring or one-off, at what interval, paid or pending or waived, and why. Assembling that by hand is seven chances to be wrong, and being wrong means a client billed for something they did not buy or granted capacity they did not pay for. Doing it twice a year is tolerable. Doing it across a growing fleet, on request, at speed, is a liability.
So the operator surface became a fourth application in the workspace rather than an admin screen. An administrative UI would have to live inside a deployment, which means one login per client, one more thing to secure on every instance, and no way to ask a question across the fleet. A command-line client lives on the operator's machine and reaches all of them, which is also what makes a question like which clients are near their property limit answerable at all.
What it actually does is scope everything to one client and then make the commercial model legible. An operator selects a client once, and every subsequent command runs against that deployment. Asking for the available add-ons prints that client's catalog with two facts per entry that are easy to conflate and important to separate: whether the item can be granted at all, and whether this client already has it. Two of the catalog entries print as not addable, which is the catalog-versus-enforcement gap from earlier surfacing exactly where an operator needs to see it, rather than being discovered when a grant silently does nothing.
From there the operations are the ones the commercial model implies. Grant an add-on with its full record attached. Revoke one by identifier or by picking it from a list, non-destructively, per the downgrade rule. List what a client currently holds and in what state. Read usage, which resolves the effective limits from plan, override and active add-ons and shows them against current consumption, so the question of whether someone needs more capacity is answered rather than estimated. Check status, which reports the selected client, its environment, whether it is reachable at all, and a usage summary in one call. Read back the local history of what this operator has done.
One thing it deliberately cannot do, and the gap is instructive. Level 2 (Premium) has no add-on type behind it, so it is the single entitlement the tool cannot manage: enabling or archiving Level 2 is still a hand-assembled API call, exactly the thing this exists to eliminate. That is the concrete cost of the triplication described later, and it is why that cleanup is worth a day.
Its state is four files in a directory under the operator's home. A client registry holding one profile per deployment, a config file recording which client is currently selected, an optional environment file, and an append-only history log. Profiles deliberately contain no secrets: a profile holds a display name, an environment, a base URL, and the *name* of the environment variable that carries the key. That indirection is what makes the registry safe to read, copy between machines, back up, or paste into a support conversation.
The history log is written owner-readable only, with one line per mutation recording the timestamp, the client, the action and a normalised detail string. It is a local record of what this operator did, and it complements rather than replaces the audit trail the deployment keeps: one survives the operator losing their laptop, the other survives a deployment being rebuilt.
Safety is layered rather than piled onto a single confirmation. A dry-run flag prints the exact request without sending it. Destructive actions require the client name typed back. Actions against an environment marked production additionally require the word production typed out, and that check returns immediately for anything else, so friction is proportional to risk rather than uniform. Idempotency keys are generated automatically when the caller does not supply one, which means retry safety is the default rather than something an operator has to remember at the moment they are least likely to.
Errors name the deployment. A failed call reports the method, the full URL and the response detail, because an operator holding six client profiles needs to know which one refused, not merely that something did.
The contrast with the sibling product is the interesting part. Both manage isolated deployments, and their CLIs are opposites. This one reaches outward over an authenticated API to administer running systems. The other scaffolds configuration inward, in the repository, and orchestrates scripts an operator could have run by hand. Neither design would suit the other product, and that is the point: an operator tool is shaped by where the thing it operates actually lives.
atlasctl use <client> everything after is scoped
addons available this client's catalog
├ addable / not addable can it be granted?
└ active / inactive does it have it?
addons add grant, with the full commercial record
quantity · amount · currency · billing
interval · payment status · note
addons remove revoke, non-destructive, by id or picked
addons list what this client holds, and in what state
usage effective limits vs current consumption
status client · environment · reachable? · usage
history what this operator did, locally
clients list · add · edit · remove · use · current
~/.atlas/ clients.json holds a key NAME, never a key
history.log is mode 0600, append-only
SAFETY, proportional to risk
--dry-run · type client name · type "production" only when
it is production · idempotency key generated if omitted
CANNOT DO enable or archive Level 2. It has no add-on type,
so it stays a hand-made API callPhase 08
Onboarding at Scale
Real-estate clients arrive with existing portfolios in spreadsheets, so bulk import is four separate services rather than one: parse, map, validate, execute. The split is what makes a preview possible, because validation can run without writing anything.
Every row is checked individually, enum fields are verified against their allowed values, nested person sections are validated within each row, and validation errors are flattened into readable field paths. Duplicates are detected both against records already in the database and within the upload itself, because a spreadsheet that repeats a property number is as much of a problem as one that collides with existing data.
The value of an import feature turns out to live entirely in the stage that writes nothing. Parsing and executing are mechanical; a complete list of problems delivered before any write is what makes the feature safe to hand to a client. The same principle later drove configuration validation elsewhere in the family: return every problem at once, because the consumer is a human fixing a file.
PARSE ──▶ MAP ──▶ VALIDATE ──▶ EXECUTE
read the columns every row, all write,
file to fields errors at once once
│
├─ enum values checked
├─ nested person sections
├─ duplicates vs database
└─ duplicates within the file
│
▼
nothing is written here
which is the entire pointPhase 09
Publishing to Strangers
Atlas is the only product in the family with an audience that is not logged in. Each deployment carries a public storefront: a browsable, paginated property listing with price-range filtering, a detail page per property addressed by slug, an image gallery, and a contact surface. A client configures their own public presence from inside the application, setting a logo, a cover image, a description and the contact channels they want shown.
Serving anonymous traffic from a database that also holds owner contact details, deal history, internal notes and encrypted national identity numbers forced a boundary the other products never had to draw. Atlas draws it three separate ways, and the redundancy is the point.
The first is a naming convention. Every field intended for anonymous display is prefixed: public description, public phone, public email, public address, public website, and the social channels. Publishing something by accident therefore requires renaming a column, and the safe-to-expose set is greppable rather than remembered. A property carries both a public description and an internal one, falling back to the internal text only when no public version has been written.
The second is a dedicated repository. The public projection is assembled from the fields it should show rather than derived from the internal record with fields removed, because subtraction fails open: the default for a newly added column is to be included, and the first person to add an internal note to a property row would publish it.
The third is that visibility decisions are enforced at the projection rather than in the interface. Whether prices appear publicly is a per-client setting, and when it is off the price is nulled in the query that builds the response. A front-end toggle would have left the number sitting in the API payload for anyone who opened the network tab. Archived properties are excluded at the same layer.
This is also where the sharpest lesson in the codebase came from. Identity documents, including scans of national identity cards, were once reachable through the public media mount: a URL was enough. The mount was narrowed to public assets and the documents moved behind an authenticated, client-scoped route, with the stored URLs deliberately preserved so nothing needed migrating. A static mount is a permission decision that does not look like one, and nothing in the review of an upload feature says out loud that a directory has just become world-readable.
INTERNAL RECORD owner contacts · deal history
internal notes · encrypted national id
│
│ THREE BOUNDARIES, deliberately redundant
│
├─ 1. naming publicDescription · publicPhone
│ publicEmail · publicAddress · socials
│ publishing by accident needs a RENAME
│
├─ 2. projection a dedicated repository selects what
│ to SHOW, never the record minus fields
│ (subtraction fails open on new columns)
│
└─ 3. enforcement price shown? per-client setting, and
the price is NULLED in the query
not hidden in the UI
│
▼
PUBLIC STOREFRONT listing + price filter · detail by slug
gallery · contact · archived excludedPhase 10
Speaking Three Languages, Two of Them Right to Left
Atlas ships in Kurdish, Arabic and English, and two of those three read right to left. That is not a localisation task bolted on at the end; it decides how text is stored, how the interface is laid out, and who is allowed to change a word.
Translations live in the database, split across two tables rather than one. A key table records what needs translating, with a unique key and a group it belongs to. A value table holds one row per key and locale, unique on that pair. The split is the decision worth stating: a key is a thing that requires translation regardless of how many languages exist, so adding a fourth locale adds rows rather than a column, and needs no migration. A single table with one column per language would make every new language a schema change, and would leave a missing translation indistinguishable from an empty one.
Because the values are data rather than compiled assets, a client can edit them. Translation management is a settings screen inside the product, which means an agency that calls a unit a suite, or uses a particular word for a deposit, can correct the interface to its own vocabulary without a deployment and without asking anyone. The alternative, shipping every wording change through a release, makes the product feel foreign in exactly the market it was built for.
Direction is handled once, at the layout level, rather than per component. Getting that wrong is not a cosmetic problem in a right-to-left language: it inverts table column order, flips icon meaning, and moves the primary action to the wrong side of a dialog, which is the side people click without reading.
The reach of this goes further than the interface. The commercial catalog described earlier carries translation keys rather than strings for every title, description, category and capability, so even the list of add-ons a client is offered arrives in their own language. A product sold in a market where English is the third language cannot have its pricing page be the one screen the buyer has to translate for themselves.
translation_keys what needs translating key (unique) · group translation_values one row per key per locale keyId · locale · value unique (keyId, locale) ku ar en adding a 4th locale ◀─── ◀─── ───▶ adds ROWS, not a column RTL RTL LTR and needs no migration EDITABLE BY THE CLIENT settings screen, not a release an agency that says "suite" instead of "unit" fixes it itself, in its own words, without a deploy REACHES THE COMMERCIAL SURFACE TOO the add-on catalog stores keys, not strings, so pricing arrives in the buyer's language rather than the seller's
Engineering Research
Investigations and modelling decisions that shaped the architecture, including one that produced a design and no code.
- A config-driven list builder, designed and never built. A migration guide in the codebase describes a reusable list component with per-module configuration, projecting a 250-line properties view down to roughly 75, including a callback seam for business actions like ownership transfer and resident assignment. None of it exists: no component, no config directory, and the file it proposed replacing is gone too. The design was sound and correctly identified that the module's difficulty was its business actions rather than its table rendering. What is missing is any record of why it was dropped, which is the clearest illustration of the documentation gap listed below.
- Public projections built up rather than down. Listings are served to anonymous visitors by a dedicated repository returning a deliberate subset, not by taking the internal record and removing fields at the edge. Subtraction fails open: the default for a newly added column is to be included, so the first person to add an internal note or an owner's contact detail to a property row publishes it. Building up means a new field is private until someone decides otherwise.
- One person, many roles, rather than a table per role. Owners, residents and staff are the same human playing different parts, so they share one person record with an optional link to a login account. The alternative duplicates a resident who buys their unit, and then has to keep two phone numbers and two sets of identity documents in step forever. Identity documents attach to the person and carry a type and a side, because a national identity card has a front and a back and a passport page does not.
- Where the upload framework forced a two-layer size check. Multer's file filter runs before any bytes are read, so the file size is simply not known at the point the natural check would go. Per-field limits therefore have to be enforced after parsing, while the stream-level cap is set to the largest configured limit so genuinely oversized uploads are rejected before they are buffered. Two checks at two layers, because one layer cannot see the size and the other cannot see the field.
- Static asset serving audited, and identity documents moved behind authentication. Person documents, which include scans of national identity cards, were reachable through the public media mount: anyone holding a URL could read one. The mount was narrowed to public assets only and the documents moved to an authenticated route that requires a session and scopes access to the caller's client. The stored URLs were deliberately preserved so existing records kept working, which is what made the fix deployable without a data migration.
- Denormalised billing fields that document their own dishonesty. The client row carries a current billing period for convenience, and the schema states in the table definition that those fields are hints rather than truth, naming the three cases where they will lie: a mid-cycle upgrade splits one period across two plans and a single row cannot hold both; an invoice that failed or has not run yet leaves the period looking active when nothing has been billed; and manual credits, discounts or proration are visible only on the invoice. Each case names its authoritative source, which is the plan history table, the invoice records, or the last-invoiced timestamp. Writing down where a cache lies is more useful than removing the cache, because the convenience is real and the next reader would otherwise rediscover the discrepancy during a billing dispute.
- Free-text audit actions rather than database enums. Recording a new kind of audited action needs no migration, which means auditing a new operation is never the thing that makes a change expensive. Every product in the family reached this conclusion independently, and independent convergence is far stronger evidence than agreement after copying.
Engineering Outcomes
- Entitlement enforcement that survives concurrency: effective limits resolved from plan and add-ons at check time, enforced under a row lock inside the transaction that performs the write.
- Limit failures that return a machine-readable code, the effective ceiling and the current count, so the interface can name the remedy rather than showing a generic error.
- Capability tiers with an explicit archived state, so a downgrade blocks future creation without deleting records, resetting custom roles or stripping permission assignments.
- Narrow, deliberate retry behaviour: immediate retry on deadlock, exponential backoff on lock timeout, and immediate failure on everything else.
- An operator API that is disabled by default, key-gated, IP-restrictable, idempotent on writes, correlated by request id, fully audited, and structurally incapable of addressing the wrong client.
- A CLI whose client profiles contain no secrets, with typed-back confirmation on production actions and an append-only operator log.
- A four-stage bulk import whose validation pass reports every problem, against both the database and the file itself, before anything is written.
- Identity documents moved off a public static mount and behind an authenticated, client-scoped route, without breaking the URLs already stored against existing records.
- The patterns a family of products later built on: the upload pipeline, audit by diff, config-driven tables and forms, and a URL-synced filter toolkit that even products rejecting the rest of the system chose to copy.
Known Gaps
Recorded because a case study that only reports what went well is not a case study.
- Test coverage is thin relative to the codebase: 28 test files against 275 backend source files. What exists is well-targeted, with dedicated suites for the usage service, the import stages and the capability guard, but the untested majority includes most of the domain services.
- The public catalog and the seeded plans disagree on a name. The database seeds standard, premium and enterprise; the public pricing page presents Starter, Premium and Enterprise. Harmless until a client quotes one name and an operator searches for the other.
- No CI. Nothing runs automatically; every test, typecheck and build depends on someone remembering.
- No architecture decision records and no operational runbook. Conventions and two good README files are the extent of the written reasoning, and the abandoned list builder is the visible cost of that.
- A duplicated page file left in the repository under the public property route, named as a copy. Cosmetic, and the same category as the doubled file extension above: a fair measure of how much of the codebase nobody has had reason to revisit.
- Retry logic identifies lock timeouts by matching text in an error message rather than by a code. Deadlocks use a stable Postgres code and are handled correctly; the timeout branch is fragile by comparison and would fail silently if a driver changed its wording.
- No storage abstraction. Files are written to and read from the local filesystem with no provider boundary, which is the largest architectural gap relative to what the family later learned, and the reason the storage layer was rebuilt from scratch elsewhere rather than extended here.
- A global exception filter with a doubled file extension in its name, surviving months. Cosmetic, and a fair measure of how much of the codebase nobody has had reason to revisit.
Lessons Learned
- Serialise the check and the write, or the limit is decorative. Every system selling capacity has a race between counting and inserting, and a row lock on the entity being limited is the smallest correct fix.
- Absent and revoked are different states. Collapsing them turns a downgrade into data loss and a returning customer into a support incident.
- Remove the parameter rather than validating it. An internal API with no field for a client identifier cannot address the wrong client, which is a stronger guarantee than any check.
- Build public projections up, never down. Subtracting fields from an internal record fails open the day someone adds a column.
- Audit what happens outside the application, and name the actor honestly. Operator actions attributed to a user are worse than no audit trail, because they are believed.
- Validate completely before writing anything. Whether the input is a spreadsheet or a configuration file, the consumer is a human who wants the whole list of problems rather than the first one.
- A first implementation proves the shape; it is not automatically the version to keep. Extracting a first draft into shared code canonises its accidents alongside its ideas.
- A design decision without a written reason will be re-litigated. The abandoned list builder cost nothing to write and cannot now be evaluated, because the reasoning that killed it was never recorded.
What I Would Do Differently
Distinct from the lessons above, which are rules that held up. These are the calls I would make differently starting over.
- Write decision records from the beginning. The abandoned list builder is the visible cost, but the real one is that a codebase carrying this much original design cannot explain why it looks the way it does. Records that state what was chosen, what was rejected and what evidence would reopen it are the cheapest durable artifact available, and Atlas has none.
- Put a provider boundary in front of the filesystem on the first upload. Files are still written and read directly, which is the largest structural gap relative to what the family learned later, and it is why the storage layer was rebuilt from scratch in a sibling product instead of extended here.
- Model Level 2 as an add-on from the start rather than as a plan flag. It ended up represented three times, needs its own endpoints that nothing else needs, and cannot be sold to a client on a lower plan. None of that was a hard problem; it was a shape chosen early and never revisited.
- Grow the test suite alongside the domain rather than behind it. The commercial core is well covered because it was obviously risky. The domain services are not, and they are where most of the code and most of the change now lives.
- Match the marketing plan names to the seeded ones. Starter in public and standard in the database is a trivial mistake that will cost someone a confusing support conversation.
Roadmap: Level 2 (Premium), and Why It Paused
Level 1, the standard plan, is finished. It serves smaller agencies, and it models a property as a record: an address, an owner, a status, documents, deals. That is the whole product for a client with a portfolio of individual properties, and it is complete.
Level 2 (Premium) changes the shape rather than the size. A property stops being a record and becomes a container: complexes hold buildings, buildings hold floors, floors hold units, units hold occupants and their vehicles, and the whole thing carries tenure history. The backend for that is built. Six entities, thirty-four routes, a capability guard on the module, per-entity permissions, and the screens to drive it all exist.
It stopped partway through, and the reason is worth recording accurately because it is not the reason an outside reader would guess. The blocker was not the entitlement plumbing or the schema. It was requirement gathering on data entry, and attention moved to other products before it concluded.
The unsolved question is how the hierarchy gets populated. Modelling a four-level containment tree is straightforward; capturing one from a real client is not. Someone has to say this complex, then this building inside it, then this floor, then these forty units on it, and every obvious answer is bad in a different way. A form per level makes creating a fifty-unit building a fifty-step task. One deep nested form is unreviewable and loses everything on a validation failure. A guided wizard is pleasant once and painful the second time. Bulk import already exists for flat properties and would need to learn a hierarchy, including how to describe a parent that does not exist yet.
That question is a product-design problem wearing an engineering costume, which is exactly why it deserved requirement gathering rather than a guess, and exactly why it was the right thing to pause on rather than push through.
LEVEL 1 complete
property as a record: address · owner · status
documents · deals · people
LEVEL 2 backend built, paused during requirements
complexes ─▶ buildings ─▶ floors ─▶ units
├ occupancy history
└ resident vehicles
34 routes · capability guard · per-entity permissions
THE OPEN QUESTION how does any of it get entered?
form per level 50 units = 50 steps
one nested form unreviewable, loses all on error
guided wizard fine once, painful twice
extend the importer needs to express a parent
that does not exist yet
designed in schema, commented out, waiting on the above:
leases · fees (pending/paid/overdue/waived)
notifications (channel · status · retry)Roadmap: Reaching Residents
The plan for residents was never a large one. The goal is that a resident receives a notification, that a service fee is coming due, that a payment landed, that something about their unit changed. Everything else a resident portal might do is secondary to that single outcome.
A mobile application delivers it and carries a great deal that has nothing to do with the outcome: two store listings, release cycles, a second kind of principal the permission model has never had, and an authentication surface for people who are not staff. The cheaper path to the same result is WhatsApp, and most of the groundwork is already in place. The client record already stores a WhatsApp number, and a notification subsystem is designed in the schema and commented out, with a channel of WhatsApp or SMS, a status of sent, delivered or failed, an error message, and indexes chosen for a retry sweep. It was written as an audit log of every message rather than as fire-and-forget, which is the part that would have been expensive to add later.
So the sequence is messaging first, application later. WhatsApp reaches a resident who has never installed anything, on a number the client already holds, without introducing a login for a person who does not have one. The application remains in the plan and becomes a straightforward addition once there is something worth opening it for, and the seam it needs already exists: a person record carries an optional link to a login account, so residents can become authenticated users without a data migration.
The same subsystem is what turns the commented-out fee tracking into a product rather than a table. A fee with a due date and an overdue status is only useful if something tells someone about it.
GOAL a resident learns that something happened OPTION A resident mobile app ├ two store listings, release cycles ├ a second principal type the model has never had └ auth surface for non-staff people delivers the outcome, carries a product with it OPTION B WhatsApp ◀── chosen first ├ client already stores a WhatsApp number ├ notification schema already designed: │ channel WHATSAPP · SMS │ status SENT · DELIVERED · FAILED │ errorMessage + status index for retry └ reaches someone who installed nothing the app stays in the plan, and stays cheap: person.userId is nullable, so a resident can become a login later with no data migration
Roadmap: Cleanup Worth Doing First
One smaller item is worth naming because it is cheap and it makes the tier easier to sell. Level 2 (Premium) is currently represented three times: as a flag on the plan, as a pair of archive columns on the client row, and as a catalog entry that maps to no add-on type at all. Every other entitlement in the system flows through one add-on table.
That is why the tier needs its own enable, archive and restore endpoints while everything else shares a generic grant and revoke path. The catalog already advertises a Level 2 upgrade as a purchasable add-on, and that entry exists precisely so a standard client can buy the capability without being moved onto Premium wholesale. It is the one catalog entry that maps to nothing, so today the only way to grant it is to change the client's plan, which also hands them every other Premium ceiling whether they bought it or not.
Giving Level 2 a real add-on type collapses three representations into one, removes three bespoke routes, makes the tier manageable from the operator CLI for free, and turns the advertised upgrade into something that can actually be sold.
It is a day of work and it was not what stopped the tier, but it is the difference between an upgrade being a conversation and an upgrade being a command.
Future Direction
The clearest next step is closing the verification gap. Atlas has the largest codebase in the family and the thinnest automated checking of any product in it, which is the wrong way round. Continuous integration, and coverage extended from the well-tested commercial core out into the domain services, would make every subsequent change cheaper rather than riskier.
The second is adopting what the family learned after Atlas stopped being the newest product. A storage abstraction with a provider boundary, boot-time configuration gates that refuse to start on an unsafe setting, and request-level correlation in the logs were all built elsewhere against production pressure Atlas has not been under. Atlas invented the patterns those products inherited; the traffic is now in the other direction, and taking hardened work back is cheaper than rediscovering it.
The third is writing decisions down. The abandoned list builder is a small loss on its own and a precedent worth not setting: a codebase this size, carrying this much original design, should be able to explain why it looks the way it does. Decision records that state what was chosen, what was rejected and what evidence would reopen the question are the cheapest durable thing on this list.
Technologies
Explore More
Other Case Studies
Enterprise Healthcare Platform
Dentra
A clinic platform built around one fully isolated deployment per customer, and the reference implementation that ZothKit was extracted from.
Internal Enterprise Operations Product
Jinara
Internal platform designed to manage the complete lifecycle of agency operations, from client relationships and contracts to payments, responsibilities, and future project management.