PropertyOS
A multi-tenant rental management SaaS, built solo end to end, from the access model to the deploy pipeline.
ReactRTK QueryNode.jsExpressPostgreSQLRedisAwilixZodVitest
- Three separate actor domains — platform operator, landlord, landlord admin — each with its own JWT secret, middleware chain and blast radius.
- No public registration endpoint anywhere: every account is provisioned from the inside, which makes the access model the security model.
- Sessions built to survive a reload and a breach: hashed refresh tokens, rotated on every use, in httpOnly cookies, mirrored into Redis so they can be revoked server-side.
- TOTP multi-factor auth with mfa-pending token gating, password expiry policy and double-submit CSRF protection.
- Support impersonation designed as a reusable pattern: a generic target_type/target_id audit trail, so adding a second impersonation target is additive rather than a rewrite.
- Tenant isolation enforced at the query layer: a cross-tenant read returns 404, never 403, so probing IDs reveals nothing about what exists.
- Unit schemas defined as data by the operator, not hardcoded per property type, so new property types onboard without a migration or a new form component.
- Repository → Service → Controller under Awilix dependency injection, 24 versioned migrations, and a GitHub Actions pipeline running Vitest and Supertest against a fresh database.






Try it yourself
Two landlord logins are published deliberately. Sign in as one and try to reach the other's properties. The isolation is the point.
| Landlord admin — Nürnberg Wohnbau Demo GmbH | demo.wohnbau@mypropertyos.deDemoWohnbau2026! |
|---|---|
| Landlord admin — Roth Gewerbepark Demo GmbH | demo.gewerbepark@mypropertyos.deDemoGewerbe2026! |
Platform-operator access, which includes the impersonation flow, is available on request.
Demo data is shared and writable, and the free-tier backend sleeps when idle, so the first request after a quiet spell can take up to a minute.
Written about this project
- Architecture · Part 1 · 6 minThe SaaS platform with no sign-up form
- Architecture · 1 minWhy I added dependency injection to a project only I would ever touch
- Security · Part 2 · 5 minThe property list that returns 404 instead of 403
- Architecture · Part 5 · 5 minA form that doesn't know what it's asking
- Security · Part 3 · 9 minBorrowing an identity safely: how HQLead impersonation works
- Security · Part 4 · 8 minThe bugs that only show up under load
- Security · Part 6 · 6 minThe session that a reload used to erase
- Architecture · Part 7 · 4 minHQLead: why the operator is a different system
- Design · Part 8 · 8 minWhat comes after Units, and why the next table isn't Tenants
- Security · Part 9 · 7 minThe bug that only existed in production