eletrotupi / tcc/ docs/STACK.md76378ec71e47317083b91d0e67d08a0ddc4d94cd
17.5 KB | 309 lines Raw
1
# nexo / orbit — Stack Overview
2

3
> **nexo** (repo `/orbit`, app slug `orbit`) is an academic (TCC/monografia) mood- and
4
> bipolar-disorder (TAB) tracker. Users log moods, sleep, triggers, medicine regimens and
5
> "care actions"; the backend produces auto-generated insights, sends push/email reminders
6
> and generates clinician-ready PDF reports.
7
>
8
> The repository is the **online/academic** version. It is a client–server app; the
9
> "final, offline" app is planned for a separate repo.
10

11
> **Visual:** open `docs/architecture.excalidraw` (excalidraw.com / tldraw import) for a
12
> component-and-flow diagram of the whole stack.
13

14
---
15

16
## Repository layout
17

18
```
19
nexo/
20
├── api/            # Backend — Node.js + Express REST API (all business logic)
21
├── frontend/       # Mobile app — React Native (Expo) source code
22
├── monografia/     # LaTeX thesis (auto-compiled to PDF by CI)
23
├── ansible/        # One-shot VPS provisioning (bootstrap, docker, deploy user)
24
├── .github/workflows/  # CI/CD: test → build → deploy; thesis PDF publishing
25
├── docker-compose.yml  # Local/dev infra: Postgres + Valkey + API
26
├── etc/valkey.conf     # Valkey configuration mounted by compose
27
└── docs/              # misc (this file, class notes)
28
```
29

30
---
31

32
## docker-compose.yml — local/dev infrastructure
33

34
| Service | Image | Port | Purpose |
35
|---|---|---|---|
36
| `db` | `postgres:17-alpine` | host `5430` → `5432` | Relational database, health-gated |
37
| `valkey` | `valkey/valkey:9.0-alpine` | `6379` | Redis-compatible server: BullMQ job broker + repeatable schedules |
38
| `api` | `./api` (dev Dockerfile) | `3000` | Backend with `nodemon` hot reload; bind-mounts `./api` and `api/.env.development` |
39

40
Notes:
41
- `api` only starts after `db` and `valkey` report healthy.
42
- The mobile app (`frontend`) is **not** part of compose — it's an Expo app run/devved
43
  separately and reaches the API over `EXPO_PUBLIC_API_BASE_URL`.
44

45
---
46

47
## API — `api/`
48

49
**Stack:** Node.js 24 · TypeScript 6 · Express 5 · Prisma 7 · PostgreSQL 17 · BullMQ +
50
Valkey · zod · pino · Resend · AWS S3 SDK · @react-pdf/renderer · Vitest.
51

52
### Architecture (layered)
53
```
54
routes/      → thin HTTP wiring (path → controller fn)
55
controllers/ → parse/validate request, call services, shape responses
56
services/    → business logic & persistence
57
schemas/     → zod input validation (auth, mood, trigger, medicine, etc.)
58
middleware/  → requireAuth (JWT), errorHandler
59
lib/         → prisma client, jwt, mail, s3, queue registries, push
60
reports/     → React-PDF report generation (JSX document + router)
61
```
62

63
- **Context of data:** every resource is scoped to `req.userId` set by `requireAuth`
64
  (Bearer JWT). Passwords hashed with `bcryptjs`.
65
- **Errors:** domain error classes map to HTTP statuses; middleware also handles Prisma
66
  errors (`P2025` → 404, `P2002` → 409); fallback 500. Validation errors returned with a
67
  `fields` map.
68
- **Validation:** zod schemas + `zod-validation-error`; report endpoint enforces
69
  date-range ≤ 90 days.
70
- **Logging:** `pino` + `pino-http` structured logs (pretty in dev via `pino-pretty`).
71

72
### Data model (Prisma, PostgreSQL)
73
- `users` — profile, encrypted password, activation code, push token, notification prefs,
74
  daily reminder time, avatar (S3 key/URL).
75
- `moods` + `mood_components` — mood rating (enum `SAD/NEUTRAL/GOOD/GREAT/ANGRY`) with
76
  anxiety/stress/energy levels + optional emotion components w/ intensity.
77
- `triggers` + `trigger_mood_links` — trigger events (category enum) optionally linked to
78
  moods with a perceived impact.
79
- `medicine_regimens` + `medicine_logs` — medication schedule (times of day, cadence enum)
80
  and "taken" logs.
81
- `care_actions` (+ `appointments`, `activities`, `medicine_logs`) — self-care events with
82
  type enum (MEDICINE/APPOINTMENT/ACTIVITY).
83
- `sleep_records` — daily hours of sleep + annotations.
84
- `insights` — generated analysis rows (type/period enums, human-readable body + JSON metadata).
85

86
### Background jobs (BullMQ on Valkey) — 4 queues
87
- **`mail`** (concurrency 10) — welcome, activation, password-reset emails via **Resend**.
88
- **`insights`** (concurrency 5) — weekly fan-out every **Monday 03:00** cron:
89
  `MOOD_TREND`, `ENERGY_SLEEP_CORRELATION`, `TRIGGER_PATTERN` (+ `DAILY_ENERGY`,
90
  `DAILY_SLEEP`) are computed per active user and upserted into `insights`.
91
- **`medicine-reminders`** (5) — per-regimen recurring cron jobs (created/removed by
92
  `syncMedicineReminderJobs`); sends Expo push "time to take medicine".
93
- **`daily-reminders`** (5) — per-user daily check-in push at `dailyReminderTime`
94
  (managed by `syncDailyReminderJob`).
95

96
Default job policy: 3 attempts, exponential backoff, cleanup of completed/failed jobs.
97

98
### Notifications & email
99
- **Push:** Expo Push API (`exp.host/--/api/v2/push/send`) using stored device push tokens.
100
- **Email:** Resend (activation code, welcome, password reset).
101

102
### Files & object storage
103
- **Avatars:** `multer` + `multer-s3` upload to an S3-compatible store (configurable
104
  endpoint/region → MinIO/R2 style); delete via `DeleteObjectCommand`.
105
- **PDF reports:** `@react-pdf/renderer` renders a React document from period data;
106
  endpoint `POST /reports` returns an `application/pdf` attachment.
107

108
### Root static site
109
The API also serves `src/static/` (a Tailwind/CDN marketing landing page + link to the
110
published thesis PDF). This is the "web/academic" face of the project.
111

112
### Tests & CI
113
- **Vitest + supertest** against a disposable Postgres (`DATABASE_URL` pointed at an
114
  `orbit_test` DB; `TRUNCATE users CASCADE` between tests). Files: auth, users, moods,
115
  triggers, sleep, medicines, care-actions, jobs (mail, mood-trend, push).
116
- CI (GitHub Actions) runs the suite on a Postgres 17 service container.
117

118
### Docker / deploy
119
- Dev `Dockerfile`: node:24-alpine, installs `postgresql-client`/`valkey-cli`, runs `dev`
120
  (`prisma generate && migrate deploy && nodemon`).
121
- `Dockerfile.production`: multi-stage build → runtime stage runs
122
  `npx prisma migrate deploy && node dist/index.js` (migrations auto-apply on boot).
123
- CI workflow (`deploy.yml`) on `master`: test → build & push image to **GHCR** (latest +
124
  short SHA tag) → SSH into the VPS → `docker compose pull && up -d --force-recreate api`.
125
- `ansible/provision.yml` bootstraps the VPS: packages, Docker Engine, deploy user + SSH
126
  key, `/srv/app/production`. `thesis.yml` compiles `monografia.tex` → PDF → SCP to
127
  `/srv/static/`.
128

129
---
130

131
## Frontend — `frontend/`
132

133
**Stack:** Expo SDK 57 · React Native 0.86 · React 19 · TypeScript 6.0 · expo-router
134
(file-based, typed routes) · TanStack Query v5 · Zustand · MMKV · expo-secure-store ·
135
expo-notifications · Reanimated/gesture-handler.
136

137
### Startup / routing
138
- `app/_layout.tsx` sets up `QueryClientProvider` → navigation `ThemeProvider` →
139
  `ToastProvider` → `AuthProvider` → Stack navigator, plus a notification observer.
140
- `app/index.tsx` redirects: loading spinner → `/auth/login`; logged-in →
141
  `(tabs)/new`, or `/auth/activate` if the account isn't active.
142
- `(tabs)/` — 5 tabs: **Histórico** (history), **Jornada** (insights), **Novo** (new
143
  entry flow), **Ações** (care actions), **Ajustes** (settings).
144
- Auth screens (`auth/`): login, signup, activate, forgot/reset password.
145
- Detail/flow screens: entry, sleep CRUD, triggers CRUD, medicine regimens, care actions
146
  (medicine / appointment / activity), notifications, report, profile.
147

148
### Data layer — TanStack Query + offline cache
149
- `lib/queryClient.ts`: persisted client backed by **MMKV** (`react-native-mmkv`) via
150
  `createSyncStoragePersister` (throttled 1s, cache survives 24h). 5-min staleTime,
151
  10-min GC, `retry: 1`, no refetch-on-window-focus.
152
- Per-domain hooks in `hooks/*.queries.ts` use typed query keys, `useInfiniteQuery`
153
  pagination, and **optimistic updates** (create/delete mood entries roll back on error and
154
  invalidate insight queries after a short delay).
155
- `hooks/useHistoryFeed.ts` merges mood/sleep/trigger/care-action entries into one feed.
156

157
### API client
158
- `lib/api/client.ts` — single `ApiClient` (fetch) for all endpoints: auth, users, moods,
159
  sleep, triggers, insights, medicine regimens, care actions, trigger↔mood linking, and PDF
160
  report (`ArrayBuffer`). JWT stored in **expo-secure-store**; a 401 handler force-logs-out.
161
- `lib/api/types.ts` holds shared payload/response types mirroring the backend.
162

163
### State & context
164
- **Zustand** `stores/moodEntry.ts` — locally composes a mood entry (mood + components +
165
  intensities) before submit; `stores/careActionLink.ts` for linking.
166
- **AuthContext** — session state, bootstrap `verifyToken`+`getMe` (with 10s timeout),
167
  login/signup/activate/logout; logout clears Query cache + MMKV.
168
- **ToastContext** — app-wide notifications/toasts.
169

170
### Notifications (push)
171
- `useRegisterPushToken.ts` — checks device/permission, sets Android channel, obtains the
172
  Expo push token (via EAS projectId) and registers it with the backend.
173
- `useNotificationObserver.ts` — handles cold-launch and foreground pushes, deep-linking to
174
  the route named in `data.screen` via `router.replace/push`.
175

176
### UI
177
- Custom kit under `components/ui/` (Button, Input, Cards, sections, sliders, datetime/
178
  time pickers, chips, empty states, toast, haptic tab). Inter font bundled via expo-font;
179
  theme tokens in `constants/theme.ts`; `misc/` has haptics, parallax, themed views.
180
- Shared UI constants reflect the backend enums: `moods.ts`, `mood-components.ts`,
181
  `trigger-categories.ts`, `care-action-categories.ts`.
182

183
### Builds & updates
184
- `eas.json` — development (dev client), preview (internal), production (auto-increment).
185
  `app.json` — package `com.eletrotupi.orbit`, `google-services.json` for push/FCM,
186
  `newArchEnabled: true`, expo-updates configured (OTA over `u.expo.dev`), typed routes +
187
  React Compiler experiments.
188

189
### E2E (Maestro on a physical Android device — no emulator)
190
- **Tooling:** Maestro drives whatever `adb devices` sees (a real phone via USB/wireless
191
  debugging). Host needs **Java 17+** and **adb** only — no GUI, no Android Studio.
192
  - Arch: `sudo pacman -S android-tools jre-openjdk-headless`
193
  - Alpine: `sudo apk add android-tools openjdk17`
194
  - then `curl -fsSL https://get.maestro.mobile.dev | bash` and add `~/.maestro/bin` to PATH.
195
- **Flows:** `frontend/e2e/*.yaml` (login, mood, logout — tagged `smoke`, share
196
  `helpers/_login.yaml`); config `frontend/.maestro/config.yaml` (appId + env defaults).
197
  UI `Button`/`Input`/`TextArea` and `MoodSelector` derive stable `testID`s from their
198
  label/title (`button-entrar`, `input-email`, `mood-great`, ...), so flows mostly use ids.
199
- **Backend under test:** `docker-compose.e2e.yml` adds an `api-e2e` service on host port
200
  **3001**, pointed at an isolated `orbit_e2e` database. On boot it runs `prisma migrate
201
  deploy`, then `api/scripts/seed-e2e.mjs` (TRUNCATEs `users` and inserts a known active
202
  user `e2e@example.com` / `e2e-password`), then the API. Activation-by-email is deliberately
203
  skipped — the seeded user is already `active`.
204
- **Run** (from `frontend/`):
205
  - `npm run test:e2e:all` → boots the stack, seeds, preps the device, runs the flows.
206
  - `npm run test:e2e:up` / `test:e2e:down` / `test:e2e` — the individual steps
207
    (`frontend/scripts/e2e.sh up|down|test|all`).
208
  - `npm run test:e2e -- print-env`-style: `bash scripts/e2e.sh print-env` prints the
209
    `EXPO_PUBLIC_*` vars to export before `expo start`.
210
- **Device connectivity:** the phone must reach this machine on the LAN:
211
  - API → `EXPO_PUBLIC_API_BASE_URL=http://<lan-ip>:3001` (bundler-time env);
212
  - Metro → flows deep-link the app via `exp://<lan-ip>:8081` (Expo Go / dev client).
213
  `HOST_IP` is auto-detected by the script and can be overridden via env.
214
- **Expo Go runtime (SDK 57):** the Play-Store Expo Go only ships SDK 54, so on a store
215
  installation the app won't load after the upgrade. Install an SDK 57 Expo Go APK from
216
  `expo start`/`eas go` on the test device (or match the runtime), otherwise Maestro flows
217
  will fail at launch.
218
- **Notes:** each run does `adb shell pm clear` on the app (fresh SecureStore/MMKV),
219
  pre-grants `POST_NOTIFICATIONS` and disables animations for deterministic runs. For a
220
  dev-client build the appId is `com.eletrotupi.orbit` — change it in `.maestro/config.yaml`
221
  and `APP_ID` when running the script.
222

223
---
224

225
## How it works end-to-end
226

227
1. **Onboarding** — user signs up with name/email/password → backend creates the account,
228
   stores a bcrypt hash, enqueues a welcome email + a 6-digit **activation code** (valid
229
   5 min) → user activates via `/auth/activate` → session issued.
230
2. **Session** — every request carries a 7-day JWT; on app boot the stored token is
231
   verified (`/auth/verify`) and refreshed user data fetched (`/users/me`).
232
3. **Daily use** — user records moods (with components and intensities), sleep, triggers,
233
   medicine intakes and care actions. The app writes I/O through TanStack Query mutations
234
   with optimistic UI; everything is cached in MMKV so history renders offline and
235
   revalidates later. Entries can link triggers to moods to model cause/effect.
236
4. **Scheduled work** — BullMQ cron jobs on Valkey:
237
   - daily/medicine push reminders per user preference;
238
   - weekly insight fan-out (Monday 03:00) computing mood trends, energy–sleep
239
     correlation, trigger patterns; daily energy/sleep summaries.
240
   Insights land in the `insights` table and surface in the **Jornada** tab.
241
5. **Reporting** — the user picks a period (≤ 90 days) and gets a **PDF** generated by the
242
   API (`@react-pdf/renderer`) summarizing mood, sleep, triggers and medicines — meant to
243
   be shared with the doctor/therapist.
244
6. **Deploy** — push to `master` → CI tests → GHCR image → SSH deploy → container boots and
245
   applies Prisma migrations before listening on :3000.
246

247
---
248

249
## Core architecture decisions & patterns
250

251
### Backend layering — Controller / Service (light repositories)
252
- Routes are thin. **Controllers** parse + validate (`zod`) and shape HTTP responses;
253
  **Services** hold business logic and persist directly through the Prisma client
254
  (`lib/prisma.ts`). There is **no explicit Repository/DAO layer** — the Prisma client is
255
  the single data-access gateway, made pluggable via driver adapters (`PrismaPg` for
256
  Postgres, `better-sqlite3` adapter already in deps for the future offline build).
257
- This is a **Service/Command-style** design: operations are plain async functions that
258
  take inputs and return domain results; there's no command bus or unit-of-work wrapper.
259
  Multi-step flows (e.g. signup → create user + store activation code + enqueue welcome
260
  email) are composed at the controller/service boundary.
261
- **Domain errors** (`lib/errors/base.ts`) are raised inside services and mapped to HTTP
262
  by the single `errorHandler` middleware (incl. Prisma `P2025`→404 / `P2002`→409), so
263
  services never leak framework concerns.
264

265
### Background processing — Queue/Worker (producer–consumer) with fan-out
266
- API code only **enqueues** jobs; dedicated BullMQ **Workers** consume each queue with
267
  per-queue concurrency (`QueueRegistry`/`WorkerRegistry`). Queues are independent, so long
268
  tasks run **truly in parallel** and can be scaled horizontally without sticky state.
269
- **Insights** use a **fan-out** pattern: one repeatable cron job (Monday 03:00) enqueues
270
  a per-user compute job for each active user. Results are written **idempotently** via a
271
  composite unique key (`@@unique([userId, type, periodStart])`) + `prisma.insight.upsert`,
272
  so re-runs never duplicate rows.
273
- Recurring per-user schedules (daily check-in, medicine times) are modeled as BullMQ
274
  **repeatable jobs** keyed by `jobId`; dedicated "sync" services reconcile them on any
275
  change (delete stale jobs, re-add current ones), so schedule state can't drift.
276

277
### Auth — stateless JWT
278
- No server-side session store: a 7-day signed JWT (userId/email) is validated by the
279
  `requireAuth` middleware. Passwords are bcrypt-hashed; activation and password reset use
280
  short-lived single-use codes. The API stays horizontally scalable (no sticky sessions).
281

282
### Database — schema-lived Prisma, migrations as code
283
- `prisma/schema.prisma` is the single source of truth; migrations are committed to the
284
  repo and applied via `prisma migrate deploy` on container boot / CI, making each deploy
285
  self-migrating and idempotent.
286

287
### Client — offline-first data access
288
- All server state is owned by **TanStack Query hooks** with typed query keys, persisted to
289
  **MMKV** (24h) and **optimistically mutated** with rollback + controlled invalidation.
290
  The server cache is treated as local source-ish during a session, not re-fetched on every
291
  focus. JWT lives in **expo-secure-store**; a global 401 handler signs the session out.
292

293
### Typing discipline
294
- Prisma generates the API's domain types; frontend DTO types in `lib/api/types.ts` mirror
295
  them and shared enums are duplicated in `constants/*` with `XXX: keep in sync` notes
296
  (no OpenAPI/codegen — a conscious trade-off for this size).
297

298
---
299

300
## One-paragraph summary
301

302
`nexo/orbit` is a TypeScript monorepo-style academic project: an **Express 5 + Prisma 7**
303
REST API over **PostgreSQL 17** with **BullMQ/Valkey** for cron-driven background work
304
(insights, push and email reminders via Expo Push and Resend), S3-compatible object storage
305
for avatars, and React-PDF for clinician reports; and an **Expo (React Native 0.81)**
306
client using **expo-router** file routing, **TanStack Query persisted to MMKV** for
307
offline-first UX, secure JWT storage, and push-notification deep linking. Everything is
308
containerized for local dev via docker-compose and shipped to a VPS through GitHub Actions
309
+ GHCR + Ansible provisioning.