A modern, scalable backend API for real estate transactions built with NestJS and PostgreSQL
- User Management - Registration, authentication, and profile management
- Property Listings - Create, manage, and search property listings
- Transaction Tracking - Record and track real estate transactions
- Tax Strategy Suggestions - Store informational, non-binding tax structuring suggestions for transactions
- Document Management - Store and manage property-related documents
- Role-Based Access Control - USER, AGENT, ADMIN roles with route protection
- Clean Architecture - Modular, testable, and maintainable code structure
- Fraud Detection - Login and listing risk rules with alerts and auto-block
- Search - Filters, facets, autocomplete and privacy-aware analytics
- Real-time Notifications - WebSocket, in-app and SMS delivery
- Blockchain Recording - On-chain transaction recording
- Operations - Scheduled backups, audit retention, Redis caching, Prometheus metrics, K8s health probes
- CI/CD Pipeline - Lint, migration safety, tests, coverage and build jobs (see Deployment & CI)
The application implements comprehensive RBAC with three user roles:
- USER: Default role for registered users. Can create properties and manage their own data.
- AGENT: Can manage properties and assist with transactions.
- ADMIN: Full system access including user management, property administration, and system configuration.
Routes are protected using decorators:
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.ADMIN)
@Get('admin/users')
getAllUsers() {
// Only admins can access
}New users are automatically assigned the USER role upon registration.
The application provides secure password reset functionality via email:
- Request Reset: User submits email address
- Token Generation: Secure reset token created (expires in 1 hour)
- Email Delivery: Reset link sent to user's email
- Token Validation: Token verified on password reset
- Password Update: New password hashed and stored
# Request password reset
POST /auth/password-reset/request
{
"email": "user@example.com"
}
# Reset password with token
POST /auth/password-reset/reset
{
"token": "reset-token-here",
"newPassword": "NewSecurePassword123!"
}- Token Expiration: Reset tokens expire after 1 hour
- Single Use: Tokens can only be used once
- Password History: Prevents reuse of recent passwords
- Rate Limiting: Previous tokens invalidated on new request
- Blocked User Protection: No emails sent to blocked accounts
- Node.js >= 18.0.0
- PostgreSQL >= 14
- npm >= 10.0.0
- Redis >= 6 (cache, WebSocket presence, BullMQ queues)
# Install dependencies
npm install
# Copy environment file
cp .env.example .env
# Set up your database URL in .env fileA production Dockerfile is included and docker-compose.yml wires up the
full stack (app, postgres, pgbouncer, redis).
# Build and boot the full stack
docker compose up --build
# Verify container health (GET /healthz returns 200)
curl -fsSL http://localhost:3000/healthz
# Tear down (including volumes)
docker compose down -v- The
appcontainer applies pending Prisma migrations (prisma migrate deploy) viadocker-entrypoint.shbefore startingnode dist/main. - Health checks:
postgres/pgbouncer/redisuse their native probes; theappservice probesGET /healthz. - Override secrets via
.envvariables:POSTGRES_PASSWORD,JWT_SECRET,JWT_REFRESH_SECRET,REDIS_PASSWORD. The JWT secrets must each be at least 32 characters or the app will refuse to boot.
The application uses environment variables for configuration. Copy .env.example to .env and adjust the values as needed.
| Variable | Description | Default |
|---|---|---|
DATABASE_URL |
PostgreSQL connection string | Required |
PORT |
Server port | 3000 |
NODE_ENV |
Environment mode | development |
FRONTEND_URL |
Frontend application URL for email links | http://localhost:3000 |
JWT_SECRET |
JWT signing secret | Required |
JWT_REFRESH_SECRET |
JWT refresh token secret | Required |
JWT_ACCESS_EXPIRES_IN |
Access token expiration | 15m |
JWT_REFRESH_EXPIRES_IN |
Refresh token expiration | 7d |
BCRYPT_ROUNDS |
Password hashing rounds | 12 |
PASSWORD_HISTORY_LIMIT |
Password history limit | 5 |
PASSWORD_MIN_LENGTH |
Minimum password length | 8 |
PASSWORD_REQUIRE_UPPERCASE |
Require uppercase in password | true |
PASSWORD_REQUIRE_LOWERCASE |
Require lowercase in password | true |
PASSWORD_REQUIRE_DIGIT |
Require digit in password | true |
PASSWORD_REQUIRE_SPECIAL |
Require special char in password | true |
PASSWORD_SPECIAL_CHARS |
Allowed special characters | !@#$%^&*()_+-=... |
FRONTEND_URL |
Frontend application URL for email links | http://localhost:3000 |
RECAPTCHA_SECRET |
Google reCAPTCHA v3 private key | Required |
CAPTCHA_THRESHOLD |
Minimum reCAPTCHA score to pass | 0.5 |
BASE_URL |
Root URL of this API server | http://localhost:3000 |
API_URL |
Full API base URL for email links | http://localhost:3000/api |
AVATAR_UPLOAD_DIR |
Directory for user avatar uploads | ./uploads/avatars |
AVATAR_MAX_FILE_SIZE |
Max avatar file size in bytes | 5242880 |
CORS_ORIGINS |
Comma-separated allowed origins | http://localhost:3000 |
DEBUG_PII |
Enable PII debugging in auth logs | false |
EMAIL_VERIFICATION_EXPIRES_IN |
Email verification token TTL | 24h |
GOOGLE_CLIENT_ID |
Google OAuth2 client ID | — |
GOOGLE_CLIENT_SECRET |
Google OAuth2 client secret | — |
GOOGLE_CALLBACK_URL |
Google OAuth2 callback URL | /api/auth/google/callback |
BLOCKCHAIN_ENABLED |
Enable blockchain integration | true |
BLOCKCHAIN_NETWORK |
Ethereum network | sepolia |
BLOCKCHAIN_RPC_URL |
Ethereum RPC endpoint (validated at boot) | — |
BLOCKCHAIN_CONTRACT_ADDRESS |
Smart contract address (EIP-55 checksum validated at boot) | — |
BLOCKCHAIN_PRIVATE_KEY |
Wallet private key for signing (validated at boot) | — |
BACKUP_STORAGE_PATH |
Directory for DB backup files | ./backups |
PG_DUMP_PATH |
Path to pg_dump binary | pg_dump |
PSQL_PATH |
Path to psql binary | psql |
PROPERTY_IMAGES_UPLOAD_DIR |
Directory for property images | ./uploads/properties |
PROPERTY_IMAGE_MAX_SIZE |
Max property image size in bytes | 10485760 |
PROPERTY_IMAGE_MAX_PER_PROPERTY |
Max images per property | 30 |
GEOCODING_PROVIDER |
Geocoding provider (nominatim/google) | nominatim |
NOMINATIM_BASE_URL |
Nominatim API base URL | https://nominatim.openstreetmap.org |
GEOCODING_USER_AGENT |
User agent for geocoding requests | PropChain-Backend/1.0 |
GEOCODING_TIMEOUT_MS |
Geocoding request timeout (ms) | 5000 |
GOOGLE_GEOCODING_API_KEY |
Google Geocoding API key (optional) | — |
FRAUD_ALERT_RECIPIENTS |
Comma-separated fraud alert emails | — |
CACHE_WARMING_ENABLED |
Enable cache warming on startup | false |
CACHE_WARMING_INTERVAL |
Cache warming interval (ms) | — |
TEST_DATABASE_URL |
PostgreSQL URL for integration tests | — |
# Generate Prisma Client
npm run db:generate
# Run migrations
npm run migrate
# (Optional) Seed database
npm run db:seedSeeding is scoped by SEED_ENV (default development). Re-running the same
scope is idempotent because the completed scope is marked in the database.
Set SEED_RESET=true only when a destructive reset is intended. Production
seeding is blocked unless SEED_ALLOW_IN_PRODUCTION=true is set explicitly.
# Development mode
npm run start:dev
# Production mode
npm run build
npm run start:prodJest picks up every *.spec.ts file under src/ and test/ (jest.config.js).
| Command | What it runs |
|---|---|
npm test |
All unit + e2e specs (excludes test/database/) |
npm run test:database |
DB-backed suites in test/database/, serially (--runInBand) |
npm run test:all |
npm test then test:database. This is what CI runs |
npm run test:cov |
Tests with coverage; fails below the thresholds in jest.config.js |
npm run test:watch |
Watch mode |
npm run test:debug |
Run Jest under the Node inspector |
npm run check:i18n |
Translation key symmetry check (src/i18n/translations.symmetry.spec.ts) |
Test layout:
src/**/*.spec.ts # unit tests next to the code
test/unit/ # cross-module unit tests
test/e2e/ # HTTP-level tests (admin API, documents, disputes, auth…)
test/database/ # integration tests against a real Postgres (TEST_DATABASE_URL)
test/{admin,auth,backup,cache,sessions,transactions,users}/ # feature suites
For database-backed tests, set TEST_DATABASE_URL to a dedicated test database. test/database/prisma-test-helpers.ts cleans fixtures and resets seeded state between suites.
Coverage thresholds (enforced by test:cov) are a global baseline of 24% statements / 16% branches / 17% functions / 24% lines, with stricter per-module floors for src/auth/, src/documents/, src/sessions/ and others. See jest.config.js.
Note:
jest.config.jslists/test/database/intestPathIgnorePatterns, which also applies when that path is passed on the CLI. Check thatnpm run test:databaseactually executes suites (npx jest test/database --listTests) before relying on it.
The full architecture write-up is in docs/architecture.md.
src/
├── main.ts # Bootstrap: Swagger (/api/docs), metrics listener, global pipes
├── app.module.ts # Root module: wires feature modules, global filters & interceptors
├── app.controller.ts
│
├── auth/ # JWT + refresh tokens, API keys, MFA, login rate limiting, RBAC guards
├── users/ # Profiles, preferences, avatars, KYC docs, activity logs, CSV import, search
├── sessions/ # Active session listing & revocation
├── properties/ # Listings, images, geocoding, expiry, tax strategy (properties/tax)
├── transactions/ # Transaction lifecycle, disputes, timeline, cancellation, audit
├── documents/ # Upload, versioning, signed download URLs, expiry
├── blockchain/ # On-chain recording, contracts, blockchain audit trail
├── commissions/ # Agent commission calculation
├── trust-score/ # User trust score + leaderboard
├── fraud/ # Fraud rules, alerts, auto-block
├── admin/ # Admin back office + BullMQ queue management
├── search/ # Property search, facets, autocomplete, analytics
├── notifications/ # In-app, WebSocket presence, SMS
├── email/ # Email service, BullMQ mail processor, templates, provider webhooks
├── backup/ # pg_dump backups, schedule, retention, restore
├── archive/ # Data archival strategy (#919)
├── audit/ # Audit history retention / pruning
├── cache/ # Redis cache, warming, invalidation, metrics
├── … # every other module is listed in the Modules table below
│
├── common/ # Filters, interceptors, logger, request-id middleware, security utils
├── config/ # Swagger/OpenAPI config & API docs controller
├── database/ # PrismaService, cleanup cron (#920)
├── i18n/ # Translations + localized errors (#964)
├── versioning/ # API versioning, deprecation headers
└── types/, utils/ # Shared types and helpers
prisma/
├── schema.prisma # Database schema
├── migrations/ # Migration history (validated in CI)
└── seed.ts # Seed data
scripts/
├── setup.sh # One-command dev onboarding (#926)
├── validate-migrations.ts # Blocks destructive migrations (CI)
└── benchmark.ts # API benchmark (benchmark workflow)
test/ # Unit, e2e and DB integration suites
docs/ # Guides & runbooks
Status: ✅ imported by AppModule (directly or transitively) ·
| Module | Purpose | Base route(s) | Status | Docs |
|---|---|---|---|---|
admin |
Admin back office: users, moderation, fraud, backups, archive, API keys, queues | /admin/* |
✅ | README |
analytics |
Request/usage analytics | /analytics |
✅ | |
archive |
Data archival strategy & restore | via /admin/archive/* |
✅ | |
audit |
Daily archive + prune of history tables (365 days) | (cron only) | ✅ | README |
auth |
Login, JWT/refresh, API keys, MFA, rate limiting, RBAC | /auth, /admin/rate-limits |
✅ | Auth & Users, Login rate limiting, RBAC matrix |
backup |
pg_dump backups, schedule, retention, restore |
via /admin/backups/* |
✅ | README |
blockchain |
On-chain recording & contract integration | /blockchain |
✅ | Integration guide, Recording, Quickstart |
cache |
Global Redis cache, warming, invalidation, stats | /cache |
✅ | README |
commissions |
Agent commissions | /commissions |
✅ | |
common |
Filters, interceptors, logger, middleware | n/a | ✅ | Coding patterns |
config |
Swagger / OpenAPI setup, API docs | /api/docs |
✅ | |
content |
CMS-style content | /content |
||
dashboard |
User dashboard stats | /dashboard |
✅ | |
database |
Prisma service, expired-record cleanup cron | n/a | ✅ | Optimize queries |
documents |
Uploads, versions, signed download URLs, expiry | /documents |
✅ | Document metadata, CDN for assets |
duplicate-detection |
Duplicate listing detection & merge | /properties/duplicates |
||
email |
Email service, mail queue processor, templates, provider webhooks |
/webhooks/email |
✅ | Templates, Campaigns |
email-digest |
Scheduled digest emails | /email-digest |
||
favorites |
Saved properties | /favorites |
✅ | |
fraud |
Fraud rules, alerts, auto-block | via /admin/fraud/* |
✅ | README |
health |
Kubernetes probes | /healthz, /readyz, /startupz |
✅ | |
i18n |
Translations, localized errors | n/a | ✅ | |
integrations |
External integration adapters | /integrations |
✅ | Adapters |
metrics |
Prometheus metrics | /metrics (+ METRICS_PORT) |
✅ | Monitor performance |
mortgage-calculator |
Mortgage calculations | /mortgage-calculator |
✅ | |
neighborhoods |
Neighborhood data | /neighborhoods |
||
notifications |
In-app + WebSocket + SMS notifications | /notifications, WS /notifications |
✅ | README |
open-house |
Open-house scheduling | /open-house |
✅ | |
properties |
Listings, images, geocoding, expiry, tax strategies | /properties |
✅ | README, Tax strategy |
property-comparison |
Side-by-side comparison | /property-comparison |
✅ | |
property-views |
View tracking | /property-views |
✅ | |
reports |
Report scheduling utilities (no Nest module) | n/a | n/a | Generate reports |
search |
Property search, facets, autocomplete, analytics | /search |
✅ | README, Analytics privacy |
sessions |
Session listing & revocation | /sessions |
✅ | |
support-tickets |
Support ticketing | /support-tickets |
✅ | Handle support |
tracing |
Request tracing interceptor | n/a | ||
tracking |
Event tracking | /track |
✅ | |
transactions |
Transactions, disputes, timeline, audit | /transactions, /disputes |
✅ | README |
trust-score |
Trust score & leaderboard | /trust-score |
✅ | |
users |
Profiles, preferences, avatars, verification, activity logs, CSV import, search | /users/*, /admin/activity-logs |
✅ | Users, User management, Audit logs, Avatars |
versioning |
API versioning & deprecation headers | n/a | ✅ | API versioning |
webhooks |
Outbound signed webhooks with retry/backoff | /webhooks |
README |
More guides: DEVELOPMENT.md, SECURITY.md, LOAD_TESTS.md, Rate-limit incident runbook, CHANGELOG guide.
| Command | Description |
|---|---|
bash scripts/setup.sh |
One-command local environment setup |
npm run build |
Compile with nest build (cleans dist/ first) |
npm run start |
Start once |
npm run start:dev |
Start in watch mode |
npm run start:debug |
Watch mode with debugger |
npm run start:prod |
Run compiled dist/main |
npm run lint |
ESLint with --fix over src and test |
npm run format |
Prettier over src and test |
npm test / npm run test:* |
See Testing |
npm run check:i18n |
Translation key symmetry check |
npm run migrate |
prisma migrate dev |
npm run migrate:deploy |
prisma migrate deploy (production) |
npm run migrate:reset |
Drop and re-apply all migrations (destroys data) |
npm run db:generate |
Generate Prisma Client |
npm run db:seed / npm run seed |
Seed the database |
npm run db:studio |
Open Prisma Studio |
npx ts-node scripts/validate-migrations.ts |
Check migrations for destructive changes |
npx ts-node scripts/benchmark.ts |
Run API benchmarks against a running app |
A Husky pre-commit hook runs lint-staged: eslint --fix --max-warnings=0 + Prettier on staged *.ts, and Prettier on *.json / *.md.
- User - Platform users (buyers, sellers, agents, admins)
- Property - Real estate listings with detailed information
- Transaction - Property transactions with blockchain integration
- Document - Property-related documents and files
The authoritative schema is prisma/schema.prisma. It also covers sessions, fraud alerts, webhooks, backups, notifications, search analytics and more.
Create a .env file based on .env.example (.env.local takes precedence if present):
DATABASE_URL=postgresql://user:password@localhost:5432/propchain
PORT=3000
JWT_SECRET=your-secret-key # at least 32 chars
JWT_REFRESH_SECRET=your-refresh-key # at least 32 chars
REDIS_HOST=localhost
REDIS_PORT=6379Module-specific variables (fraud, webhooks, SMS, backups, cache, audit archive) are documented in each module's README.
| Workflow | Jobs | Trigger |
|---|---|---|
ci.yml |
lint (ESLint, zero warnings) · validate-migrations (scripts/validate-migrations.ts) · test (Postgres 15 service → prisma db push → npm run test:all → npm run test:cov) · build (needs the three above; uploads dist/) · deploy-staging (develop) · deploy-production (main) |
on: block is commented out |
benchmark.yml |
Boots the app against Postgres 15 + Redis 7, runs scripts/benchmark.ts, uploads results, comments on the PR |
All jobs use Node 20. The deploy jobs are placeholders (echo only). No real deployment is automated yet.
Until CI is re-enabled, run the same checks locally before opening a PR:
npm ci
npm run lint -- --max-warnings=0
npx ts-node scripts/validate-migrations.ts
npm run test:all
npm run test:cov
npm run buildSee Docker Workflow above. The image entrypoint (docker-entrypoint.sh) runs prisma migrate deploy before node dist/main.
npm ci
npm run build
npm run migrate:deploy
npm run start:prodRuntime requirements beyond Node: PostgreSQL, Redis (cache, presence, BullMQ), and pg_dump/psql on the PATH if backups are used.
Routes are served without a global prefix (e.g. /properties, not /api/properties). The complete, always-current reference is the Swagger UI:
GET /api/docs: Swagger UIGET /api/openapi.json: OpenAPI spec
GET /healthz: livenessGET /readyz: readinessGET /startupz: startup probeGET /metrics: Prometheus metrics (served onMETRICS_PORTinstead when set)
See the module table for base routes, and docs/Auth_and_User_APIs.md / src/properties/README.md for details.
GET /transactions/:transactionId/tax-strategies- List tax strategy suggestions for a transactionPOST /transactions/:transactionId/tax-strategies- Create a tax strategy suggestionPATCH /transactions/:transactionId/tax-strategies/:strategyId- Update a tax strategy suggestion
Tax strategy suggestions are informational only and are not legal or tax advice. See docs/Tax_Strategy_Suggestions.md for usage details.
See CONTRIBUTING.md for contribution guidelines, branch naming conventions, PR expectations, and local test/lint instructions.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the MIT License.
For support, email support@propchain.com or join our Slack channel
- TypeScript strict mode: The project now enables
strictTypeScript checks. The base config is in tsconfig.json. - Key compiler flags enforced:
strict,noImplicitAny,strictNullChecks,useUnknownInCatchVariablesandnoImplicitOverrideare enabled for app builds via tsconfig.app.json, which must not override them tofalse. OnlystrictPropertyInitializationis relaxed (NestJS DI-injected properties). - Guard:
npm run check:tsconfig-strict(scripts/check-tsconfig-strict.js) fails if either config weakens these flags; CI runs it in the lint job. - ESLint rules:
@typescript-eslint/no-explicit-anyis set toerrorand explicit boundary/return types are encouraged via@typescript-eslint/explicit-module-boundary-typesand@typescript-eslint/explicit-function-return-type(set towarn). See .eslintrc.js.
Local checks before committing/pushing:
# Install
npm ci
# Run linter (auto-fixable issues)
npm run lint
# Verify tsconfig strict flags are intact
npm run check:tsconfig-strict
# Build to verify TypeScript strict checks
npm run buildCI: .github/workflows/ci.yml defines lint (zero warnings), migration validation, tests and build jobs, but its triggers are currently commented out. See Deployment & CI.