This is the map of the system as it runs in production: every client surface, the API behind them, the database, how new code reaches production, and how we find out when something breaks. The diagram above shows the whole picture; the sections below walk through each piece.
| Layer | Technologies |
|---|---|
| Web app | React 18, TypeScript, Vite, Redux Toolkit, Axios, react-hook-form + Zod, i18next (en/pt), Tailwind CSS 3. The study notebook adds BlockNote on Mantine 8 for the editor and React Flow for the roadmap board. The Ideas board is React Flow too |
| Mobile app | React Native + Expo (Android first), NativeWind, TypeScript. Shares the state, API client, and i18n packages with the web app through the monorepo |
| Backend | Spring Boot 4.1, Java 25 (virtual threads), Spring Security, JWT (auth0 java-jwt), Spring AOP, Spring AI for the agent chat, onboarding suggestions and the study notebook's AI (LLM fallback chain), PDFBox and jsoup for notebook sources |
| Database | PostgreSQL 17 in production (a CloudNativePG cluster; tests and the local stacks run 15), Flyway-owned schema (Hibernate validates it, never writes it), UUID primary keys, Caffeine cache in front of hot reads |
| Delivery | GitHub Actions builds the backend image to GHCR; Flux (GitOps) rolls it out on a single-node k3s cluster; CI deploys the web and docs builds to Cloudflare Workers Static Assets with wrangler; Docker Compose for local development and e2e |
| Monitoring | kube-prometheus-stack (Prometheus, Grafana), Loki + Alloy, GlitchTip (Sentry-compatible) |
Production runs on a single-node k3s cluster whose whole state lives in Git and is applied by Flux, with Cloudflare serving the two static sites. The application side is three pieces, all built by CI:
/api/v1, running in the beyou namespace from an image on GitHub Container Registry. Public at api.beyouweb.com, which the web app and the docs pages call straight from the browser. One replica, with uploads on a persistent volume.The database is beyou in a shared PostgreSQL 17 cluster run by the CloudNativePG operator in the data namespace. Merging to main is the deployment. For the backend, CI pushes a new :latest image, Flux's image automation records its digest in Git and rolls it out, and rolling back is a git revert. The two static sites go from their own CI to Cloudflare with wrangler deploy, and they roll back there too. Nothing on the server listens on a public port. A Cloudflare Tunnel dials out from the server and hands the cluster's public hostnames to Traefik, the only ingress, which routes by host to each Ingress; every namespace sits behind a default-deny network policy. The infrastructure topic covers the machine, the cluster and the backups. Two more pieces live outside the cluster: the marketing site at beyouweb.com (static HTML on Cloudflare Pages) and the mobile app, which installs on the phone and talks to the same API as the web app.
The content of this docs site has its own pipeline: markdown and OpenAPI specs live in the beyou-arch-design repository, the backend imports them into PostgreSQL, and a content change rebuilds the prerendered docs site and deploys it to Cloudflare.
In production the observability stack runs inside the cluster as kube-prometheus-stack plus Loki, Alloy and GlitchTip; locally a Compose overlay answers the same questions. Each component answers a different question:
Grafana sits on top of all three, with dashboards loaded from Git: backend service health, the AI agent, product usage, logs, the host and the ingress. It also e-mails the alerts about the machine and the cluster (memory, disk, restarting or stuck pods, targets down), while GlitchTip keeps the application's. It lives at obs.beyouweb.com, behind its own login. Those two are the only public monitoring surfaces; Prometheus, Loki and the exporters have no hostname and are only reachable through Grafana.
erDiagram
USER ||--o{ CATEGORY : owns
USER ||--o{ HABIT : owns
USER ||--o{ TASK : owns
USER ||--o{ GOAL : owns
USER ||--o{ ROUTINE : owns
USER ||--o{ NOTEBOOK_PAGE : owns
USER ||--o{ CALENDAR_TASK : owns
USER ||--o{ QUICK_NOTE : owns
CATEGORY }o--o{ HABIT : tagged
CATEGORY }o--o{ TASK : tagged
CATEGORY }o--o{ GOAL : tagged
ROUTINE ||--o{ ROUTINE_SECTION : contains
ROUTINE ||--|| SCHEDULE : "scheduled by"
ROUTINE_SECTION ||--o{ HABIT_GROUP : groups
ROUTINE_SECTION ||--o{ TASK_GROUP : groups
HABIT_GROUP }o--|| HABIT : references
TASK_GROUP }o--|| TASK : references
HABIT_GROUP ||--o{ HABIT_GROUP_CHECK : tracks
TASK_GROUP ||--o{ TASK_GROUP_CHECK : tracks
NOTEBOOK_PAGE ||--o{ NOTEBOOK_PAGE : "parent of"
NOTEBOOK_PAGE ||--o{ BOARD_NODE : "board of"
BOARD_NODE }o--|| NOTEBOOK_PAGE : opens
NOTEBOOK_PAGE ||--o{ FLASHCARD : holds
NOTEBOOK_PAGE ||--o{ SOURCE : reads
QUICK_NOTE |o--o| TASK : "became"
sequenceDiagram
participant U as User
participant FE as Frontend
participant BE as Backend
participant GO as Google
rect rgba(59, 130, 246, 0.25)
U->>FE: 🔑 Email + Password Login
FE->>BE: POST /auth/login
BE-->>FE: JWT (header) + Refresh Token (HttpOnly cookie)
FE->>FE: Store JWT in memory
end
rect rgba(234, 88, 12, 0.25)
U->>FE: 🔐 Google OAuth Login
FE->>GO: Authorization redirect
GO-->>FE: Authorization code
FE->>BE: GET /auth/google?code=...
BE->>GO: Exchange code for access token
GO-->>BE: User profile
BE-->>FE: JWT + Refresh Token
end
rect rgba(16, 185, 129, 0.25)
FE->>BE: 🔄 Token Refresh: request with expired JWT
BE-->>FE: 401 Unauthorized
FE->>BE: POST /auth/refresh (cookie)
BE-->>FE: New JWT + new Refresh Token
FE->>BE: Retry original request
end
Authorization: Bearer; the backend delivers a fresh one in the X-Access-Token response header.beyou.oidc.providers gets a button on the web login. The client sends the ID token it verified through PKCE, and the account is found by the token's issuer and subject, never by its e-mail, so a new provider can only reach an existing account through a link made from inside it. Off when nothing is configured. The security topic has the rules.37 REST controllers organized by domain, all under /api/v1:
| Group | Controllers | Base paths |
|---|---|---|
| Auth | Authentication, AuthVerification | /auth/* |
| Core entities | Category, Habit, Task, Goal | /category, /habit, /task, /goal |
| Routines | Routine, Schedule, Snapshot | /routine, /schedule, /routine/snapshot |
| History | CheckHistory, XpHistory | /check-history, /xp |
| Focus Mode | Focus | /focus/cycles, /focus/micro-tasks, /focus/day |
| Diary | Mood | /mood |
| Calendar | Calendar | /calendar, /calendar/tasks |
| Ideas board | QuickNote | /quick-notes |
| Study notebook | Notebook, NotebookBoard, NotebookCard, NotebookSource, NotebookStudy, NotebookAi, StudyCycle | /notebook/* |
| Daily Briefing | DailyBriefing | /daily-briefing |
| User | User, UserPhoto, UserExport | /user, /user/photo |
| AI | AiAgent, Onboarding | /ai/agent, /onboarding |
| Feedback | Feedback, FeedbackAdmin | /feedback, /feedback/admin |
| Notifications | NotificationPreferences | /notification/preferences, /notification/unsubscribe |
| Docs | Architecture, Blog, Api, Project, Search, Import | /docs/* |
flowchart LR
REQ["📥 Request"] --> FILT["🛡️ Security Filter<br/>JWT validation"]
FILT --> CTRL["🎯 Controller<br/>DTO validation"]
CTRL --> SVC["⚙️ Service<br/>Business logic"]
SVC --> REPO["💾 Repository<br/>JPA queries"]
REPO --> DB[("🐘 PostgreSQL")]
DB --> REPO
REPO --> SVC
SVC --> MAP["🔄 Mapper<br/>Entity → DTO"]
MAP --> CTRL
CTRL --> RES["📤 Response"]
flowchart TD
AX["Axios + Interceptor"]
ST["Redux Store<br/>shared packages/state"]
PS["redux-persist<br/>localStorage (web)"]
UI["React / React Native<br/>components"]
UI -->|"dispatch actions"| ST
ST -->|"selectors"| UI
ST <-->|"hydrate / persist"| PS
UI -->|"API calls"| AX
AX -->|"dispatch on success"| ST
AX -->|"auto 401 → refresh"| AX
The Redux slices live in a shared workspace package (packages/state, 22 slices), so the web and mobile apps run the same state logic. The web app wraps it with redux-persist and deliberately excludes the profile, snapshot, celebration, mood, notebook, studyCycles and quickNotes slices, so no PII, journal text, study notes or ideas land in localStorage. The mobile app keeps its store in memory and refetches on launch.
flowchart LR
ACT["✅ Check habit/task<br/>in routine"] --> XP["🎮 XP Calculator"]
XP --> UXP["👤 User XP<br/>+ level up"]
XP --> HXP["💪 Habit XP<br/>+ level up"]
XP --> CXP["📂 Category XP<br/>+ level up"]
XP --> RXP["📋 Routine XP<br/>+ level up"]
ACT --> STR["🔥 Constance<br/>streak tracking"]