Architecture Overview
System Architecture
Feel Pro Club (FPC) is a cross-platform sports community application maintained as a pnpm monorepo with four workspaces. The repository uses atomic cross-workspace changes and one dependency-aware CI pipeline.
┌─────────────────────────────────────────────────────────────────┐
│ @feelproclub/shared │
│ TypeScript types, Zod schemas, constants │
│ (internal pnpm workspace) │
└─────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌──────────────────────────────┐ ┌──────────────────────────────┐
│ fpc-app │ │ fpc-api │
│ React 19 + Vite + Capacitor │ │ NestJS 11 + Prisma │
│ (Frontend) │◄─►│ (Backend) │
│ │ │ │
│ Cloudflare Pages (web) + │ │ Cloud Run (development) / │
│ App Stores │ │ Railway (production) │
└──────────────────────────────┘ └──────────────────────────────┘
│ │
▼ ▼
┌──────────────────────────────┐ ┌──────────────────────────────┐
│ WorkOS Auth; production │ │ Cloud SQL (development) / │
│ Storage, PostHog, Sentry │ │ Supabase PostgreSQL (prod) │
└──────────────────────────────┘ └──────────────────────────────┘Environment Topology
| Boundary | Development | Production |
|---|---|---|
| API hosting | Cloud Run | Railway |
| Database | Cloud SQL PostgreSQL | Supabase PostgreSQL |
| Authentication | WorkOS with client-scoped JWKS validation | WorkOS with client-scoped JWKS validation |
| Object storage | GCS public assets | Supabase Storage |
The development API connects to private Cloud SQL through an IAM-authenticated proxy sidecar. Its GCS implementation persists opaque team-logo object keys and derives public URLs dynamically. Production stores team logos in Supabase Storage and persists their URLs.
Workspace Layout and Responsibilities
apps/
├── api/ # fpc-api package
└── app/ # fpc-app package
packages/
└── shared/ # @feelproclub/shared package
docs/ # fpc-docs package| Path | Package | Purpose | Stack | Delivery |
|---|---|---|---|---|
packages/shared | @feelproclub/shared | Type definitions, Zod schemas, constants | TypeScript 5.9, Zod 4, tsup | Built inside the workspace |
apps/api | fpc-api | REST API, business logic, database | NestJS 11, Prisma 6, Express | Cloud Run (dev); Railway (prod) |
apps/app | fpc-app | UI, state management, mobile | React 19, Vite 7, Capacitor 8 | Cloudflare Pages + app stores |
docs | fpc-docs | PRDs, ADRs, architecture docs | Markdown, VitePress | Built in CI |
Type Sharing Strategy
All cross-package types follow a single-source-of-truth pattern:
- Define canonical TypeScript interfaces and Zod schemas in
packages/shared - Build
@feelproclub/sharedas CJS, ESM, and.d.tsinside the pnpm workspace - Both
fpc-apiandfpc-appimport from@feelproclub/shared - Never duplicate type definitions between packages
Shared Surface
Shared exports canonical auth, user, player, free-player, invitation, team, match, notification, and home contracts; their Zod schemas; shared constants; and small pure completion/readiness helpers. Consumers import from the package root rather than source paths.
Data Flow
User Action → React Component → TanStack Query → Axios (+ JWT) → HTTP Request
│
▼
NestJS Controller
(Swagger + DTOs)
│
▼
NestJS Service
(Business Logic)
│
▼
Prisma ORM → PostgreSQL
(Cloud SQL in development; Supabase in production)Authentication Flow
FPC uses WorkOS AuthKit with client-scoped JWKS validation — no shared JWT secret is used. User.id is always an application-generated UUID, never a WorkOS subject; an AuthIdentity row maps (issuer, subject) to that UUID (ADR 0008).
┌─────────┐ ┌──────────┐ ┌─────────┐ ┌──────────────┐
│ User │────►│ WorkOS │────►│ fpc-app │────►│ fpc-api │
│ │ │ Auth │ │ │ │ │
└─────────┘ └──────────┘ └─────────┘ └──────────────┘
│ │ │ │
│ 1. Sign in │ │ │
│──────────────►│ │ │
│ │ │ │
│ 2. Access token │ │
│◄──────────────│ │ │
│ │ │ │
│ 3. Store token in session │ │
│───────────────────────────────►│ │
│ │ │ │
│ 4. API request + Bearer token │ │
│──────────────────────────────────────────────────►│
│ │ │ │
│ │ │ 5. Verify via │
│ │ │ JWKS endpoint│
│ │◄──────────────────────────────────│
│ │ │ │
│ │ 6. Public key │ │
│ │──────────────────────────────────►│
│ │ │ │
│ 7. Response │ │ 8. Return data │
│◄──────────────────────────────────────────────────│Authentication Details
- Token format: WorkOS access token with
sub(WorkOS subject),sid(session id),iss,exp,iat, and deliberately noaudclaim - JWKS validation:
jwks-rsalibrary against WorkOS's client-scoped JWKS endpoint (https://api.workos.com/sso/jwks/<WORKOS_CLIENT_ID>), with a 5-key cache (24h TTL, 10 req/min rate limit); the client id in the JWKS URL is what scopes verification to this application, since every WorkOS customer shares the same issuer - Algorithm: RS256
- Identity resolution:
IdentityResolutionServiceresolves the verified(issuer, subject)to an application-ownedUser.idviaAuthIdentity, provisioning a newUser/AuthIdentitypair idempotently on first login and failing closed on a revoked identity - Guard:
JwtAuthGuardverifies the bearer token through theTokenVerifierseam, resolves identity, and attachesAuthenticatedUserto the request - Decorator:
@CurrentUser()for accessing authenticated user in controllers - Frontend: a WorkOS AuthKit adapter under
src/auth/(behind the platform capability seam of ADR 0014) manages the session; the Axios request interceptor injects the current access token - 401 handling: Axios response interceptor signs user out on unauthorized responses
- Webhooks:
POST /webhooks/workosconsumes signeduser.created/user.updated/user.deletedevents through a durable, deduplicated inbox;user.deletedrevokes theAuthIdentitymapping without touching domain data
Backend Architecture (apps/api)
Module Structure
src/
├── main.ts # Bootstrap: Helmet, CORS, Swagger, validation pipes
├── app.module.ts # Root module: global providers + feature imports
├── config/
│ └── configuration.ts # Centralized env-based config
├── common/ # Cross-cutting concerns
│ ├── audit/ # AuditModule (global) — structured action logging
│ ├── auth/ # AuthModule (global) — TokenVerifier seam, WorkosTokenVerifier, IdentityResolutionService
│ ├── database/ # DatabaseModule (global) — PrismaService
│ ├── decorators/ # @CurrentUser() parameter decorator
│ ├── filters/ # AllExceptionsFilter (Sentry integration)
│ ├── guards/ # JwtAuthGuard
│ ├── services/ # PrismaService, StorageService, AuditService
│ ├── types/ # AuthenticatedUser, AuthenticatedRequest
│ └── utils/ # sanitize.ts, validate-env.ts
└── modules/ # Feature modules
└── health/ # HealthModule — liveness/readiness checksGlobal Middleware & Configuration
| Layer | Implementation |
|---|---|
| Security | Helmet HTTP headers |
| CORS | Dynamic origins from env var; dev mode allows any localhost port |
| Rate Limiting | @nestjs/throttler: 100 requests/minute globally |
| Validation | Global ValidationPipe: whitelist, transform, forbidNonWhitelisted |
| Error Handling | AllExceptionsFilter with Sentry capture + structured responses |
| API Docs | Swagger at /api/docs (disabled in production) |
| Monitoring | Sentry with profiling (conditional on SENTRY_DSN) |
| Audit | Structured audit logging for sensitive operations |
Audit System
The AuditService tracks sensitive operations with structured JSON logging.
File Storage
Development uses StorageService with GCS at runtime and an in-memory client only in tests. Development teams persist an opaque logoObjectKey, and the API derives logoUrl dynamically. Development team logos are immutable public objects. Production stores team logos in Supabase Storage and persists their URLs. The API does not provide private-media features.
Prisma Conventions
- Model names:
PascalCase - Table names:
snake_casevia@@map("table_name") - Column names:
camelCasein Prisma, mapped tosnake_casevia@map("column_name") - IDs: UUID with
@default(uuid()) - Timestamps:
createdAtandupdatedAton every model
Frontend Architecture (apps/app)
Application Layer Stack
┌─────────────────────────────────────────────┐
│ React Router v7 │
│ (BrowserRouter) │
├─────────────────────────────────────────────┤
│ Error Boundary (Sentry) │
├─────────────────────────────────────────────┤
│ QueryClientProvider │
│ (TanStack Query: 5min stale, 2 retries) │
├─────────────────────────────────────────────┤
│ PostHogProvider │
│ (pageview tracking via hook) │
├─────────────────────────────────────────────┤
│ App Routes │
│ / → /home (redirect) │
└─────────────────────────────────────────────┘State Management
| Layer | Tool | Storage | Purpose |
|---|---|---|---|
| Server state | TanStack Query | In-memory cache | API data fetching/caching |
| Client state | Zustand | Memory + localStorage persist | Theme, sidebar state |
| Form state | React Hook Form | Component-local | Form inputs + validation |
| Auth state | WorkOS AuthKit + useAuth | Platform-specific token storage | User/session tracking |
Design System
| Property | Value |
|---|---|
| Primary | Lime green (oklch(0.8537 0.1749 149.35)) |
| Base | Stone (warm neutral) |
| Font | Hanken Grotesk (Google Fonts) |
| Radius | 0.625rem (with sm through pill variants) |
| Color space | OKLCH |
| Dark mode | .dark class on <html>, persisted to localStorage |
| Icons | Lucide React |
Component Library
- shadcn/ui components in
src/components/ui/ - Custom FPC components in
src/components/custom/ - Add new shadcn components:
pnpm dlx shadcn@latest add [name]
i18n
- Library: i18next + react-i18next
- Languages: Spanish (es, default) and English (en)
- Translation files:
src/locales/{en,es}.json
Mobile (Capacitor)
- App ID:
com.feelproclub.app - Web Dir:
dist(Vite build output) - Android Scheme:
https - Splash Screen: 2s duration, black background
- Deep Linking: ChottuLink for deferred deep links
Security Architecture
Input Validation
Request → ValidationPipe (class-validator) → Controller → Service → Prisma
│
├── whitelist: true (strip unknown props)
├── transform: true (auto-convert types)
└── forbidNonWhitelisted: true (reject unknown)XSS Prevention
| Package | Environment | Usage |
|---|---|---|
| isomorphic-dompurify | Node.js (API) | sanitizeText() strips all HTML tags |
| DOMPurify | Browser (App) | Client-side sanitization |
File Upload Security
Five validation layers: file size → MIME type → magic number → image dimensions → filename sanitization.
Rate Limiting
Global throttle: 100 requests per 60 seconds via @nestjs/throttler.
CORS
- Production: explicit allowed origins from
CORS_ORIGINenv var (comma-separated) - Development: any
localhost:*port allowed
HTTP Security Headers
Helmet middleware applied globally for standard HTTP security headers.
Monitoring & Observability
| Service | Package | Usage | Environment |
|---|---|---|---|
| Sentry | @sentry/nestjs | Backend error tracking + profiling | API |
| Sentry | @sentry/react | Frontend error boundary + browser tracing | App |
| PostHog | posthog-js | Product analytics + pageview tracking | App |
Sentry Configuration (Frontend)
- Trace sample rate: 10% (prod), 100% (dev)
- Session replay: 10% on errors, 10% for sessions (prod)
- Browser tracing and replay integrations enabled
Environment Variables
Development API (apps/api)
| Variable | Required | Purpose | Default |
|---|---|---|---|
DATABASE_URL | Yes | PostgreSQL connection string | — |
DATABASE_URL_MIGRATE | Yes | Migration PostgreSQL URL | — |
WORKOS_CLIENT_ID | Yes | WorkOS client id; scopes JWKS-based token verification to this application | — |
WORKOS_API_KEY | Yes | WorkOS secret API key for server-to-server calls (user profile lookups, webhook processing) | — |
WORKOS_WEBHOOK_SECRET | Yes | WorkOS webhook signing secret | — |
GCS_PUBLIC_ASSETS_BUCKET | Required | GCS bucket for team logos | — |
SENTRY_DSN | Recommended | Error monitoring | — |
CORS_ORIGIN | Recommended | Allowed CORS origins | http://localhost:5173 |
PORT | No | Server port | 3000 |
NODE_ENV | No | Environment | development |
App (apps/app)
| Variable | Required | Purpose |
|---|---|---|
VITE_API_URL | Yes | Backend API URL |
VITE_WORKOS_CLIENT_ID | Yes | WorkOS client id |
VITE_SENTRY_DSN | Optional | Error monitoring |
VITE_PUBLIC_POSTHOG_KEY | Optional | Product analytics |
VITE_PUBLIC_POSTHOG_HOST | Optional | PostHog endpoint |
Deployment
┌──────────────┐ ┌────────────────┐ ┌────────────────────────┐
│ Developer │────►│ GitHub Actions │────►│ Deployment │
│ (push) │ │ (CI/CD) │ │ │
└──────────────┘ └────────────────┘ │ fpc-app → Cloudflare │
│ Pages │
│ fpc-api → Cloud Run │
│ (dev); Railway (prod) │
└────────────────────────┘| Component | Development | Production |
|---|---|---|
fpc-app | Cloudflare Pages | Cloudflare Pages |
fpc-api | Cloud Run | Railway |
fpc-shared | Internal pnpm workspace | Internal pnpm workspace |
fpc-docs | Documentation artifact | Documentation artifact |
| Database | Cloud SQL PostgreSQL | Supabase PostgreSQL |
| Authentication | WorkOS with client-scoped JWKS validation | WorkOS with client-scoped JWKS validation |
| Object storage | GCS public team-logo assets | Supabase Storage |
The root CI workflow detects affected paths. Root/workspace configuration changes run all jobs; Shared changes also run API and App checks; workspace-only changes run their scoped checks. The always-running aggregate check is CI.
Development Workflow
Spec-Driven Development
┌──────────────┐
│ Write PRD │ ← PRD in docs/prd/ (SOURCE OF TRUTH)
└──────────────┘
│
▼
┌──────────────┐
│ Figma Design │ ← Optional, for UI features
│ (optional) │
└──────────────┘
│
▼
┌──────────────┐
│ Implement │ ← packages/shared → apps/api → apps/app
│ Feature │
└──────────────┘
│
▼
┌──────────────┐
│ Test & │ ← Unit → Integration → E2E
│ Review │
└──────────────┘Implementation Order
- packages/shared — Types and Zod schemas (if new data structures)
- apps/api — Backend endpoints, business logic, database migrations
- apps/app — Frontend components, state management, UI
- Tests — At each level, mapped to spec acceptance criteria
Task Orchestration and Caching
Turborepo (turbo.json at the repo root) runs the build, lint, test, and test:e2e tasks across the four workspaces, with a packages/shared → apps/api/apps/app dependency edge so Shared builds before its consumers. Turborepo also provides local filesystem caching for these tasks, skipping work for workspaces whose declared inputs are unchanged. CI's per-path job gating (scripts/ci/detect-changes.mjs) is unchanged and still decides which GitHub Actions jobs run; Turborepo operates inside those jobs at the task level. See ADR 0012 for the full rationale.
Testing Strategy
▲
╱ ╲
╱ ╲ E2E (Playwright)
╱ ╲ - Critical user flows
╱ ╲ - Cross-system integration
╱─────────╲
╱ ╲ Integration (Jest/Supertest)
╱ ╲ - API endpoint testing
╱ ╲ - Database operations
╱ ╲
╱───────────────────╲
╱ ╲ Unit (Jest/Vitest)
╱ ╲ - Business logic
╱ ╲ - Component rendering
╱ ╲
╱─────────────────────────────╲| Level | Framework | Package | Location | Command |
|---|---|---|---|---|
| Unit | Jest | fpc-api | test/unit/ | pnpm test |
| Unit | Vitest | fpc-app | Component directories | pnpm test |
| Integration | Jest/Supertest | fpc-api | test/e2e/ | pnpm test:e2e |
| E2E | Playwright | fpc-app | e2e/ | pnpm test:e2e |
| Type check | TypeScript | fpc-shared | — | pnpm typecheck |