Backend API for My Ulo, a PropTech platform that helps Nigerians find verified rental and purchase properties through identity-verified agents and landlords, location-aware property data, secure payments, and transparent property reviews.
- Prerequisites
- Tech Stack
- Current API Surface
- Project Structure
- Getting Started
- Environment Variables
- Available Scripts
- Development Workflow
- Testing
- Git Workflow and Commit Convention
- Before You Push - Sync Your Env
Before you begin, ensure you have the following installed:
| Tool | Notes |
|---|---|
| Node.js | Current LTS version recommended |
| npm | Default package manager for this project |
| PostgreSQL | Database server |
| PostGIS | PostgreSQL spatial extension |
Do not mix package managers. This project uses npm.
- Runtime — Node.js
- Framework — Express.js
- Language — TypeScript
- Database — PostgreSQL
- ORM — Drizzle ORM
- Spatial Database — PostGIS
- Validation — Zod
- Authentication — Passwordless (email OTP) + Google Sign-In + JWT (Access & Refresh tokens), Argon2 for hashing login codes
- File Storage — Cloudinary(via Multer)
- Identity Verification — Dojah (synchronous NIN/CAC lookup)
- Payments — Paystack (recurring monthly subscription)
- Logging — Pino + pino-http
- Security — Helmet, CORS, Cookie Parser, Rate Limiter
- API Documentation — Swagger (OpenAPI), deployed alongside the API on Azure for frontend consumption
- Git Hooks — Husky + Commitlint
Note on Identity Verification: Dojah's synchronous lookup was the best fit driven by cost and integreation complexity for a 4-week MVP.
All routes are mounted under /api/v1, plus a root-level health check.
- GET /health
Properties
- GET /properties — search/filter/paginate (public, auth-aware for isSaved/contact-gating once available)
- GET /properties/recommended
- GET /properties/:id — includes Trek Check, trust score, owner info (contact gated by premium status), isSaved
- POST /properties — agent/landlord only
- PATCH /properties/:id
- PATCH /properties/:id/publish — requires owner KYC verified
- DELETE /properties/:id
- POST /properties/:id/media — photo/video upload via Cloudinary
- POST /properties/:id/report — reason, description, evidence upload
- POST /properties/:id/save, DELETE /properties/:id/save
Saved
- GET /saved — supports ?listingPurpose=rent|sale
- GET /saved/counts
Saved Filters
- GET /saved-filters, POST /saved-filters, DELETE /saved-filters/:id
Reviews
- GET /properties/:propertyId/reviews — public, paginated, split into verifiedResident / communityTip
- POST /properties/:propertyId/reviews — GPS-gated, photo upload supported
Inquiries
- POST /properties/:id/inquiries
- GET /inquiries
Amenities
- GET /amenities — filter by type, optional proximity search
KYC
- POST /kyc/verify-nin
- POST /kyc/verify-cac
- GET /kyc/status
Payments
- POST /payments/subscribe
- POST /payments/webhook
- GET /payments/history
- GET /payments/subscription
- POST /payments/cancel
Referrals
- GET /referrals/me
- POST /referrals/apply
Users
- GET /users/me
- PATCH /users/me/avatar
- DELETE /users/me
- GET /users/:id/profile — public agent/landlord profile
Auth
- POST /auth/request-code
- POST /auth/verify-code
- POST /auth/google
- PATCH /auth/complete-profile
- POST /auth/refresh
- POST /auth/logout
Admin(requires admin role - bootstrapped manually, no self-registration)
- GET /admin/reports
- PATCH /admin/properties/:id/status
- GET /admin/kyc/:id/resolve
- PATCH /admin/kyc/:id/resolve
- PATCH /admin/users/:id/blacklist - also revokes all active sessions for that user
.
├── .github/
├── .husky/
├── scripts/
├── src/
│ ├── app.ts
│ ├── server.ts
│ ├── config/
│ ├── constants/
│ ├── controllers/
│ ├── db/
│ │ ├── schema/
│ │ ├── migrations/
│ │ ├── index.ts
│ │ └── seed.ts
│ ├── docs/
│ ├── errors/
│ ├── lib/
│ ├── middlewares/
│ ├── routes/
│ ├── services/
│ ├── templates/
│ ├── types/
│ ├── utils/
│ └── validations/
├── .env.example
├── .oxlintignore
├── commitlint.config.cjs
├── drizzle.config.ts
├── lint-staged.config.js
├── oxlintrc.json
├── package-lock.json
├── package.json
├── README.md
├── tsconfig.build.json
├── tsconfig.json
└── tsdownconfig.ts
config/env validation and database connection setup.controllers/handles incoming HTTP requests.services/contains business logic.routes/defines API endpoints.db/manages database configuration, migrations, seeding, and schema definitions.middlewares/contains reusable Express middleware.validations/stores Zod validation schemas.lib/contains thin wrappers around third-pary APIs.docs/contains Swagger/OpenAPI documentation.
git clone https://github.com/Group-2-Build-SZN/node-backend.git
cd my-ulo-backendnpm installcp .env.example .envUpdate the values in .env.
CREATE EXTENSION IF NOT EXISTS postgisnpm run migratenpm run devGET http://localhost:5000/healthThe application uses environment variables stored in .env.
Common variables include:
| Variable | Description |
|---|---|
| NODE_ENV | Application environment |
| PORT | Server port |
| DATABASE_URL | PostgreSQL connection string |
| ALLOWED_ORIGINS | Comma-separated list |
| JWT_SECRET | JWT signing secret |
| JWT_REFRESH_SECRET | Refresh token secret |
| CLOUDINARY_CLOUD_NAME | Cloudinary configuration |
| CLOUDINARY_API_KEY | Cloudinary configuration |
| CLOUDINARY_API_SECRET | Cloudinary configuration |
| PAYSTACK_SECRET_KEY | Paystack Secret Key |
| PAYSTACK_PLAN_CODE | Pystack plan code |
| DOHAH_APP_ID | Dojah KYC - App ID |
| DOJAH_SECRET_KEY | Dojah KYC - Secret key |
| DOJAH_BASE_URL | Dojah API Base URL |
| EMAIL_HOST/PORT/USER/PASS | SMTP config for sending login codes |
| OTP_EXPIRY_MINUTES | Login code expiry window |
| OTP_CODE_LENGTH | Digits in the emailed login code |
| GOOGLE_CLIENT_ID | Google Sign-In token verification |
Never commit your .env file.
| Script | Description |
|---|---|
| npm run dev | Start development server |
| npm run build | Build the application |
| npm start | Run production build |
| npm run type-check | Run TypeScript checks |
| npm run lint | Run linting |
| npm run format | Format source files |
| npm run generate | Generate Drizzle migrations |
| npm run migrate | Run database migrations |
| npm run seed | Seed the database |
| npm run prepare | Install Git hooks |
- Pull the latest changes from the
devbranch. - Create a feature branch.
Example:
feat/authentication
feat/property-module
fix/payment-webhook
- Implement your feature.
- Run:
npm run lint
npm run type-check- Commit using Conventional Commits.
- Push your branch.
- Open a Pull Request into
dev.
Automated tests will be added as the project evolves.
Future tests should cover:
- Authentication
- Property Management
- Reviews
- Payments
- Verification
- Location Services
This repository follows Conventional Commits.
feat(auth): implement login endpoint
feat(property): add property creation endpoint
fix(payment): handle failed webhook verification
refactor(review): simplify review service
docs: update README
chore: initialize backend project
Whenever a new environment variable is introduced:
- Update your local
.env - Update
.env.example - Never commit secrets
- Verify all required keys are documented
Authentication is complete: passwordless email OTP, Google Sign-In, and JWT access tokens paired with DB-backed, rotating refresh tokens (refresh_tokens table) — not stateless JWT refresh tokens. Key behaviors worth knowing:
-
Refresh tokens are opaque random strings, stored hashed (SHA-256) in the database, and rotated on every use — the old one is revoked the moment a new one is issued, so a stolen-but-unused refresh token becomes worthless as soon as the real owner refreshes again.
-
authenticate(strict) vs.attachUserIfPresent(soft, doesn't reject unauthenticated requests) are both implemented — used on public-but-auth-aware routes likeGET /properties. -
Blacklisted users are rejected at login/refresh time. Admin blacklisting also calls
revokeAllSessions()to immediately kill a user's ability to refresh, without waiting for their short-lived (15 min) access token to expire naturally.