all: drop stack docs
Pedro Lucas Porcellis porcellis@eletrotupi.com · 1 month ago
88238be34dd62f1819ca47962a700d505097311f Browse filesParents
920832cChanged files
1
Additions
0
Deletions
-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.