# System Overview

## Tech Stack

- **Runtime:** Bun (TypeScript)
- **Framework:** Elysia.js
- **API:** GraphQL via GraphQL Yoga (`@elysiajs/graphql-yoga`)
- **Database:** PostgreSQL (Hetzner-hosted)
- **ORM:** Drizzle ORM with `postgres` driver
- **Auth:** Arctic (Google + Microsoft OAuth), oslo crypto for session tokens
- **Validation:** Zod + `@t3-oss/env-core` for environment variables

## Architecture

Modular monolith organized by domain:

```
src/
├── config/          # Environment validation and app configuration
├── db/
│   ├── schema/      # Drizzle table definitions (users, rbac, invitations, audit, catalog, campaigns)
│   ├── migrations/  # Generated SQL migrations
│   ├── client.ts    # Database connection pool
│   └── seed.ts      # Bootstrap Superadmin + permissions
├── modules/
│   ├── auth/        # OAuth providers, session management, auth routes
│   ├── users/       # User CRUD, invitation lifecycle
│   ├── rbac/        # Roles, permissions, guards, permission registry
│   ├── catalog/     # Product types, wine types, and products
│   ├── campaigns/   # Marketing campaigns and product-linked campaign state
│   └── audit/       # Audit log writer and query service
├── graphql/
│   ├── schema.ts    # GraphQL type definitions
│   ├── resolvers.ts # All query and mutation resolvers
│   └── context.ts   # Request context builder (auth + permissions)
└── index.ts         # Elysia app entry point
```

## Module Boundaries

| Module | Responsibility | Depends on |
|--------|---------------|------------|
| `auth` | OAuth flow, sessions, login/logout | `users`, `audit` |
| `users` | User records, invitations, activation | `audit` |
| `rbac` | Roles, permissions, authorization guards | — |
| `catalog` | Product types, wine types, products | `audit` |
| `campaigns` | Campaign CRUD, reusable colors, product-linked marketing data | `catalog`, `users`, `audit` |
| `audit` | Append-only log, query service | — |
| `graphql` | Schema, resolvers, context | All modules |

## Frontend-Relevant Domain Notes

- Campaigns now own their own `title`, `vintage`, and `color` fields.
- A campaign may optionally reference a product through `productId`.
- Product selection is a source of initial values, not the source of truth for campaign display data.
- Reusable campaign colors are available through a dedicated GraphQL query.

See [campaigns.md](campaigns.md) for the frontend-facing contract and behavior details.

## Deployment

- **Hosting:** Hetzner dedicated server with Docker
- **Database:** PostgreSQL on Hetzner
- **Entry point:** `bun run src/index.ts`
- **Port:** Configured via `PORT` env var (default: 3000)
- **Health check:** `GET /health` returns `{ status: "ok", timestamp: "..." }`

## Environment Variables

All required env vars are validated at startup via Zod:

| Variable | Description |
|----------|-------------|
| `DATABASE_URL` | PostgreSQL connection string |
| `ENABLE_E2E_TEST_MODE` | Enables request-scoped frontend e2e isolation via `X-Test-run-id` |
| `GOOGLE_CLIENT_ID` | Google OAuth client ID |
| `GOOGLE_CLIENT_SECRET` | Google OAuth client secret |
| `GOOGLE_REDIRECT_URI` | Google OAuth callback URL |
| `MICROSOFT_CLIENT_ID` | Microsoft Entra ID client ID |
| `MICROSOFT_CLIENT_SECRET` | Microsoft Entra ID client secret |
| `MICROSOFT_REDIRECT_URI` | Microsoft OAuth callback URL |
| `MICROSOFT_TENANT_ID` | Microsoft Entra tenant ID |
| `SESSION_SECRET` | Min 32-char secret for session operations |
| `CORS_ORIGIN` | Allowed CORS origin (default: `http://localhost:43173`) |
| `PORT` | Server port (default: 3000) |
| `NODE_ENV` | `development`, `production`, or `test` |
