Node.js/Express REST API for the Morty AI-powered mortgage analysis platform.
npm install
cp .env.example .env # fill in your values
npm run devFull API reference with request/response shapes, error codes, and frontend integration examples: docs/API.md
/api/v1
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /auth/register | – | Register new user |
| POST | /auth/login | – | Login, receive JWT |
| POST | /auth/refresh | – | Refresh access token |
| POST | /auth/logout | – | Invalidate refresh token |
| GET | /auth/me | ✓ | Get current user |
| GET | /profile | ✓ | Get financial profile |
| PUT | /profile | ✓ | Upsert financial profile |
| PATCH | /profile | ✓ | Partially update financial profile |
| POST | /offers | ✓ | Upload mortgage offer file |
| GET | /offers | ✓ | List offers (paginated) |
| GET | /offers/stats | ✓ | Offer statistics |
| GET | /offers/:id | ✓ | Get single offer |
| DELETE | /offers/:id | ✓ | Delete offer |
| GET | /analysis/:id | ✓ | Get AI analysis results |
| GET | /dashboard | ✓ | Dashboard summary |
| GET | /health | – | Health check |
All responses use a consistent JSON envelope:
{ "success": true, "data": { ... }, "message": "..." }See docs/API.md for full details.
See .env.example for all required variables.
- Runtime: Node.js 20 / Express 4
- Database: Google Cloud Firestore (via firebase-admin 12)
- Auth: JWT (access 15m + refresh 7d) + bcryptjs
- Validation: Joi
- File Upload: Multer → Cloudinary
- AI: OpenAI GPT-4o Vision
- Logging: Winston
- Security: Helmet, CORS, express-rate-limit
The application uses three Firestore collections:
| Collection | Document ID | Key Fields |
|---|---|---|
users |
Auto-generated | email, refreshToken, verified |
financials |
userId (same as user doc ID) |
userId, income, expenses, assets |
offers |
Auto-generated | userId, status, createdAt |
Firestore requires explicit composite indexes for queries that filter on one
field and sort/filter on another. The index definitions are stored in
firestore.indexes.json.
| Collection | Fields | Direction | Used By |
|---|---|---|---|
offers |
userId, createdAt |
ASC, DESC | listOffersByUser, getRecentOffers |
offers |
userId, status |
ASC, ASC | countOffersByUser (with status filter) |
offers |
userId, status, createdAt |
ASC, ASC, DESC | Future filtered+sorted queries |
Firestore creates single-field indexes automatically by default. The following fields are explicitly configured to ensure they are never accidentally disabled:
| Collection | Field | Used By |
|---|---|---|
users |
email |
findByEmail (login / register uniqueness check) |
users |
refreshToken |
findByRefreshToken (token rotation) |
financials |
userId |
Direct doc lookup (no query index needed) |
# Install Firebase CLI if not already installed
npm install -g firebase-tools
# Authenticate
firebase login
# Deploy only the Firestore indexes
firebase deploy --only firestore:indexes- Open Firebase Console → Firestore → Indexes
- Click Add Index for each composite index listed above
- Single-field indexes are created automatically
Run the verification script to print a summary of all required indexes:
node scripts/verify-indexes.jsWithout the offers (userId ASC, createdAt DESC) composite index, Firestore
will reject the following queries with a FAILED_PRECONDITION error:
The query requires an index. You can create it here: https://console.firebase.google.com/...
This would break:
GET /api/v1/offers– listing a user's offersGET /api/v1/dashboard– recent offers in the dashboard summary
npm test
npm run test:coverage