Architecture Overview

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.

Tech stack

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 topology

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:

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.

Monitoring

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.

Domain model

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"

Entity highlights

Authentication flow

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

Token details

API layer

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/*

Request/response pattern

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"]

State management (frontend)

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.

Gamification system

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"]