# Halo Bestie — Backend

Fastify.js REST API serving both mobile apps and the internal control center.

> See root `CLAUDE.md` for full project context and architectural decisions.

## Stack

- **Runtime:** Node.js + Fastify.js
- **Database:** PostgreSQL via GCP Cloud SQL
- **Auth:** Self-managed JWT (HS256 access, 1h) + opaque refresh token (30d, rotated, bcrypt-hashed in `auth_sessions`). Firebase Auth removed in Phase 3.4 (commit `f860ab6`). `firebase-admin` is kept but only for FCM messaging.
- **Payment:** Xendit
- **Infra:** GCP Cloud Run

## Two Listeners

```
Public  (0.0.0.0:3000)   → client_app + mitra_app routes
Internal (private IP:3001) → control_center routes only
```

Internal listener must never be exposed to the public internet.

## Route Namespacing

```
/api/client/...    → client app routes
/api/mitra/...     → mitra app routes
/api/shared/...    → shared routes (e.g. auth, refresh, logout, anonymous)
/internal/...      → control center routes (internal listener only)
```

## Auth Flow

- **Mobile (client/mitra):** `Authorization: Bearer <access_token>` header. Access token is our own JWT (HS256, `AUTH_JWT_SECRET`), with claims `{ sub, user_type, session_id }`. Refresh via `POST /api/shared/auth/refresh` with the opaque refresh token in the body.
- **Control center:** Access token in `Authorization: Bearer` (kept in memory by the SPA). Refresh token lives in an `httpOnly` Secure cookie; refresh calls `POST /internal/auth/refresh` with `credentials: 'include'`.
- **Entry points:**
  - Anonymous customer: `POST /api/shared/auth/anonymous`
  - Phone OTP (customer/mitra): `/api/{client,mitra}/auth/otp/{request,verify}` — **Fazpass is stubbed** in `otp.service.js`; code is logged to the backend console (`[OTP STUB] phone=… code=…`) until real API docs arrive.
  - Google/Apple: `/api/client/auth/{google,apple}` (client_app only — creds pending)
  - CC login: `POST /internal/auth/login` (email + bcrypt password)
- **Middleware:** `authenticate` plugin verifies the JWT and attaches `request.auth = { userType, userId, sessionId }`. WebSocket handshake uses the same verification. **No DB lookup on every request** — the user ID is encoded in the token.

## Key Conventions

- All routes must be authenticated unless explicitly marked public (auth + anonymous routes are the exceptions)
- Internal routes additionally require `request.auth.userType === 'cc_user'`
- Use Fastify plugins for shared middleware (auth, error handling, logging)
- Business logic lives in `services/` — never directly in route handlers
- Never reintroduce Firebase Auth. `firebase-admin` is FCM-only; do not import `.auth()` from it.
