Skip to content

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.

text
┌─────────────────────────────────────────────────────────────────┐
│                    @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

BoundaryDevelopmentProduction
API hostingCloud RunRailway
DatabaseCloud SQL PostgreSQLSupabase PostgreSQL
AuthenticationWorkOS with client-scoped JWKS validationWorkOS with client-scoped JWKS validation
Object storageGCS public assetsSupabase 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

text
apps/
├── api/                 # fpc-api package
└── app/                 # fpc-app package
packages/
└── shared/              # @feelproclub/shared package
docs/                    # fpc-docs package
PathPackagePurposeStackDelivery
packages/shared@feelproclub/sharedType definitions, Zod schemas, constantsTypeScript 5.9, Zod 4, tsupBuilt inside the workspace
apps/apifpc-apiREST API, business logic, databaseNestJS 11, Prisma 6, ExpressCloud Run (dev); Railway (prod)
apps/appfpc-appUI, state management, mobileReact 19, Vite 7, Capacitor 8Cloudflare Pages + app stores
docsfpc-docsPRDs, ADRs, architecture docsMarkdown, VitePressBuilt in CI

Type Sharing Strategy

All cross-package types follow a single-source-of-truth pattern:

  1. Define canonical TypeScript interfaces and Zod schemas in packages/shared
  2. Build @feelproclub/shared as CJS, ESM, and .d.ts inside the pnpm workspace
  3. Both fpc-api and fpc-app import from @feelproclub/shared
  4. 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

text
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).

text
┌─────────┐     ┌──────────┐     ┌─────────┐     ┌──────────────┐
│  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 no aud claim
  • JWKS validation: jwks-rsa library 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: IdentityResolutionService resolves the verified (issuer, subject) to an application-owned User.id via AuthIdentity, provisioning a new User/AuthIdentity pair idempotently on first login and failing closed on a revoked identity
  • Guard: JwtAuthGuard verifies the bearer token through the TokenVerifier seam, resolves identity, and attaches AuthenticatedUser to 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/workos consumes signed user.created/user.updated/user.deleted events through a durable, deduplicated inbox; user.deleted revokes the AuthIdentity mapping without touching domain data

Backend Architecture (apps/api)

Module Structure

text
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 checks

Global Middleware & Configuration

LayerImplementation
SecurityHelmet HTTP headers
CORSDynamic origins from env var; dev mode allows any localhost port
Rate Limiting@nestjs/throttler: 100 requests/minute globally
ValidationGlobal ValidationPipe: whitelist, transform, forbidNonWhitelisted
Error HandlingAllExceptionsFilter with Sentry capture + structured responses
API DocsSwagger at /api/docs (disabled in production)
MonitoringSentry with profiling (conditional on SENTRY_DSN)
AuditStructured 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_case via @@map("table_name")
  • Column names: camelCase in Prisma, mapped to snake_case via @map("column_name")
  • IDs: UUID with @default(uuid())
  • Timestamps: createdAt and updatedAt on every model

Frontend Architecture (apps/app)

Application Layer Stack

text
┌─────────────────────────────────────────────┐
│              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

LayerToolStoragePurpose
Server stateTanStack QueryIn-memory cacheAPI data fetching/caching
Client stateZustandMemory + localStorage persistTheme, sidebar state
Form stateReact Hook FormComponent-localForm inputs + validation
Auth stateWorkOS AuthKit + useAuthPlatform-specific token storageUser/session tracking

Design System

PropertyValue
PrimaryLime green (oklch(0.8537 0.1749 149.35))
BaseStone (warm neutral)
FontHanken Grotesk (Google Fonts)
Radius0.625rem (with sm through pill variants)
Color spaceOKLCH
Dark mode.dark class on <html>, persisted to localStorage
IconsLucide 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

text
Request → ValidationPipe (class-validator) → Controller → Service → Prisma

                 ├── whitelist: true (strip unknown props)
                 ├── transform: true (auto-convert types)
                 └── forbidNonWhitelisted: true (reject unknown)

XSS Prevention

PackageEnvironmentUsage
isomorphic-dompurifyNode.js (API)sanitizeText() strips all HTML tags
DOMPurifyBrowser (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_ORIGIN env var (comma-separated)
  • Development: any localhost:* port allowed

HTTP Security Headers

Helmet middleware applied globally for standard HTTP security headers.

Monitoring & Observability

ServicePackageUsageEnvironment
Sentry@sentry/nestjsBackend error tracking + profilingAPI
Sentry@sentry/reactFrontend error boundary + browser tracingApp
PostHogposthog-jsProduct analytics + pageview trackingApp

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)

VariableRequiredPurposeDefault
DATABASE_URLYesPostgreSQL connection string
DATABASE_URL_MIGRATEYesMigration PostgreSQL URL
WORKOS_CLIENT_IDYesWorkOS client id; scopes JWKS-based token verification to this application
WORKOS_API_KEYYesWorkOS secret API key for server-to-server calls (user profile lookups, webhook processing)
WORKOS_WEBHOOK_SECRETYesWorkOS webhook signing secret
GCS_PUBLIC_ASSETS_BUCKETRequiredGCS bucket for team logos
SENTRY_DSNRecommendedError monitoring
CORS_ORIGINRecommendedAllowed CORS originshttp://localhost:5173
PORTNoServer port3000
NODE_ENVNoEnvironmentdevelopment

App (apps/app)

VariableRequiredPurpose
VITE_API_URLYesBackend API URL
VITE_WORKOS_CLIENT_IDYesWorkOS client id
VITE_SENTRY_DSNOptionalError monitoring
VITE_PUBLIC_POSTHOG_KEYOptionalProduct analytics
VITE_PUBLIC_POSTHOG_HOSTOptionalPostHog endpoint

Deployment

text
┌──────────────┐     ┌────────────────┐     ┌────────────────────────┐
│   Developer  │────►│ GitHub Actions │────►│       Deployment       │
│    (push)    │     │    (CI/CD)     │     │                        │
└──────────────┘     └────────────────┘     │  fpc-app → Cloudflare  │
                                            │         Pages          │
                                            │  fpc-api → Cloud Run   │
                                            │ (dev); Railway (prod)  │
                                            └────────────────────────┘
ComponentDevelopmentProduction
fpc-appCloudflare PagesCloudflare Pages
fpc-apiCloud RunRailway
fpc-sharedInternal pnpm workspaceInternal pnpm workspace
fpc-docsDocumentation artifactDocumentation artifact
DatabaseCloud SQL PostgreSQLSupabase PostgreSQL
AuthenticationWorkOS with client-scoped JWKS validationWorkOS with client-scoped JWKS validation
Object storageGCS public team-logo assetsSupabase 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

text
        ┌──────────────┐
        │  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

  1. packages/shared — Types and Zod schemas (if new data structures)
  2. apps/api — Backend endpoints, business logic, database migrations
  3. apps/app — Frontend components, state management, UI
  4. 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/sharedapps/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

text

                   ╱ ╲
                  ╱   ╲  E2E (Playwright)
                 ╱     ╲  - Critical user flows
                ╱       ╲  - Cross-system integration
               ╱─────────╲
              ╱           ╲  Integration (Jest/Supertest)
             ╱             ╲  - API endpoint testing
            ╱               ╲  - Database operations
           ╱                 ╲
          ╱───────────────────╲
         ╱                     ╲  Unit (Jest/Vitest)
        ╱                       ╲  - Business logic
       ╱                         ╲  - Component rendering
      ╱                           ╲
     ╱─────────────────────────────╲
LevelFrameworkPackageLocationCommand
UnitJestfpc-apitest/unit/pnpm test
UnitVitestfpc-appComponent directoriespnpm test
IntegrationJest/Supertestfpc-apitest/e2e/pnpm test:e2e
E2EPlaywrightfpc-appe2e/pnpm test:e2e
Type checkTypeScriptfpc-sharedpnpm typecheck
  • PRDs — Product Requirement Documents
  • ADRs — Architectural Decision Records