Club London Docs
Technical documentation for the premium luxury club mobile application.
clublondon.app
Expo SDK 54
NestJS 11
PostgreSQL 16
Turborepo
▣ Architecture Overview
High-level infrastructure and service topology.
clublondon.app · *.clublondon.app
┌────────────────────────┐
│ Hetzner CPX22 │
│ Helsinki (cl-app) │
│ │
│ ┌────────────────┐ │
│ │ Traefik v3 │ │
│ │ (SSL auto) │ │
│ └────┬──────┬────┘ │
│ │ │ │
│ ┌────┴─┐ ┌──┴────┐ │
│ │ API │ │ Admin │ │
│ │ :3000 │ │ :80 │ │
│ └───┬──┘ └───────┘ │
│ │ │
│ ┌───┴───┐ ┌──────┐ │
│ │Postgres│ │Redis │ │
│ │ 16 │ │ 7 │ │
│ └───────┘ └──────┘ │
└────────────────────────┘
┌────────────────────────┐
│ Expo / EAS Build │
│ iOS + Android apps │
│ → App Store / Play │
└────────────────────────┘
Domains
| Domain | Service |
clublondon.app | Landing page |
api.clublondon.app | NestJS API |
admin.clublondon.app | React admin panel |
staging-api.clublondon.app | Staging API |
📁 Monorepo Structure
Turborepo-powered workspace organization.
clublondon/
├── apps/
│ ├── mobile/
│ │ ├── app/
│ │ │ ├── (auth)/
│ │ │ ├── (tabs)/
│ │ │ └── *.tsx
│ │ ├── assets/images/
│ │ ├── lib/
│ │ └── app.json
│ │
│ ├── admin/
│ │ ├── src/
│ │ │ ├── App.tsx
│ │ │ ├── api.ts
│ │ │ └── main.tsx
│ │ ├── Dockerfile
│ │ └── nginx.conf
│ │
│ └── api/
│ ├── src/
│ │ ├── modules/
│ │ ├── drizzle/
│ │ ├── seed.ts
│ │ ├── app.module.ts
│ │ └── main.ts
│ └── Dockerfile
│
├── packages/
│ ├── shared-types/
│ └── validators/
│
├── infrastructure/
│ ├── docker/
│ ├── cloudflare/
│ └── hetzner/
│
├── skills/
│ └── clublondon-deploy/
│
├── .github/workflows/
├── eas.json
├── turbo.json
├── pnpm-workspace.yaml
└── CLAUDE.md
⚙ Technology Stack
Complete list of technologies across all layers.
Mobile App
| Layer | Technology | Version |
| Framework | Expo SDK | 54.0.33 |
| UI | React Native | 0.81.5 |
| Navigation | Expo Router | 6.0.23 |
| State | Zustand | 5.0.0 |
| Data Fetching | TanStack Query | 5.60.0 |
| Forms | React Hook Form | 7.53.0 |
| Validation | Zod | 3.24.0 |
| Icons | Lucide React Native | 0.460.0 |
| Animations | Reanimated | 4.1.7 |
| Secure Storage | expo-secure-store | 15.0.8 |
| Real-time | Socket.io Client | 4.8.0 |
Admin Panel
| Layer | Technology | Version |
| Framework | React | 19.0.0 |
| Build | Vite | 6.0.0 |
| Routing | Hash-based (custom) | - |
| Data Fetching | TanStack Query | 5.60.0 |
| CSS | Inline styles (dark theme) | - |
| Icons | Lucide React | 0.460.0 |
API Backend
| Layer | Technology | Version |
| Framework | NestJS | 11.0.0 |
| HTTP | Fastify | - |
| ORM | Drizzle ORM | 0.38.0 |
| Database | PostgreSQL | 16 |
| Cache | Redis | 7 |
| Queue | BullMQ | 5.0.0 |
| Auth | Passport JWT | 4.0.0 |
| Storage | Cloudflare R2 (S3) | - |
| Real-time | Socket.io | - |
| Validation | Zod + class-validator | - |
Infrastructure
| Component | Technology |
| Server | Hetzner CPX22 (Helsinki) |
| Reverse Proxy | Traefik v3 (auto SSL) |
| CDN/DNS | Cloudflare |
| Container | Docker + Docker Compose |
| CI/CD | GitHub Actions |
| Mobile Builds | EAS Build |
| Package Manager | pnpm 9.15.0 |
| Monorepo | Turborepo 2.3.0 |
▶ Getting Started
Set up your local development environment.
Prerequisites
- Node.js >= 20
- pnpm >= 9.15.0
- Docker & Docker Compose
- Expo CLI (
npm install -g eas-cli)
Installation
git clone https://github.com/alegkoshyk/clublondon.git
cd clublondon
pnpm install
docker compose -f infrastructure/docker/docker-compose.dev.yml up -d
cp .env.example .env.local
pnpm db:push
cd apps/api && npx ts-node src/seed.ts && cd ../..
pnpm dev
Development URLs
| App | URL |
| Mobile (web) | http://localhost:8081 |
| Admin | http://localhost:5173 |
| API | http://localhost:3000/api/v1 |
| Drizzle Studio | pnpm db:studio |
Test Accounts
📱 Mobile App (Expo)
30 screens built with Expo SDK 54 and React Native.
Auth Flow
| Screen | File | Status |
| Login | (auth)/login.tsx | API connected |
| Register | (auth)/register.tsx | API connected |
| Onboarding | (auth)/onboarding.tsx | Saves interests to API |
Tab Navigation (5 tabs)
| Tab | File | Data Source |
| Home/Explore | (tabs)/index.tsx | GET /events?limit=3 |
| Events | (tabs)/events.tsx | GET /events?limit=20 |
| Travel | (tabs)/travel.tsx | GET /travel?limit=20 |
| Matching | (tabs)/matching.tsx | GET /users?limit=10 |
| Profile | (tabs)/profile.tsx | Auth store (user data) |
Detail Screens
| Screen | File | API |
| Event Detail | event-detail.tsx | GET /events/:id + POST /events/:id/rsvp |
| Trip Detail | trip-detail.tsx | GET /travel/:id + POST /travel/:id/book |
| Match Profile | match-profile.tsx | User data from navigation params |
Feature Screens
| Screen | File | API |
| Wallet | wallet.tsx | GET /wallet + GET /wallet/balance |
| Concierge | concierge.tsx | GET /concierge/my |
| New Request | new-request.tsx | POST /concierge |
| Notifications | notifications.tsx | GET /notifications + POST /notifications/read-all |
| Edit Profile | edit-profile.tsx | PATCH /users/me |
| Loyalty | loyalty.tsx | Auth store (points, tier) |
| Club Card | club-card.tsx | Auth store (user data) |
| Settings | settings.tsx | Local state + logout |
Other Screens
Concierge Chat
concierge-chat.tsx
Send Payment
send-payment.tsx
Crypto Vault
crypto-vault.tsx
Payment Methods
payment-methods.tsx
Special Offers
special-offers.tsx
World Agenda
world-agenda.tsx
Booking Confirmed
booking-confirmed.tsx
Request Membership
request-membership.tsx
State Management
- Auth: Zustand store with JWT in SecureStore
- Data: TanStack Query for server state
- Forms: React Hook Form + Zod validation
Theme
const colors = {
bg: '#0D0D1A',
bg2: '#1A1A2E',
bg3: '#16213E',
gold: '#C4A35A',
gold2: '#D4B96A',
white: '#FFFFFF',
green: '#4CAF50',
red: '#F44336',
}
const fonts = {
display: 'Cormorant Garamond',
body: 'DM Sans',
}
EAS Build Profiles
| Profile | Distribution | Use Case |
development | Internal | Dev client with debugger |
preview | Internal | QA testing |
production | Store | App Store / Google Play |
eas build --profile preview --platform all
eas build --profile production --platform ios
eas submit --platform ios --latest
eas update --branch production --message "hotfix: ..."
💻 Admin Panel (React)
React 19 admin dashboard with hash-based routing.
URL Routing
| URL | Page | Description |
#/dashboard | Dashboard | KPIs, activity chart, tier distribution |
#/members | Members | Table + edit (tier/points/status) |
#/events | Events | Full CRUD + publish/unpublish |
#/travel | Travel | Full CRUD + publish/unpublish |
#/concierge | Concierge | Status management (6 statuses) |
#/loyalty | Loyalty | Tier distribution, top holders |
#/matching | Matching | Active profiles grid |
#/notifications | Notifications | Read/unread management |
#/cms | CMS | Pages, Blog, Media (coming soon) |
#/settings | Settings | General config + infrastructure |
CRUD Operations
| Entity | Create | Read | Update | Delete | Publish |
| Events | Yes | Yes | Yes | Yes | Yes |
| Travel | Yes | Yes | Yes | Yes | Yes |
| Members | - | Yes | tier/points | - | - |
| Concierge | - | Yes | status | - | - |
Login
- URL:
admin.clublondon.app
- Auth: JWT (stored in localStorage)
- Credentials: same as API accounts
⚡ API Backend (NestJS)
NestJS 11 with Fastify adapter and modular architecture.
Modules
| Module | Controller Prefix | Description |
| AuthModule | /auth | Login, register, refresh, logout |
| UsersModule | /users | User CRUD, profile, stats |
| EventsModule | /events | Events CRUD, RSVP, stats |
| TravelModule | /travel | Trips CRUD, booking |
| ConciergeModule | /concierge | Requests, status updates |
| WalletModule | /wallet | Balance, transactions |
| NotificationsModule | /notifications | CRUD, mark read |
Middleware & Security
- CORS: Configured for admin, mobile, localhost, Expo
- Helmet: Security headers
- Validation: Global ValidationPipe (whitelist + transform)
- Auth: JWT Bearer tokens via Passport.js
- Global Prefix:
/api/v1
🗃 Database Schema
PostgreSQL 16 with Drizzle ORM. 9 tables total.
users
| Column | Type | Notes |
id | UUID | Primary key |
email | VARCHAR | Unique |
passwordHash | VARCHAR | bcrypt |
firstName, lastName | VARCHAR | |
phone, avatarUrl, bio, location | VARCHAR | Optional |
membershipTier | VARCHAR | standard / silver / gold / platinum / diamond |
loyaltyPoints | INTEGER | Default: 0 |
interests | JSONB | Array of strings |
isVerified, isActive | BOOLEAN | |
createdAt, updatedAt, deletedAt | TIMESTAMP | Soft delete |
events
| Column | Type | Notes |
id | UUID | Primary key |
title, description | VARCHAR/TEXT | |
location, address | VARCHAR | |
startDate, endDate | TIMESTAMP | |
category | VARCHAR | |
tags | JSONB | Array of strings |
maxGuests, priceGbp, pricePoints | INTEGER | |
isFeatured, isPublished | BOOLEAN | |
rsvpCount | INTEGER | Computed from rsvps table |
trips
| Column | Type | Notes |
id | UUID | Primary key |
title, description, destination, location | VARCHAR | |
startDate, endDate | TIMESTAMP | |
tags | JSONB | Array |
maxGuests, priceGbp, pricePoints | INTEGER | |
itinerary | JSONB | Array of {day, description} |
isFeatured, isPublished | BOOLEAN | |
rsvps
| Column | Type | Notes |
userId, eventId | UUID | Foreign keys |
status | VARCHAR | confirmed / cancelled |
guestCount | INTEGER | Default: 1 |
tripBookings
| Column | Type | Notes |
userId, tripId | UUID | Foreign keys |
status | VARCHAR | confirmed / cancelled |
totalPaid | INTEGER | |
conciergeRequests
| Column | Type | Notes |
userId, agentId | UUID | Foreign keys |
serviceType, title, description | VARCHAR | |
priority | VARCHAR | normal / high / urgent |
status | VARCHAR | pending / assigned / in_progress / confirmed / completed / cancelled |
notifications
| Column | Type | Notes |
userId | UUID | Foreign key |
type, title, body | VARCHAR | |
data | JSONB | Extra metadata |
isRead | BOOLEAN | |
walletTransactions
| Column | Type | Notes |
userId | UUID | Foreign key |
type | VARCHAR | credit / debit / swap |
currency | VARCHAR | GBP / BTC / ETH / USDT |
amount | TEXT | String for precision |
status | VARCHAR | completed / pending / failed |
sessions
| Column | Type | Notes |
userId | UUID | Foreign key |
refreshToken | VARCHAR | |
userAgent, ipAddress | VARCHAR | |
expiresAt | TIMESTAMP | |
🔒 Authentication
JWT-based authentication with access and refresh tokens.
Flow
1. POST /auth/login { email, password }
→ { accessToken, refreshToken, user }
2. All requests: Authorization: Bearer {accessToken}
3. Token expired → POST /auth/refresh { refreshToken }
→ { accessToken, refreshToken }
4. POST /auth/logout { refreshToken }
Token Configuration
| Token | Expiration | Storage |
| Access Token | 15 minutes | Mobile: SecureStore, Admin: localStorage |
| Refresh Token | 7 days | Mobile: SecureStore, Admin: localStorage |
🔗 API Endpoints
Complete REST API reference. Base URL: /api/v1
Auth
| Method | Endpoint | Body | Auth |
| POST | /auth/login | {email, password} | No |
| POST | /auth/register | {email, password, firstName, lastName} | No |
| POST | /auth/refresh | {refreshToken} | No |
| POST | /auth/logout | {refreshToken} | Yes |
Users
| Method | Endpoint | Description | Auth |
| GET | /users | List all users | Yes |
| GET | /users/me | Get current user | Yes |
| GET | /users/stats | User statistics | Yes |
| GET | /users/:id | Get user by ID | Yes |
| PATCH | /users/me | Update own profile | Yes |
| PATCH | /users/:id | Update user (admin) | Yes |
Events
| Method | Endpoint | Description | Auth |
| GET | /events | List events (?limit=N) | Yes |
| GET | /events/stats | Event statistics | Yes |
| GET | /events/:id | Get event details | Yes |
| POST | /events | Create event | Yes |
| PATCH | /events/:id | Update event | Yes |
| DELETE | /events/:id | Delete event | Yes |
| POST | /events/:id/rsvp | RSVP to event | Yes |
Travel
| Method | Endpoint | Description | Auth |
| GET | /travel | List trips | Yes |
| GET | /travel/:id | Get trip details | Yes |
| POST | /travel | Create trip | Yes |
| PATCH | /travel/:id | Update trip | Yes |
| DELETE | /travel/:id | Delete trip | Yes |
| POST | /travel/:id/book | Book trip | Yes |
Concierge
| Method | Endpoint | Description | Auth |
| GET | /concierge | List all requests | Yes |
| GET | /concierge/my | My requests | Yes |
| GET | /concierge/stats | Statistics | Yes |
| POST | /concierge | Create request | Yes |
| PATCH | /concierge/:id/status | Update status | Yes |
Wallet
| Method | Endpoint | Description | Auth |
| GET | /wallet | Get transactions | Yes |
| GET | /wallet/balance | Get balance | Yes |
Notifications
| Method | Endpoint | Description | Auth |
| GET | /notifications | List notifications | Yes |
| POST | /notifications/read-all | Mark all read | Yes |
☁ Infrastructure
Server setup, Docker containers, and Cloudflare configuration.
Server: Hetzner CPX22
- Location: Helsinki, Finland
- Resources: 3 vCPU, 4 GB RAM, 80 GB NVMe
- OS: Ubuntu 24.04
Docker Containers (Production)
| Container | Image | Port | Role |
cl-traefik | traefik:v3.3 | 80, 443 | Reverse proxy + SSL |
cl-api | cl-api:latest | 3000 | NestJS backend |
cl-admin | cl-admin:latest | 80 | React admin (nginx) |
cl-postgres | postgres:16-alpine | 5432 | Database |
cl-redis | redis:7-alpine | 6379 | Cache + queues |
Cloudflare
- DNS Records: A records for @, api, admin, staging-api
- SSL: Full (strict)
- Storage: R2 bucket
cl-media
🚀 CI/CD Pipelines
GitHub Actions workflows for testing, building, and deploying.
GitHub Actions Workflows
1. CI (ci.yml)
- Trigger: PR to develop/main, push to develop
- Steps: Install → Lint → TypeCheck → Build → Test
2. Deploy Staging (deploy-staging.yml)
- Trigger: Push to
develop
- Steps: Build → Push images (
:staging) → SSH deploy → Health check
3. Deploy Production (deploy-production.yml)
- Trigger: Push to
main
- Steps: Build → Push images (
:latest + :sha) → SSH deploy → Health check
4. EAS Build (eas-build.yml)
- Trigger: Manual dispatch
- Inputs: platform (ios/android/all), profile (dev/preview/prod)
Git Branching
main ← production deploys
↑
develop ← staging deploys (auto)
↑
feature/* ← development (squash merge to develop)
hotfix/* ← from main (merge commit)
🔧 Deploy Scripts
Automation scripts in skills/clublondon-deploy/scripts/
| Script | Trigger | Description |
git-push.sh [msg] | push | Git add → commit → push → offer PR |
eas-build.sh [profile] [platform] | build | EAS build (dev/preview/prod) |
deploy.sh [staging|prod] | deploy | Docker deploy to Hetzner |
eas-submit.sh [platform] | submit | Submit to App Store / Play |
eas-update.sh [branch] [msg] | OTA | Over-the-air update |
release.sh [msg] | release | Full release pipeline |
status.sh | status | Health check all services |
cloudflare-dns.sh [ip] | DNS | Upsert DNS records |
hetzner-setup.sh | servers | Provision servers |
Manual Deploy
docker build --platform linux/amd64 -t cl-api:latest -f apps/api/Dockerfile .
docker build --platform linux/amd64 -t cl-admin:latest -f apps/admin/Dockerfile .
docker save cl-api:latest | gzip | ssh -i ~/.ssh/cl-deploy root@server "gunzip | docker load"
docker save cl-admin:latest | gzip | ssh -i ~/.ssh/cl-deploy root@server "gunzip | docker load"
ssh -i ~/.ssh/cl-deploy root@server "cd /opt/clublondon && docker compose up -d"
🔑 Environment Variables
Required and optional configuration variables.
Required
DATABASE_URL=postgresql://clublondon:password@localhost:5432/clublondon
REDIS_URL=redis://:password@localhost:6379
JWT_SECRET=your-secret-key
JWT_REFRESH_SECRET=your-refresh-secret
CLOUDFLARE_R2_ACCESS_KEY_ID=
CLOUDFLARE_R2_SECRET_ACCESS_KEY=
CLOUDFLARE_R2_BUCKET=cl-media
EXPO_PUBLIC_API_URL=https://api.clublondon.app
Optional
NODE_ENV=development
PORT=3000
APP_URL=https://clublondon.app
ADMIN_URL=https://admin.clublondon.app
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
HCLOUD_TOKEN=
CLOUDFLARE_API_TOKEN=
CLOUDFLARE_ZONE_ID=
CLOUDFLARE_ACCOUNT_ID=
EXPO_TOKEN=
🎨 Design System
Colors, typography, spacing, and component patterns.
Colors
| Token | Value | Swatch | Usage |
bg | #0D0D1A | | Primary background |
bg2 | #1A1A2E | | Cards, sidebar |
bg3 | #16213E | | Tertiary surfaces |
gold | #C4A35A | | Primary accent |
gold2 | #D4B96A | | Light gold variant |
white | #FFFFFF | | Primary text |
green | #4CAF50 | | Success |
red | #F44336 | | Error/danger |
Typography
| Font | Usage |
| Cormorant Garamond | Headings, display text |
| DM Sans | Body text, UI elements |
Spacing
| Token | Value |
xs | 4px |
sm | 8px |
md | 12px |
lg | 16px |
xl | 20px |
xxl | 24px |
xxxl | 32px |
Border Radius
| Token | Value |
sm | 8px |
md | 12px |
lg | 16px |
xl | 20px |
pill | 100px |
Component Patterns
- Cards:
rgba(255,255,255,0.04) bg + rgba(255,255,255,0.06) border + 16px radius
- Badges: Colored text + tinted background + pill radius
- Buttons Primary: Gold bg + dark text + 600 weight
- Buttons Secondary: Transparent bg + white border + white text
- Inputs:
rgba(255,255,255,0.04) bg + 8px radius + white text
- Tables: Collapsed borders + hover
rgba(255,255,255,0.02)
⌨ Key Commands Reference
Quick reference for all development, build, and deploy commands.
pnpm dev
pnpm dev:api
pnpm dev:mobile
pnpm dev:admin
pnpm db:generate
pnpm db:push
pnpm db:studio
pnpm build
pnpm build:api
pnpm build:admin
pnpm lint
pnpm typecheck
pnpm test
eas build --profile preview --platform all
eas submit --platform ios --latest
eas update --branch production --message "fix"
bash skills/clublondon-deploy/scripts/status.sh
bash skills/clublondon-deploy/scripts/deploy.sh prod