DoorOps: Building an AI Property Manager on a Real Rental Portfolio
How this was put together: reconstructed from the actual agent working sessions behind DoorOps, not a diary kept at the time. Dates are approximate, pieced together from file timestamps and dates mentioned in-session. It covers roughly five weeks, worked part-time around a full-time job. One detail has been generalized rather than spelled out: an early credential-handling incident is described by its outcome, not its specifics, since naming exactly what was exposed serves no one at this point.
Key milestones
- Day 1Leasing compliance engine proven 7/7 on a six-prospect simulation before a single real text was sent
- Day 1Live on doorops.io — same session, then multi-tenant self-serve signup shipped same-day
- Weeks 2–4Feature growth: Insights (3 rebuilds), business SMS line, rent-matching fix, team invites (5-bug saga)
- Late JuneSandbox sync bugs ship a broken build to production twice — triggers a full evaluation-first rebuild decision
- Early JulyRebuild plan signed off; Slice 0 ships fail-closed tenant isolation + finds a silently-broken migration
- Slice 4Money path hardened — single source of truth for every payment split
- AutopilotPropose-first ops agent ships: earns autonomy gradually, never on pricing
- Classic SunsetOld UI fully retired after all 10 remaining workflows rebuilt natively
- Slice 7bills@ email ingestion pipeline — catches a silently-broken Gmail filter
- Console C1Single command registry ships — one path for any plain-English change
- Jul 15First real brand mark (avatar + banner) and a proper contact email
The tool stack, end to end
The honest starting point
Before there was any software, there was a manual, text-message-and-spreadsheet process: a tenant gives notice, a unit gets re-listed by hand, prospective renters get told to text a personal cell number, a driver's license photo gets requested before anyone gets a lockbox code, a screening decision gets made by eyeballing a credit/background report, a lease gets built by copy-pasting the last tenant's Word doc, and rent collection happens by periodically checking a banking app. Rent-collection follow-up was inconsistent, and the unpleasant parts — chasing overdue rent, eviction notices — got avoided more often than they should have.
That workflow, described step-by-step in the very first build session, became the actual product spec. Every module in DoorOps maps to a real step in a real, previously-manual process the founder was running on his own properties.
Phase 0: proving the hard part first
The first build session didn't start with a landing page. It started with the single riskiest piece of the whole idea: can an AI agent legally and safely run a leasing conversation over text — screen a prospect, collect ID, decide whether to release a self-guided tour code — without creating fair-housing or privacy liability? Everything else was explicitly deferred until that question had a real answer.
The architecture decisions from that first session turned out to be the ones that stuck: a thin shared "shell" (auth, database access, a shared AI agent loop, a compliance core, an encrypted-PII store) that individual product modules plug into through a registry, so adding a second module later would be additive rather than a rewrite. Compliance and PII handling were built as shell-level platform features from day one — the reasoning was that "compliant AI for landlords" needed to be true everywhere, not just in the leasing flow.
Before any real text message was ever sent, the leasing engine was proven against a scripted six-prospect simulation: a clean qualifier, a below-criteria applicant (who must be invited to apply, never rejected outright), someone who volunteers protected-class information (logged as ignored, never acted on), a hostile prospect demanding an immediate decision (escalates to the owner rather than letting the AI decide), an in-person tour no-show, and a blurry-then-clear ID photo. All six passed offline with no API keys at all, then again live against a real model and a real database — 7 of 7 scenarios green. Running it live earned its keep: it surfaced two real bugs (losing track of whether an ID was already on file across turns, and a hostile "just decide" demand failing to escalate), both fixed and re-verified the same day.
Two things worth being honest about from this window: the product didn't have its name yet ("DoorOps" was chosen mid-session), and a set of credentials was briefly exposed in chat while wiring up the database. It was caught immediately, nothing was used, and everything affected was rotated the same day — the kind of mistake that's easy to make once and a hard rule not to repeat.
By the end of that first sprint, a second module — Rent Collection — existed as a real (if shallow) shell, alongside a generic per-unit lifecycle state machine (occupied → notice → turn → listed → leasing → lease-out → payment setup) and a pipeline board visualizing every unit moving through it. That generality was deliberate: even though the only real user was the founder's own portfolio, it was built to work for any landlord managing 4–200 units, because the intent from day one was a sellable product, not a personal script.
Going live
The same initial stretch carried through to an actual public deployment: a login gate, a GitHub repo, a Vercel project, and a purchased custom domain. DoorOps went from code on a machine to live on doorops.io within that first work session — and it didn't stay single-tenant for long. Anyone could sign up, get their own isolated workspace, and start a free trial, built and shipped same-day.
Two rough edges surfaced right after launch, both fixed within hours: sign-up and sign-in went completely blank on the real domain because the app was still running development auth keys, which silently refuse to load outside localhost. And Google sign-in initially failed because a reused OAuth client from an entirely different project didn't have the new domain's callback URL authorized.
One deliberate decision from this period: when asked to "auto-pull" listing and accounting data from popular landlord sites, the answer was no. None of them expose a public API for a landlord's own data, and scraping them would violate their terms of service and risk the founder's real accounts. The product shipped a manual entry wizard with address auto-parsing and CSV import instead — slower, but honest about what was actually possible.
The growth phase: real features, real bugs
Over the following weeks, a long list of genuinely useful features shipped on the original ("classic") architecture:
- Property and unit management grew from a single form into inline editing and bulk creation, fixing a real data-modeling bug where a 4-unit property was only creating one unit record.
- AI lease-document extraction: uploading a lease PDF auto-fills a new property's fields and flags whether rent looks above/below market.
- An Insights tab went through three real versions — a cumulative collected-vs-target chart, then a rebuild grouping history by building with manually-editable cells, then a third pass adding multi-year history, a full Excel export/import round-trip, and trend arrows.
- A dedicated business SMS number went through carrier compliance registration — the first attempt was rejected because of leftover configuration from an unrelated earlier project.
- The rent-matching engine's real bug: bank deposits were matched to tenants mostly by dollar amount, with the payer's name as a tiebreaker — except one payment platform truncates surnames to roughly five characters, which silently broke the name check. On a portfolio with several units at identical rent, that collapsed matching to a coin-flip. The fix matched on name-token prefixes instead of exact substrings and weighted the name signal above the dollar amount, verified against a full month of real deposits before shipping.
- Team access turned into a five-bug saga: a silent no-op invite, an email that went to spam, an allowlist wrongly blocking legitimate invites, a stuck half-created account, and a resend dead-end — each root-caused and fixed in turn.
- A Q&A learning loop for the leasing assistant: when a renter asks something outside its configured facts, it tells them it's checking, texts the owner, and once he replies, relays the answer back and saves it permanently.
- A Reports / tax pack: income statement, per-property net income, monthly cash flow, and a Schedule-E-shaped export, deliberately excluding uncategorized transactions from totals rather than guessing.
The roughest patch of this period was a deploy saga around the Transactions page: real fixes simply stopped reaching production, and diagnosing why took several wrong turns — a paused-billing flag silently blocking deployments, confusion over which commit had actually redeployed, a red herring from an already-fixed build failure, a genuine dependency gap, and finally the real cause: a cron-schedule change had quietly violated the hosting plan's once-a-day cron limit, silently blocking every subsequent deployment. That prompted an explicit course correction — from that point on, the actual hosted build result, not a local check, would be treated as the source of truth.
The unglamorous foundation: reconciling years of real history
Running in parallel with feature work was a much less glamorous project: making years of the founder's actual portfolio history trustworthy enough to automate against. This meant physically cleaning up around a hundred scattered lease documents into an organized archive, then a multi-round reconciliation cross-referencing signed leases, historical bank deposits, and a formal short-term-rental income report against an evolving master spreadsheet.
That reconciliation surfaced and fixed real data-quality problems: a tenant's payment history attributed to the wrong unit, a "mystery vacancy" that turned out to be short-term guests rather than a missing tenant, and at least one phantom tenant record that dissolved once the real signed lease was located. It established a clear precedence order for conflicting sources (signed lease beats bank data beats a listing export beats the prior spreadsheet) and modeled short-term rental income as its own category rather than letting it read as vacancy.
Mounting technical debt, and the decision to rebuild
By late June, a pattern had repeated often enough to become its own theme: the sandboxed editing environment sat behind a synced folder that did not reliably propagate writes or reads. Files would occasionally appear truncated or stale when read back — and, more dangerously, a couple of times a commit was made from that stale view and shipped a broken build straight to production. Both were caught and fixed, but each incident sharpened the same lesson: never trust the sandbox's copy of a file that had just been edited, and never run git from inside it.
Against that backdrop, the founder asked for something bigger than another feature: a dedicated, evaluation-first pass across the entire codebase, the original spec, and the live site. That request became the hinge point of the whole project — the "classic" architecture's organic growth had started to cost more, in silent bugs and deploy fragility, than it saved.
The rebuild
The evaluation ran as several parallel research passes — product alignment, code and architecture, data-model consolidation, UX and accessibility, reliability and compliance, and competitive research — synthesized into a single findings report and a ranked rebuild plan, signed off in early July. From there the rebuild shipped as a numbered sequence of "slices," each verified and pushed independently, deliberately preserving all the correct existing money-path and leasing logic rather than rewriting it from scratch.
An earlier migration adding an "owner message memory" feature had, in fact, never been applied to the live production database at all — meaning that feature had been silently doing nothing for weeks, because a try/catch swallowed the resulting database error instead of surfacing it.
Slice 0 hardened tenant isolation: fail-closed account provisioning, scoping every lead-related action to the correct tenant, and a scheduled PII-purge job. Running its migration surfaced the finding above — a live, real-world example of exactly the kind of silent-failure risk the evaluation had been commissioned to find.
Around this point, two pieces of infrastructure came out of a conversation about closing the loop on manual copy-pasting: a proper database connector, and a small local "bridge" script that watches for a request file, clears any stale git lock, strips corrupted bytes, runs the full verification suite, and only commits and pushes if that suite is green. The first version of that script broke immediately — a plain-ASCII requirement choked on an em-dash character — fixed by rewriting it pure-ASCII, which then became a hard rule for every script in the project.
Slice 4 hardened the money path: a database trigger became the single source of truth for how much of a charge had been paid, after reconnaissance found three separate, subtly different code paths that could apply a payment split — consolidated into one atomic function.
Autopilot — the project's most ambitious slice — began with the founder dictating a genuine product vision: an agent that runs continuously, works to minimize vacancy time, makes sure rent gets collected in full, keeps transactions classified correctly, and otherwise interacts over text — prioritizing what matters, asking permission before acting, and learning from his answers so it needs to ask less over time. What got built matched that brief under a "propose first, earn autonomy" model: a deterministic daily rules sweep, a rule-miner that proposes a new categorization rule only once it's seen the same pattern repeat, and a trust system where three unprompted approvals earn standing permission, one decline revokes it instantly, and price changes are hard-coded as never eligible for autonomy no matter how many times they're approved.
The Classic Sunset. Asked to fully retire the original UI, the first honest answer was "not yet" — an audit found ten real workflows that only existed in the old interface. The resulting plan was a phased sunset: rebuild each of those ten first, keep the old one fully working the entire time, and only delete it — after screenshotting every old page for the record — once nothing depended on it anymore. Every financial report was cross-checked against the old interface's own numbers before that code was deleted, to make sure nothing shifted in the process. It matched exactly.
A hard safety gate, born from a real scheduling need. Wanting to market a unit for rent before its current tenant had actually moved out prompted a new, deliberately strict rule: a unit can be marked "pre-leasing" and the assistant will answer questions about it, but a separate, explicit "tourable from" date gates both releasing a self-guided access code and booking a tour — and that gate fires before every other check and fails closed if the date can't be evaluated. The reasoning was stated plainly: releasing a door code early means a stranger could walk in on someone who still lives there.
The Command Console. Asked to build an in-app chat window where a plain-English instruction would execute a change directly, the decision was to generalize an existing SMS-only command system into a single, shared, typed action registry that both the web console and the SMS assistant would eventually use — reasoned through explicitly: two systems able to write to the same live tenant and money data will eventually drift, and one of them will quietly corrupt something. Its design keeps the model's role narrow, always shows a preview before anything is written, requires explicit confirmation, and logs a full before/after audit trail with the ability to undo.
Open issues
- Onboarding wizard for a landlord who isn't the founder was retired during the rebuild and hasn't been replaced yet
- E-signature lease sending is built and verified but not yet tested against a real, live tenant signature
- No brand identity existed until today — the product was text-wordmark-only for its entire life so far
Upgrade opportunities
- Rebuild a general-purpose onboarding flow so DoorOps can take on landlords beyond its own portfolio
- Expand Autopilot's autonomy tiers as more action types earn three-strikes trust
- Extend the command registry from the web console to the SMS transport (planned for a later slice)
Reflecting on the effort
Everything above happened in roughly five weeks, worked part-time around a full-time job, on a product whose only real user for its entire life so far has been the founder's own 22-unit rental portfolio — meaning every bug described here was, at the time, a bug in the software actually running his business. There was no separate staging environment standing in for reality. Real production incidents happened, and each one produced a concrete, lasting fix rather than just an apology — which is arguably the actual throughline of the whole project.
globals.css (deep teal, clay, warm paper) and a text wordmark. The avatar and banner shown at the top of this post were built directly from those tokens, saved into the repo's own public/brand/ folder as SVGs so they render crisply at any size. The product also picked up a proper human-facing contact address today, separate from its automated bill-ingestion inbox.