OpenLet
Free, self-hosted property and tenancy management for small landlords
- Role
- Creator & Sole Developer (Open Source)
- Dates
- Nov 2026
- Status
- Live
- Next.js
- NestJS
- PostgreSQL
- BullMQ
- TypeScript
- React
- Prisma
- Redis
- Zod
- Tailwind CSS
- shadcn/ui
- PDFKit
- sharp
- Vitest
- Docker
- GitHub Actions
Summary
Every version 1 feature in the plan is built: properties, tenancies, an append-only rent ledger, repairs with contractor links, a compliance board, a tenant portal and reports. 115 API tests pass in CI against real PostgreSQL and Redis.
The problem
Most UK landlords own one to a few properties, and many small letting agents manage fewer than 50. They run everything on spreadsheets, WhatsApp, email and paper. Things get missed: late rent isn't chased, repairs drag on, certificates expire and deposits aren't protected in time. Full property management systems charge a monthly fee per property and are built for bigger agencies. OpenLet is a free alternative that a small landlord or agent runs on their own server and can use from day one.
My role
Solo project. I own the whole project. I wrote the PRD, feature list and build plan, and built the API, web app, tenant portal, demo, Docker setup and CI.
What I built
- Wrote the PRD, a prioritised feature list and a 10-phase build plan with the data model, endpoints and pages.
- Built an append-only rent ledger in integer pence, with charges made by a pure schedule function and payments allocated oldest first.
- Built repairs end to end: tenant reports with photos, priorities, and contractor links stored only as a hash.
- Built a compliance board and reminder emails that are claimed in a table before sending, so each one goes at most once.
- Built the phone-first tenant portal, the dashboard and reports, PDF statements, CSV downloads and a full data export.
- Set up Docker self-hosting, demo mode with one-click logins, a secret-scanning pre-commit hook and CI.
Architecture
A pnpm and Turborepo monorepo: - apps/web: Next.js (App Router), with the landing page, staff app, tenant portal and contractor job page. It only displays data, and forwards /api/* to the API so cookies stay first-party. - apps/api: NestJS with Prisma on PostgreSQL. Every rule, permission and query lives here. A global guard denies every route unless it is marked public, for tenants or for given roles. - Worker: BullMQ queues for emails, rent charges, PDFs, CSVs, exports, photo conversion, reminders and retention. Scheduled jobs run on London time. - packages/shared: Zod schemas and types used by both apps, plus pure rules for dates, money and statuses. Redis holds the queues, rate limits and the dashboard cache. docker compose runs the web app, API, worker, PostgreSQL and Redis.
Key decisions and trade-offs
Chose: an append-only rent ledger in integer pence. Charges, payments, credits and reversals are only ever inserted, and a mistake is fixed with a reversal plus a new entry. Because: rent money must never be wrong. If nothing is updated or deleted, the history always adds up and can be audited. Chose: to work out which charges a payment paid from the ledger each time (oldest unpaid charge first), instead of storing it. Because: it is cheap for a small portfolio, and a reversal can never leave old allocations behind. Chose: to send the chosen workspace in a header, and check the caller's membership or tenant record in that workspace on every request. Because: one person can be staff in one workspace and a tenant in another. Every query is then scoped by the workspace the guard checked, never by an id from the request body. Chose: contractor links made from a random token, with only its SHA-256 hash stored, scoped to one repair. Because: contractors have no account. A copied database can't open jobs, and a link stops working 7 days after the repair closes (by default), when the repair is reassigned, or when it is withdrawn.
Hardest problem and how I solved it
Rent charges have to appear by themselves, and a daily job must never charge a period twice. A pure function turns a tenancy's start date, due day, frequency, rent history and end date into the same list of periods every time. Monthly rent due on the 29th, 30th or 31st falls back to the month's last day, so the 31st becomes 30 April or 28 February, then goes back to the 31st. Weekly and four-weekly rent counts 7 or 28 days from the start date. A rent change applies to every period that starts on or after its date. The job creates charges up to 14 days ahead, so tenants see the next one coming. A unique key on (tenancy, type, period start), with inserts that skip duplicates, makes it safe to run as often as you like. Ending a tenancy early never deletes anything: charges for periods after the last day get reversal entries. 13 tests cover this, including leap years, a start part-way through a month, rent changes, end dates and running the job twice.
Testing and quality
115 API tests in 15 files (Vitest and Supertest) run against a real PostgreSQL database and Redis, not mocks. They cover sign-in and refresh tokens, every role and workspace rule, tenant isolation, contractor link scope and expiry, rent charge generation, payment allocation and balances, reminders, compliance status, uploads, demo mode and anonymising old data. One test checks that every non-public route refuses a request with no session. CI (GitHub Actions with PostgreSQL 18 and Redis 7) runs a secret scan, typecheck, lint, tests, build and pnpm audit. A second job builds the Docker images, starts the full stack and checks the web app and API answer.
Results
- 115 API tests in CI against real PostgreSQL and Redis
- 13 tests for rent charge dates, including the 29th–31st
- 10 of 10 version 1 build phases done
Every version 1 phase is built and CI passes on main. OpenLet is open source under the AGPL-3.0 licence, and the public demo resets every night. Try the live demo: press "Try as landlord" or "Try as tenant". No password is needed, and everything in it is made up.
Key features
- Properties split into units (flats in a block, rooms in an HMO), with status worked out from the tenancies.
- Tenancies with joint tenants, monthly, weekly or four-weekly rent, rent changes, notice, guarantors and deposits.
- Rent ledger: automatic charges, part and over payments, credits, reversals, arrears by age and PDF statements.
- Repairs with tenant photos, priorities and a secure no-login job page for contractors.
- Compliance board for gas safety, EICR, EPC, alarms, licences and insurance, with reminders at 60, 30 and 7 days.
- Phone-first tenant portal: balance, next rent, repairs, shared documents and messages.
- Dashboard, calendar, reports, CSV downloads and a full data export.
- Owner, Manager and Staff roles, team invites, an audit log and several workspaces per person.
- Photo metadata stripped, private files, and former tenants anonymised after 6 years by default.
Out of scope
Version 1 leaves out payment processing, bank feeds, accounting, tenant referencing, listings, a portal for property owners, native apps and e-signatures. Renewals, inspections and inventories are planned for a later phase.
