Designing one app for two people who don't trust each other.
A property management CRM for independent landlords, designed and built solo and shipped to the App Store.
The short version
- Problem
- Independent landlords run 1 to 20 units from a phone, across a spreadsheet, a notes app, a bank app and a text thread per tenant, so the same rent figure lives in four places and no two agree.
- Approach
- I designed and built the whole product solo: a separate portal per role, routed at login rather than one interface hiding fields by permission, and a tenant money flow that never asks anyone to do arithmetic.
- Result
- Shipped to the App Store as v1.0 in March 2026, 74 screens across 2 roles, after clearing five separate guideline rejections in a single pass.
Context and stakes
Independent property managers, people running 1 to 20 units rather than institutions, do the job out of a phone while standing in a driveway. The tooling they actually use is a spreadsheet, a notes app, a bank app, and a text thread per tenant.
The cost of that fragmentation is not inefficiency. It is that the same fact lives in four places and no two agree. What a tenant owes this month exists in the manager’s head, a spreadsheet cell, a Venmo request, and a text. When those diverge, the manager finds out during an argument.
The product brief I wrote for myself put it plainly:
You switch between tools to track tenants, leases, and maintenance. You lose context across properties and units. You manage tasks from a phone while on the move.
Constraints
Three, and they shaped more of the product than any preference did.
No team. No researcher, no PM to argue with, no engineer to tell me a thing was expensive. Every tradeoff in this case study is one I made and then had to build.
No users to test against. The product had no install base before v1, so there were no sessions to watch and nobody to interview.
A gatekeeper who could stop the release, and did. Apple rejected v1.0 on five separate guidelines in a single review pass. Four of them were design and product problems, not engineering ones.
I designed this and I built it, using AI assistance for a large share of the implementation. That is what made a 74-screen native app possible for one person in the time available, and it is why the decisions below are design decisions I then had to live with in code rather than hand off.
Research, and the reframe
This is a solo build with no user base, so there were no interviews and no usability testing before v1. What I had was the product brief, the build itself, and eventually the App Store review record. Where a decision below rests on reasoning rather than observation, I say so.
The reframe is that this is not a workflow tool, it is a system of record. Every competing product for this market is built around helping a manager do tasks faster. The actual failure is that nobody agrees what is owed. So the design goal became one place where the number is the number, legible to everyone with a stake in it. Almost everything below falls out of that, up to and including accepting payment methods the app cannot process.
The complication is that the two roles have opposed relationships to the same data. The manager owns and edits everything, and wants control and speed. The tenant sees only their unit, pays money, and wants not to get cheated. They are not colleagues. They are the two sides of a transaction, and the app has to be credible to both at once.
A tenant and a manager looking at the same rent charge want opposite things from the interface. The manager wants to record it in two taps. The tenant wants to understand why it is that number before paying it.
Explorations
One interface, or two
Rejected: one adaptive interface with permission-based hiding. Cheaper to build, and the standard move.
I rejected it because permission-hiding produces interfaces that feel like a fuller product with pieces missing: the tenant gets a nav bar built for someone else’s job. Worse, it makes leaks a matter of getting every conditional right. One missed check and a tenant sees another tenant’s balance. In an app whose core promise is that the number is the number, that failure is fatal to trust.
Chosen: two separate portals, routed at the root. Role resolves once at login and the user lands in a portal built only for them. The routing guard is deliberately blunt, because being in the wrong portal is a bug, not a state to render around. Row-level security enforces the same boundary in the database, so the split is a usability decision layered on a security guarantee rather than a substitute for one.
Login
Role resolved once, at login
-
Manager portal
- Reports
- Properties
- Tenants
- Payments (designed twice, once per portal)
- Maintenance (designed twice, once per portal)
- Messages (designed twice, once per portal)
- Reminders
- Settings
-
Tenant portal
- Home
- Payments (designed twice, once per portal)
- Maintenance (designed twice, once per portal)
- Messages (designed twice, once per portal)
Row-level security
The same boundary, enforced again in the database rather than trusted to the interface.
A dot marks a concept that exists in both portals and was therefore designed twice. Three of the manager's eight, three of the tenant's four.
The tradeoff I accepted: every shared concept, payments and messaging and maintenance, needed two designs instead of one. That is real duplicated effort, and it is why the shared component layer had to exist. I would make the same call again. The duplication is bounded and visible; conditional-rendering bugs are unbounded and invisible.
What the manager portal opens on
The first build opened on Properties, the obvious choice since properties are the root object in the data model.
I changed it while using the app myself. A manager with six properties does not open the app to browse a list they have memorised. They open it to answer one of two questions: did the money come in? or what is on fire? A property list answers neither, and charges a tap before any information arrives.
So the portal opens on Reports, which answers the money question above the fold, and the notification bell carries the other. Properties moved one tap away, where browsing belongs. This is reasoning from my own use rather than from observed behaviour, and it is the first thing I would put in front of real managers.
The solution
Manager: eight tabs, three levels deep
Eight tabs is more than iOS convention encourages and more than I wanted. The role genuinely spans eight unrelated jobs, and burying any of them behind a More menu costs a tap on something done daily.
Depth is capped at three levels throughout: list, detail, edit. When a flow threatened a fourth, it became a modal instead.
Property setup discloses progressively, because it is the heaviest form in the app and the first thing a new manager hits. The first screen asks only what makes the record real (name, address, status) with units, leases and tenants added afterward from the property’s own detail screen. Address entry uses autocomplete rather than five free-text fields, because typing a full address on a phone in a driveway is exactly the friction that sends someone back to the spreadsheet.
Tenant: never make them do the math
The tenant portal is the part of this product I would defend hardest. Four tabs, one job, and unusually sharp constraints: a non-technical user, on a phone, about to move real money, who has every reason to be suspicious.
The home screen leads with a single card: the total, the due date, and the status. Nothing competes with it. Underneath sits a breakdown toggle (base rent, monthly fees, late fee if applicable) collapsed by default.
The collapse is the decision worth explaining, because I built it expanded first. Showing the itemisation by default is arguably more transparent. But it means every tenant does arithmetic every month to confirm a number the app already knows, and most months rent is just rent. Leading with the total respects that, and a tenant who does want to check, the month a late fee appears, is one tap away, with the itemisation never hidden behind a support request.
Payment methods, including two the app cannot process
The obvious build is card-only. One integration, shortest path to a working payment. I built four.
Credit card carries a 3% convenience fee. Bank transfer has none. Cash is arranged with the manager. Zelle goes through the tenant’s own banking app.
Two things are happening there.
The fee is stated on the choice, not after it. A tenant picking a method is deciding how to spend money, so the 3% belongs at the moment of choosing rather than on a confirmation screen after they have committed. It also makes bank transfer visibly the cheaper option, which is the option that costs the manager less: a rare case where the honest disclosure and the business incentive point the same way.
Cash and Zelle are in the app even though the app cannot process them. A large share of independent-landlord rent is paid by check and Zelle. An app that only accepts cards does not convert those tenants to cards. It just stops knowing about their rent, and the manager is back to a spreadsheet for half their units. The core promise dies right there.
So offline methods are first class. The tenant records the payment and it enters the ledger as pending until the manager verifies it. The app gives up processing the money in order to keep being the system of record for it. Being the place where the number lives matters more than being the place the money moves through.
Worth naming as unfinished: check payment is half-built. The schema and the manager-side verification handle it, but a tenant cannot select it yet. Check is a large share of independent-landlord rent, which makes this the most valuable gap in the tenant flow rather than a cosmetic one.
Reports: making the money legible
Reports replaced a stats dashboard that showed counts, total properties and occupied units. Those look like information and answer nothing, because a manager already knows how many properties they have.
The redesign answers one question: did I make money this month, and where did it go? Three summary cards, three tabs, filters by property and date range, and export to PDF or CSV.
Export exists because of a boundary I set early: this is not accounting software. It does not do double-entry bookkeeping and it does not file taxes, and trying would have made it a much worse product. It holds clean records and hands them to the accountant who does. Export is the seam where the product admits its own edge.
Removing recurring work
Reminders for rent, lease expiry and expenses fire on a schedule, with a snooze sheet rather than a binary dismiss. Snooze matters: “not now” is the honest answer to most reminders, and an interface offering only deal with it or make it disappear trains people to dismiss everything.
Receipt scanning photographs a receipt and extracts vendor, amount, date and a suggested category. Expense entry is the most tedious and most-skipped task in the product, and its failure mode is not a wrong expense, it is no expense record at all and then reports that lie. Extracted values land in an editable form rather than saving silently, so a bad read is a correction rather than a corrupted record.
The system holding both portals together
A shared primitive layer is the structural answer to the duplication cost the routing decision accepted. One token set with full light and dark palettes, and one primary blue across both portals, deliberately: they differ in what they show, not in feeling like different apps. Status is named by meaning rather than hue (occupied, vacant, maintenance, success, warning, error).
Two components carry more weight than their size suggests.
Empty states are the real onboarding. Every list can be empty, and for a new manager most lists are empty at once. Each one names what goes there and offers the action that creates the first one.
The offline banner exists because this app is used in basements and stairwells, where connectivity is a normal condition rather than an error. It also shipped a false-positive bug, appearing when the device was fine, which is worse than having no banner: it teaches people to distrust the one signal meant to tell them whether their data is real.
Shipping it
Five App Store rejections in one pass
v1.0 came back rejected on five separate guidelines, reviewed on an iPad. Four of the five were design and product problems.
| Guideline | Issue | Fix |
|---|---|---|
| 2.1(a) | Crash when adding a property | Deferred payment SDK init, anchored the action sheet |
| 3.1.1 | No Restore Purchases control | Explicit button on the paywall and both settings screens |
| 5.1.1(v) | No account deletion path | In-app deletion flow with double confirmation |
| 3.1.2(c) | Missing subscription metadata | Price, period, auto-renewal terms, terms and privacy links |
| 2.3.10 | Google Play references in an iOS binary | Removed |
Two taught me something.
Restore Purchases. The app did restore purchases, automatically, on launch. I had removed the manual button deliberately, reasoning that automatic restoration is strictly better than making someone hunt for a button to get back what they already own.
Apple rejected it anyway, and the guideline is explicit that auto-restore does not satisfy the requirement. The principle I had missed: a user who has lost access needs a control they can press. Automatic recovery that fails is indistinguishable from no recovery at all, because the user is left with no move to make. The button is not primarily a mechanism, it is the visible affordance that recovery is possible. Invisible automatic behaviour is only good UX when it works.
Account deletion was a genuine omission. I had built the entire account lifecycle except its ending. Deletion is easy to skip because no roadmap asks for it and no user requests it until they want to leave, and by then they cannot tell you.
The crash is worth one more note, because a design decision caused it. Apple tested on an iPad, where the app’s modals render as form sheets. Add Property crashed on a missing location permission: the address autocomplete, the convenience feature meant to reduce typing friction, requested location without the usage description declared. A feature added to save a tenant thirty seconds of typing became the crash that blocked the release. The fix declared the permission, made the hook degrade to manual entry when someone denies it, and forced full-screen modals on iPad.
All five were fixed in a single pass and the app shipped.
The third role I decided not to build
There is an obvious third user for a product like this: the owner, the person who holds the property and wants proof the manager is doing the job. It exists in the data model and as a pricing tier. It was never designed, and it did not ship.
That was a decision rather than an omission. A manager and a tenant form a closed loop: one collects rent, the other pays it, and the product is complete with just those two. An owner is a third party to a transaction that already works without them, and everything they would see is read-only, which makes it build cost with no effect on whether the core loop functions. Two roles that genuinely need each other is a product. Three roles where one is a spectator is a roadmap.
Being honest about the current state: because the role exists in the schema, an owner account today is signed out at login rather than shown an explanation. That is a rough edge, not a design.
Pricing
Three tiers, gated on the constraint that actually correlates with getting value from the product: portfolio size. A landlord with one unit uses it free forever, which is intentional. They are the least able to pay and the most likely to recommend it. Paying starts when the product is demonstrably carrying real work.
Locked features stay visible but marked rather than hidden, because a user who cannot see a feature cannot want it.
Results
-
74 screens shipped solo
Across two roles and two portals, designed and built by one person.
-
5 guideline rejections cleared in one pass
Four of the five were design and product problems rather than engineering ones.
-
4 tenant payment methods, two of them offline
The app gives up processing two of them in order to stay the record of all four.
Live on the App Store since March 2026.
There are no usage figures yet: no installs, no active managers, no payment volume. Naming that is more useful than a number I cannot source, and closing it is the third item below.
See Lnked on the App Store (opens in a new tab)What I’d do differently
Validate the Reports-first decision. It is the highest-leverage assumption in the manager portal and the cheapest to test. Five managers and a first-run observation would settle it.
Fix the accessibility gaps. The reports cards distinguish income from expense by colour alone, which fails for roughly one man in twelve, and screen-reader labelling and dynamic-type support have not been audited. The colour issue is a defect, not a nice-to-have.
Instrument the payment funnel. The tenant portal has explicit success targets, payment success above 97% and median time-to-pay under 30 seconds, and no analytics to measure them against. Targets you cannot measure are not targets.
What I’d tell someone building this next
Route by role, do not render by permission. Two deliberately separate portals cost real duplicated work and bought a guarantee no conditional could: a tenant cannot see another tenant’s balance, because the screens that show it do not exist in their app.
Support the payment methods people actually use, even the ones you cannot process. Being the system of record is worth more than being the payment processor.
Put the cost on the choice, not the confirmation. Fee disclosure belongs where the decision is made.
Automatic recovery still needs a button. Invisible behaviour is only good UX when it works. When it fails, the user needs something to press, and this cost me a rejection to learn.
Design the ending, not just the onboarding. Account deletion is invisible on every roadmap and required by the platform. The people who need it cannot ask you to build it, because by then they are already gone.