Aryan Kumar

chunk: estatelinkweb workmy product2025

EstateLink

A property management app for UK landlords: properties, tenants with UK checks, leases and a live dashboard. I built it alone over 7 months.

Moving a tenant in: one transaction
  1. sessionOpen a session
  2. check
    • The tenant is yours
    • The property is yours
    • The tenant isn't already assigned
    • The property or unit is free
  3. writeUpdate the tenant and the property
    • commitEvery step passed: both writes land together
    • abortAnything threw: nothing changes
  4. after commitPush fresh numbers to that landlord's dashboard
Source: server/services/tenantAssignmentService.js, lines 23 to 111 and 93 to 101
Role
Solo, 148 commits
Timeline
May to December 2025
Status
Live status unconfirmed
Stack
React, TypeScript, Express, MongoDB
Sources for these facts
  • Role: git log, EstateLink repo, all mine (Dependabot aside)
  • Timeline: git log: first commit 17 May 2025, last 18 Dec 2025
  • Status: server/server.js, lines 58 to 59 name estatelink.live; no response on 15 Oct 2026
  • Stack: client/package.json; server/package.json

The short version

EstateLink keeps a landlord's properties, tenants and leases in one place, and checks tenants against UK lettings rules such as right to rent.

The interesting part is underneath. Moving a tenant into a property changes more than one record, so it runs as a single database transaction that lands completely or not at all.

There's no live link and no app screenshot on this page, and I won't draw a fake one. The diagram at the top is what one assignment does on the server. Further down I say what isn't finished.

For landlords with one house or a block of flats

Landlords, and the people who manage property for them. One might have a single house; another a block of flats with a different tenant and rent in every unit.

Letting in the UK comes with its own paperwork: right to rent, National Insurance numbers, referencing, guarantors. EstateLink models those as real fields instead of leaving them to a notes box. The tenant model alone runs to 1,585 lines.Source: server/models/Tenant.js (line count from wc -l)

Properties, tenants, leases and a live dashboard

Properties
Houses or blocks of flats, with each unit's status, tenant and rent tracked separately, plus amenities, pets policy, financials and a monthly cash-flow figure.Source: server/models/Property.js, lines 181 to 340 and 509 to 526
Tenants
Personal details, addresses, employment and income, guarantor, referencing and vehicles. UK phone numbers, postcodes, National Insurance numbers and sort codes are validated on the way in.Source: server/models/Tenant.js
Leases
Assign a tenant to a whole property or a single unit, unassign them, manage the lease. Each of those is a transaction: the diagram at the top, and the next section.Source: server/services/tenantAssignmentService.js; server/routes/tenantRoutes.js
A live dashboard
Portfolio numbers that update over a websocket, a snapshot saved for every month and kept for 24 months, and month-on-month trends.Source: server/services/realTimeAnalytics.js; server/utils/scheduledTasks.js, lines 41 to 80
Photos
Property photos are resized on the server into four WebP sizes and stored on S3, each user in their own key space.Source: server/utils/imageProcessor.js; server/utils/multiUserS3.js
Accounts and the rest
Register, verify your email, reset a forgotten password. Dark mode, a mobile pass, and API docs served at /api-docs.Source: server/routes/authRoutes.js; client/src/stores/themeStore.ts; pull requests #24 and #25; server/utils/swagger.js

The hard part: nobody half moves in

Moving a tenant in isn't one write. The tenant record has to point at the property, and the property (or the flat inside it) has to point back at the tenant. If the server falls over between those two writes, you get a tenant who thinks they live somewhere that has never heard of them.

So each assignment runs inside a MongoDB transaction. The server opens a session, runs its checks, makes the writes, and only then commits. If anything throws on the way, it aborts, and the database looks exactly as it did before. Assigning, unassigning and the lease operations all work like this: four transactions in all.Source: server/services/tenantAssignmentService.js, lines 23 to 111, 128 to 193, 208 to 272 and 287 to 356

A middleware also runs a consistency check before the request reaches the service at all.Source: server/middleware/tenantConsistencyMiddleware.js, lines 14 to 60

Then telling the dashboard

Each landlord's dashboard keeps a Socket.IO connection open. The server checks their login token (a JWT) during the handshake before it will talk. When a tenant is assigned, it recomputes that landlord's numbers and pushes them only to that landlord's sockets, so nobody sees anyone else's portfolio.Source: server/server.js, lines 160 to 260; server/services/realTimeAnalytics.js, lines 20 to 45

That push happens after the commit, outside the transaction. Why that matters is the decision further down.

Also underneath: Redis, four photo sizes and rate limits

A cache that's allowed to break

Reads go through Redis first, cached for 5 minutes, 15 minutes, 1 hour or 24 hours depending on the data, with keys split per user. Changing a property clears its entries. If Redis errors, the request goes straight to MongoDB instead of failing.Source: server/utils/cache.js, lines 7 to 60; server/controllers/propertyController.js

Four sizes per photo

The server checks the file first, gives up reading its metadata after 10 seconds, and rejects anything over 10,000 pixels. Then sharp makes four WebP versions at quality 80: 300 by 200, 800 by 600, 1200 by 900, and 1920 wide. Abandoned uploads in a temp folder are cleared every 24 hours.Source: server/utils/imageProcessor.js, lines 5 to 60; server/utils/scheduledTasks.js, lines 20 to 40

Layers in front of the API

Helmet with a content security policy, a limit of 100 requests per 15 minutes and 20 for writes, a separate upload limit, an origin check against cross-site request forgery on anything that changes data, NoSQL-injection sanitising and a 10 MB cap on request bodies.Source: server/server.js, lines 54 to 122; server/middleware/rateLimitMiddleware.js; server/middleware/csrfMiddleware.js

CI runs the server tests on every change to main

server tests, Jest and Supertest, in CI
9 files, about 238 testsSource: server/tests/; test count is a grep of it( and test( calls, so approximate
client tests, Vitest, not run in CI
6 files, about 106 testsSource: client test files, same grep method; .github/workflows/ci.yml

One of the server test files is for the assignment service itself, where the transactions live. Others cover auth, properties, tenants, the validators and the live analytics.Source: server/tests/ (tenantAssignmentService, realTimeAnalytics and seven more)

On every push and pull request to main, GitHub Actions lints the server and the client, runs the server tests and builds the client.Source: .github/workflows/ci.yml

I don't have a coverage figure I'd trust, so there isn't one here.

The dashboard push waits until after the commit

Pushing fresh numbers to the dashboard could have gone inside the transaction. I put it after the commit instead. A websocket can drop at any moment, and if the push sat inside the transaction, a dropped connection could roll back a perfectly good tenant assignment.

The cost: if a push fails, the dashboard is a step behind until it refreshes or reconnects, which the client does on its own. A stale chart is annoying. A half-assigned tenant is a mess to untangle.Source: server/services/tenantAssignmentService.js, lines 93 to 101; client/src/hooks/useRealTimeAnalytics.ts, lines 74 to 115

The cache follows the same rule. Redis makes reads faster, but if it goes down the app reads from MongoDB, and the health check reports "Redis not connected" instead of falling over.Source: server/utils/cache.js; server/routes/healthRoutes.js, lines 19 to 25

What isn't finished

  • No live link. The code points at estatelink.live, but I can't confirm it's still running, so I haven't linked it.Source: server/server.js, lines 58 to 59; no response on 15 Oct 2026
  • The landing page promises more than the app does: marketing stats that were placeholders, never real numbers; online rent collection and maintenance work orders, neither built; and three pricing plans with no billing behind them.Source: client/src/components/landing/StatsSection.tsx, lines 10 to 13; FeaturesSection.tsx, lines 103 to 114; PricingSection.tsx, lines 14 to 55; no payment code in the repo
  • The client tests exist, but CI doesn't run them. Only the server tests do.Source: .github/workflows/ci.yml

If I picked it back up, the first jobs would be: run the client tests in CI, take the placeholder stats down, and either build rent collection or stop promising it.

Where EstateLink sits in my index

EstateLink's chunks are ringed and numbered. The dimmed points around them are the rest of that corner of my index.

1 EstateLink2 live websockets
  1. 1EstateLink: property management for UK landlords, built over 7 months.source: EstateLink client landing copy and data models; git log
  2. 2EstateLink: live dashboard numbers pushed over logged-in websockets.source: EstateLink repo, server/services/realTimeAnalytics.js

Query the full index