Skip to content
Ercan Labs
← All workPLATFORM · Own product · in-development

Real Estate Platform

Multi-role property platform: listing lifecycle, agency management, subscriptions and moderation.

118
API endpoints
25
Prisma models
56
Test files
521
TS files

How this was measured: Endpoints counted from route decorators, models from the Prisma schema, test files by *.test.ts/*.spec.ts.

Architecturerole gate on every endpoint
  1. CLIENTS
    • emlak_webNext.js
    • emlak_adminNext.js
  2. CONTRACT
    • packages/sharedtypes + client
  3. API
    • emlak_apiNestJS · 118 endpoints
  4. DATA
    • PostgreSQL25 models
    • Prismamigrations
Both clients read one contract; a renamed field reaches both from the same source.

Problem

A property listing in Turkey is not one form. Owner, agent, agency and buyer work in the same pool with different permissions. Publishing a listing, assigning it to a consultant, charging a subscription quota and passing moderation are all connected.

This is not a client delivery, it is my own product. The difference: scope was set by "who maintains this in two years" rather than a client request.

Constraints

  • Four roles on one data model: owner, agent, agency manager, super admin.
  • Every user-facing string in TR/EN; identifiers in code stay English.
  • Subscription and quota rules are tied to listing creation; a check added later leaks somewhere.

Architecture decisions

01

Turborepo monorepo, three apps, one contract

emlak_api (NestJS), emlak_web (Next.js), emlak_admin; shared types in packages/shared. When API field names change, both clients are affected from the same source.

Cost: Monorepo tooling (turbo, shared tsconfig) costs setup and CI time.

02

Data model first: 25 models, relationship-centred

Listing state is not squeezed into one status field; publication state, subscription quota and moderation decision are separate relations. "Why is this listing not visible?" becomes a traceable chain.

Cost: More queries; in return the moderation history never disappears.

03

Authorization enforced at the boundary

Every endpoint goes through a role gate; the admin panel is a separate app so the super-admin surface is not shipped inside the public client.

Cost: Shared form logic is written twice.

04

Security claims proven by code

Every item in the security document is tagged with file:line and marked applied / partial / missing. The document does not say "done", it shows where.

Cost: Documentation upkeep; in return the claim is auditable.

05

i18n from day one

All user text comes from translation keys; TR default, EN second.

Cost: Every string lives twice; in return English is not a later translation project.

Proof

  • API endpoints118
  • Prisma models25
  • Test files56
  • TypeScript files521 (api 312 · web 154 · admin 55)
  • Docs12+ architecture/security/deploy documents

Known limits

  • Production load and real user behaviour are not measured — this is my own product, so there is no user-count claim.
  • Payment provider integration is environment dependent; test coverage limited to sandbox.
  • Source is in a private repo; this is an engineering account, not proof.

What I learned

Moderation is not a button, it is a history. "Reject listing" is one line; "why was it rejected, who decided when, was it appealed" depends on the data model. Writing the decision onto the listing is easy, but a month later that field answers nothing.

Stack

NestJSNext.jsPrismaPostgreSQLTypeScriptDocker