Real Estate Platform
Multi-role property platform: listing lifecycle, agency management, subscriptions and moderation.
How this was measured: Endpoints counted from route decorators, models from the Prisma schema, test files by *.test.ts/*.spec.ts.
- CLIENTS
- emlak_webNext.js
- emlak_adminNext.js
- CONTRACT
- packages/sharedtypes + client
- API
- emlak_apiNestJS · 118 endpoints
- DATA
- PostgreSQL25 models
- Prismamigrations
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
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.
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.
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.
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.
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.