eletrotupi / tcc/ commit / 88238be

all: drop stack docs

Pedro Lucas Porcellis porcellis@eletrotupi.com · 1 month ago 88238be34dd62f1819ca47962a700d505097311f Browse files
Parents 920832c
Changed files
1
Additions
0
Deletions
-308
docs/STACK.md deleted-308
docs/STACK.md → /dev/null
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.