Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions .github/workflows/clubscan-backend-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: clubscan-backend-ci

on:
push:
branches: [main, "claude/**"]
paths:
- "clubscan/backend/**"
- ".github/workflows/clubscan-backend-ci.yml"
pull_request:
paths:
- "clubscan/backend/**"
- ".github/workflows/clubscan-backend-ci.yml"

defaults:
run:
working-directory: clubscan/backend

jobs:
build-test:
runs-on: ubuntu-latest
services:
postgres:
image: postgis/postgis:16-3.4
env:
POSTGRES_USER: clubscan
POSTGRES_PASSWORD: clubscan
POSTGRES_DB: clubscan_test
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U clubscan"
--health-interval 5s --health-timeout 5s --health-retries 10
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 5s --health-timeout 3s --health-retries 10
env:
DATABASE_URL: postgresql://clubscan:clubscan@localhost:5432/clubscan_test?schema=public
REDIS_URL: redis://localhost:6379
JWT_ACCESS_SECRET: ci-only-secret-please-change-me-32chars
NODE_ENV: test
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4
with:
version: 9

- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: clubscan/backend/pnpm-lock.yaml

- name: Install dependencies
run: pnpm install --no-frozen-lockfile

- name: Prisma generate
run: pnpm prisma:generate

- name: Lint
run: pnpm lint

- name: Typecheck
run: pnpm typecheck

- name: Migrate + PostGIS
run: |
pnpm prisma migrate deploy || pnpm prisma db push
psql "$DATABASE_URL" -f prisma/sql/postgis.sql || true

- name: Unit tests
run: pnpm test
67 changes: 67 additions & 0 deletions clubscan/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# ClubScan

> A safer, more transparent nightlife ecosystem — discover clubs, evaluate venues with
> structured trust signals, share experiences, and make informed decisions.

ClubScan is a production-grade mobile platform built to scale. This repository contains the
full architecture (Phases 1–7) and the production code foundation (Phase 8).

## Repository layout

```
clubscan/
├── docs/ Architecture (8 phases): product, DB, backend, frontend, infra, security, folders
├── backend/ NestJS modular monolith (Clean Architecture + DDD + CQRS where it pays off)
├── mobile/ Expo (React Native) app — design system, navigation, state, API client
├── docker-compose.yml
└── README.md
```

## Architecture docs

| Phase | Document |
|---|---|
| 1 | [Product Architecture](docs/01-product-architecture.md) |
| 2 | [Database Design](docs/02-database-design.md) |
| 3 | [Backend Architecture](docs/03-backend-architecture.md) |
| 4 | [Frontend Architecture](docs/04-frontend-architecture.md) |
| 5 | [Infrastructure Architecture](docs/05-infrastructure-architecture.md) |
| 6 | [Security Architecture](docs/06-security-architecture.md) |
| 7 | [Folder Structures](docs/07-folder-structures.md) |

## Quick start (local)

```bash
# 1) Bring up Postgres (PostGIS), Redis, MinIO, Mailhog
cd clubscan
docker compose up -d postgres redis minio createbuckets mailhog

# 2) Backend
cd backend
cp .env.example .env
pnpm install
pnpm prisma:generate
pnpm prisma migrate dev # baseline schema
psql "$DATABASE_URL" -f prisma/sql/postgis.sql # geospatial augmentation
pnpm prisma:seed
pnpm start:dev # http://localhost:3000/api/v1 (docs at /api/v1/docs)
```

## The ClubScan Score

Venues are rated across nine structured categories (Security, Staff behavior, Fair pricing,
Crowd quality, Music quality, Sound system, Cleanliness, **Safety for women**, Atmosphere).
The composite 0–100 score is a weighted, recency-decayed, reputation-weighted, Bayesian-shrunk
aggregate that prioritizes safety and resists manipulation. See
[Backend §11](docs/03-backend-architecture.md) and `backend/src/modules/scoring`.

## Tech stack

**Mobile:** Expo · TypeScript · Expo Router · TanStack Query · Zustand · RHF + Zod · NativeWind
**Backend:** NestJS · TypeScript · Prisma · PostgreSQL · Redis
**Auth:** JWT (access) + rotating refresh tokens + Google/Apple OAuth
**Infra:** Docker · GitHub Actions · S3-compatible storage · FCM · Sentry · OpenTelemetry

## License

UNLICENSED — proprietary.
35 changes: 35 additions & 0 deletions clubscan/backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# ---- Runtime ----
NODE_ENV=development
PORT=3000
API_PREFIX=api/v1
CORS_ORIGINS=*

# ---- Data ----
DATABASE_URL=postgresql://clubscan:clubscan@localhost:5432/clubscan?schema=public
REDIS_URL=redis://localhost:6379

# ---- Auth ----
# Generate strong secrets: `openssl rand -base64 48`
JWT_ACCESS_SECRET=replace-with-a-strong-32+char-secret-value
JWT_ACCESS_TTL=15m
JWT_REFRESH_TTL_DAYS=30

# ---- OAuth ----
GOOGLE_OAUTH_CLIENT_ID=
APPLE_OAUTH_CLIENT_ID=

# ---- Storage (S3-compatible; MinIO in local dev) ----
S3_ENDPOINT=http://localhost:9000
S3_REGION=us-east-1
S3_BUCKET=clubscan-media
S3_ACCESS_KEY=clubscan
S3_SECRET_KEY=clubscan-secret

# ---- Push / Observability ----
FCM_PROJECT_ID=
SENTRY_DSN=
OTEL_EXPORTER_OTLP_ENDPOINT=

# ---- Seed (dev) ----
SEED_ADMIN_EMAIL=admin@clubscan.app
SEED_ADMIN_PASSWORD=ChangeMe123!
17 changes: 17 additions & 0 deletions clubscan/backend/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: { project: 'tsconfig.json', sourceType: 'module' },
plugins: ['@typescript-eslint'],
extends: [
'plugin:@typescript-eslint/recommended',
'prettier',
],
root: true,
env: { node: true },
ignorePatterns: ['dist', 'node_modules', '.eslintrc.cjs'],
rules: {
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
},
};
6 changes: 6 additions & 0 deletions clubscan/backend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules/
dist/
coverage/
.env
*.log
.DS_Store
32 changes: 32 additions & 0 deletions clubscan/backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# ClubScan API — multi-stage build (Phase 5 §2)
# 1) deps 2) build (tsc + prisma generate) 3) slim non-root runtime

FROM node:22-bookworm-slim AS deps
WORKDIR /app
RUN corepack enable
COPY package.json pnpm-lock.yaml* ./
RUN npm install -g pnpm@9 && pnpm install --frozen-lockfile || pnpm install

FROM node:22-bookworm-slim AS build
WORKDIR /app
RUN corepack enable && npm install -g pnpm@9
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN pnpm prisma generate && pnpm build

FROM node:22-bookworm-slim AS runtime
ENV NODE_ENV=production
WORKDIR /app
# Non-root user
RUN groupadd -r app && useradd -r -g app app
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=build /app/prisma ./prisma
COPY --from=build /app/package.json ./
COPY docker-entrypoint.sh ./
RUN chmod +x docker-entrypoint.sh && chown -R app:app /app
USER app
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s \
CMD node -e "fetch('http://localhost:3000/api/v1/health/ready').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
ENTRYPOINT ["./docker-entrypoint.sh"]
10 changes: 10 additions & 0 deletions clubscan/backend/docker-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/bin/sh
set -e

# Apply migrations forward-only (advisory-locked by Prisma to avoid races across
# replicas — Phase 5 §4). Then start the API.
echo "Running database migrations..."
npx prisma migrate deploy

echo "Starting ClubScan API..."
exec node dist/main.js
8 changes: 8 additions & 0 deletions clubscan/backend/nest-cli.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}
65 changes: 65 additions & 0 deletions clubscan/backend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
{
"name": "@clubscan/backend",
"version": "0.1.0",
"description": "ClubScan API — NestJS modular monolith",
"license": "UNLICENSED",
"private": true,
"scripts": {
"build": "nest build",
"start": "node dist/main.js",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"lint": "eslint \"src/**/*.ts\" --max-warnings 0",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest",
"test:e2e": "vitest run --config vitest.e2e.config.ts",
"prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev",
"prisma:deploy": "prisma migrate deploy",
"prisma:seed": "tsx prisma/seed.ts"
},
"prisma": {
"seed": "tsx prisma/seed.ts"
},
"dependencies": {
"@nestjs/common": "^10.4.4",
"@nestjs/config": "^3.2.3",
"@nestjs/core": "^10.4.4",
"@nestjs/cqrs": "^10.2.7",
"@nestjs/event-emitter": "^2.1.1",
"@nestjs/jwt": "^10.2.0",
"@nestjs/platform-express": "^10.4.4",
"@nestjs/swagger": "^7.4.2",
"@nestjs/throttler": "^6.2.1",
"@prisma/client": "^5.20.0",
"argon2": "^0.41.1",
"axios": "^1.7.7",
"google-auth-library": "^9.14.1",
"helmet": "^8.0.0",
"ioredis": "^5.4.1",
"nestjs-i18n": "^10.4.9",
"nestjs-zod": "^3.0.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"uuid": "^10.0.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@nestjs/cli": "^10.4.5",
"@nestjs/testing": "^10.4.4",
"@types/express": "^4.17.21",
"@types/node": "^22.7.4",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^8.8.0",
"@typescript-eslint/parser": "^8.8.0",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0",
"prettier": "^3.3.3",
"prisma": "^5.20.0",
"supertest": "^7.0.0",
"tsx": "^4.19.1",
"typescript": "^5.6.2",
"vitest": "^2.1.1"
}
}
Loading
Loading