feat: add initial project configuration and smoke tests

- Created pnpm workspace configuration to manage packages.
- Added a placeholder .gitkeep file in the scripts directory.
- Implemented a smoke test script to validate core API and web endpoints.
- Established TypeScript base configuration for consistent compilation settings.
- Introduced Turbo configuration for task management and build processes.
This commit is contained in:
KinSun
2026-03-13 10:30:16 +08:00
commit 9d5616fdc6
68 changed files with 9851 additions and 0 deletions

8
.dockerignore Normal file
View File

@@ -0,0 +1,8 @@
node_modules
.git
dist
.next
.env
coverage
.turbo
*.md

15
.env.example Normal file
View File

@@ -0,0 +1,15 @@
# Database
DATABASE_URL=postgresql://davinci:davinci@localhost:55434/davinci_platform_dev
# Server
PORT=3001
NODE_ENV=development
# Mode: "mothership" (default) or "community"
DAVINCI_MODE=mothership
# Auth (placeholder — configured in auth feature plan)
JWT_SECRET=change-me-in-production
# Frontend
NEXT_PUBLIC_API_URL=http://localhost:3001/api

39
.gitignore vendored Normal file
View File

@@ -0,0 +1,39 @@
# Dependencies
node_modules/
# Build output
dist/
.next/
# Turborepo cache
.turbo/
# TypeScript incremental
*.tsbuildinfo
# Environment
.env
# Prisma
prisma/*.db
generated/
# Test coverage
coverage/
# IDE
.idea/
.vscode/
.claude/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

1
.husky/pre-commit Normal file
View File

@@ -0,0 +1 @@
npx lint-staged

2
.npmrc Normal file
View File

@@ -0,0 +1,2 @@
shamefully-hoist=false
strict-peer-dependencies=true

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
22

6
.prettierignore Normal file
View File

@@ -0,0 +1,6 @@
dist
.next
node_modules
coverage
pnpm-lock.yaml
.turbo

7
.prettierrc Normal file
View File

@@ -0,0 +1,7 @@
{
"singleQuote": true,
"trailingComma": "all",
"semi": true,
"printWidth": 100,
"tabWidth": 2
}

28
CHANGELOG.md Normal file
View File

@@ -0,0 +1,28 @@
# Changelog
All notable changes to this project will be documented in this file.
## [0.1.0-alpha.0] — 2026-03-12
### Added
- **Monorepo scaffold** — pnpm workspaces with `packages/api`, `packages/web`, `packages/shared`
- **NestJS 11 API shell** — Fastify 5 adapter, health endpoint (`GET /api/health`), ConfigModule with env validation
- **Next.js 15.5.x frontend shell** — App Router, next-intl i18n, Tailwind CSS v4, shadcn/ui base config
- **Prisma 7 schema shell** — PostgreSQL datasource, placeholder model for client generation
- **Shared types package** — `@davinci/shared` with barrel exports, build via TypeScript
- **Turborepo** — build caching and task orchestration across packages
- **Vitest** — test runner for all packages (replaces Jest)
- **ESLint flat config** — TypeScript + Prettier integration, single root config
- **Prettier** — consistent formatting with pre-commit hooks (Husky + lint-staged)
- **Docker** — multi-stage Dockerfile (nginx + NestJS + Next.js), docker-compose for dev
- **nginx** — internal routing `/api/*` → :3001, `/*` → :3000
- **Makefile** — developer command interface matching architecture spec
- **README** — quickstart, project structure, environment variables
- **smoke-test.sh SKIP_WEB** — `SKIP_WEB=1 make smoke` skips web container for API-only workflows
### Fixed
- **Docker healthcheck IPv6** — changed `localhost` to `127.0.0.1` in docker-compose healthcheck URLs (Alpine wget resolves localhost → ::1 IPv6, Fastify listens on IPv4 only)
- **Scalar ESM import (TS1479)** — converted static import of `@scalar/fastify-api-reference` to dynamic `import()` since the package is ESM-only
- **Test import** — added missing `afterEach` to vitest import in `api-docs.setup.spec.ts`

83
Dockerfile Normal file
View File

@@ -0,0 +1,83 @@
# --- Stage 1: Dependencies ---
FROM node:22-alpine AS deps
RUN apk add --no-cache openssl python3 make g++
RUN corepack enable && corepack prepare pnpm@10.32.0 --activate
WORKDIR /app
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
COPY packages/api/package.json packages/api/
COPY packages/shared/package.json packages/shared/
COPY packages/web/package.json packages/web/
RUN pnpm install --frozen-lockfile
# --- Stage 2: Build API ---
FROM deps AS api-builder
WORKDIR /app
COPY tsconfig.base.json ./
COPY packages/shared/ packages/shared/
RUN pnpm --filter @davinci/shared build
COPY packages/api/tsconfig.json packages/api/tsconfig.build.json packages/api/nest-cli.json packages/api/prisma.config.ts packages/api/
COPY packages/api/prisma/ packages/api/prisma/
COPY packages/api/src/ packages/api/src/
RUN pnpm --filter @davinci/api prisma:generate
RUN pnpm --filter @davinci/api build
# --- Stage 3: Build Web ---
FROM deps AS web-builder
WORKDIR /app
COPY tsconfig.base.json ./
COPY packages/shared/ packages/shared/
RUN pnpm --filter @davinci/shared build
COPY packages/web/ packages/web/
RUN pnpm --filter @davinci/web build
# --- Stage 4: Runner ---
FROM node:22-alpine AS runner
RUN apk add --no-cache openssl nginx
WORKDIR /app
ENV NODE_ENV=production
# Non-root user
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 appuser
# Copy nginx config
COPY nginx.conf /etc/nginx/http.d/default.conf
# Copy API build
COPY --from=api-builder /app/packages/api/dist packages/api/dist
COPY --from=api-builder /app/packages/api/prisma packages/api/prisma
COPY --from=api-builder /app/packages/api/node_modules packages/api/node_modules
COPY --from=api-builder /app/packages/api/package.json packages/api/
COPY --from=api-builder /app/node_modules ./node_modules
COPY --from=api-builder /app/package.json ./
# Copy Web standalone build
COPY --from=web-builder /app/packages/web/.next/standalone packages/web/standalone
COPY --from=web-builder /app/packages/web/.next/static packages/web/standalone/packages/web/.next/static
COPY --from=web-builder /app/packages/web/public packages/web/standalone/packages/web/public
# Copy entrypoint
COPY entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
# nginx needs write access to certain dirs
RUN mkdir -p /run/nginx && chown -R appuser:nodejs /run/nginx
RUN chown -R appuser:nodejs /var/log/nginx /var/lib/nginx
EXPOSE 80
USER appuser
ENTRYPOINT ["/app/entrypoint.sh"]

138
Makefile Normal file
View File

@@ -0,0 +1,138 @@
# Davinci Platform — Developer Commands
#
# Unified monorepo: packages/api (NestJS), packages/web (Next.js), packages/shared.
#
# Dev stack: persistent data, ports 4300 (api) / 4000 (web) / 55434 (pg)
# Test stack: ephemeral (always fresh DB), ports 4301 (api) / 4001 (web) / 45434 (pg)
#
# Usage:
# make help Show all commands
# make dev Start full dev stack
# make test Full isolated smoke test
.PHONY: dev dev-build dev-logs dev-stop dev-reset dev-seed build test test-up test-down test\:unit test\:e2e test\:cov lint format format\:check db\:migrate db\:studio db\:push docker\:build docker\:up smoke down help
TEST = docker compose -p davinci-platform-test -f docker-compose.test.yml
# ─── Dev Stack ────────────────────────────────────────────────────
dev: ## Start docker-compose postgres + API + web in watch mode
docker compose up db -d --wait
pnpm dev
@echo ""
@echo " API: http://localhost:4300"
@echo " Web: http://localhost:4000"
@echo " Postgres: localhost:55434"
@echo ""
dev-build: ## Rebuild and start dev stack (docker)
docker compose up -d --build
dev-logs: ## Tail API container logs
docker compose logs -f api
dev-stop: ## Stop dev containers (data preserved)
docker compose down
dev-reset: ## Stop, destroy data, restart fresh
docker compose down -v
docker compose up -d --build
@echo ""
@echo " Dev stack reset with fresh database"
@echo ""
dev-seed: ## Seed dev database with test data
@echo "Seeding dev database..."
DATABASE_URL=postgresql://davinci:davinci@localhost:55434/davinci_platform_dev \
pnpm --filter @davinci/api exec prisma db seed
@echo "Done."
# ─── Build ────────────────────────────────────────────────────────
build: ## Build all packages
pnpm build
# ─── Testing ──────────────────────────────────────────────────────
test\:unit: ## Run unit tests only
pnpm test:unit
test\:e2e: ## Run e2e tests only
pnpm test:e2e
test\:cov: ## Run tests with coverage report
pnpm test:cov
# ─── Test Stack (Isolated) ────────────────────────────────────────
test-up: ## Start isolated test environment (fresh DB, ephemeral)
$(TEST) down -v 2>/dev/null || true
$(TEST) up -d --build
@echo ""
@echo " Test API: http://localhost:4301"
@echo " Test Web: http://localhost:4001"
@echo " Test DB: localhost:45434"
@echo ""
test-down: ## Tear down test environment (destroy data)
$(TEST) down -v 2>/dev/null || true
test: test-up ## Run smoke tests in isolated test environment
@echo "Waiting for services to be ready..."
@sleep 10
@API_PORT=4301 WEB_PORT=4001 bash smoke-test.sh; \
EXIT_CODE=$$?; \
$(MAKE) test-down; \
exit $$EXIT_CODE
# ─── Smoke (against running dev stack) ────────────────────────────
smoke: ## Run smoke tests against dev stack (must be running)
bash smoke-test.sh
# ─── Code Quality ─────────────────────────────────────────────────
lint: ## Lint all packages
pnpm lint
format: ## Format all files with Prettier
pnpm format
format\:check: ## Check formatting without writing
pnpm format:check
# ─── Database ─────────────────────────────────────────────────────
db\:migrate: ## Create a new Prisma migration after schema changes
@read -p "Migration name: " name; \
DATABASE_URL=postgresql://davinci:davinci@localhost:55434/davinci_platform_dev \
pnpm --filter @davinci/api exec prisma migrate dev --name $$name
db\:studio: ## Open Prisma Studio
DATABASE_URL=postgresql://davinci:davinci@localhost:55434/davinci_platform_dev \
pnpm --filter @davinci/api exec prisma studio
db\:push: ## Push schema changes (no migration — quick prototyping only)
DATABASE_URL=postgresql://davinci:davinci@localhost:55434/davinci_platform_dev \
pnpm --filter @davinci/api exec prisma db push
# ─── Docker ───────────────────────────────────────────────────────
docker\:build: ## Build production Docker image
docker build -t davinci-platform .
docker\:up: ## Run full stack via docker-compose
docker compose up -d
# ─── Cleanup ──────────────────────────────────────────────────────
down: ## Stop everything (dev + test)
docker compose down 2>/dev/null || true
$(TEST) down -v 2>/dev/null || true
# ─── Help ─────────────────────────────────────────────────────────
help: ## Show this help
@grep -E '^[a-zA-Z_\\:-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}'
.DEFAULT_GOAL := help

338
README.md Normal file
View File

@@ -0,0 +1,338 @@
# Davinci Platform
Unified monorepo for the Davinci platform — replaces `davinci-mothership` and `davinci-community-server` as a single codebase with a `DAVINCI_MODE` flag.
## Prerequisites
- **Docker Desktop** (includes Docker Compose v2)
- **Node.js** 22+ (see `.nvmrc`)
- **pnpm** 10+ (`corepack enable && corepack prepare pnpm@10.32.0 --activate`)
- **make** (preinstalled on macOS/Linux)
> Two development modes are supported:
> 1. **Docker (recommended)** — everything runs in containers. Zero local setup beyond Docker + Node.js.
> 2. **Local + Docker infra** — run the NestJS API and Next.js frontend natively for faster iteration, Docker for Postgres only. See [Local Development](#local-development-without-docker-for-the-app).
## Quick Start (Docker — Recommended)
```bash
# 1. Install local deps (for Prisma CLI, tests, IDE support)
pnpm install
# 2. Copy environment config
cp .env.example .env
# 3. Start the full dev environment (Postgres + API + Web)
make dev-build
# 4. Run migrations
make db:push
# 5. Seed the dev database with test data
make dev-seed
# 6. Verify everything works
make smoke
```
**That's it.** You now have:
| Service | URL |
|---------|-----|
| API | http://localhost:4300/health |
| API Docs (Scalar) | http://localhost:4300/api-reference |
| Web | http://localhost:4000 |
| Postgres | localhost:55434 |
### Useful Commands
```bash
make help # Show all available commands
make dev-logs # Tail API container logs
make dev-stop # Stop containers (data preserved)
make dev-reset # Destroy + rebuild from scratch
make dev-seed # Re-seed database
```
## Local Development (Without Docker for the App)
If you prefer running the NestJS API and Next.js frontend natively (for faster restarts, debugger attach, HMR, etc.) while still using Docker for infrastructure:
### 1. Start Infrastructure Only
```bash
# Start only Postgres (no app containers)
docker compose up db -d --wait
```
### 2. Set Up Environment
Create a `.env` file (git-ignored) in the repo root:
```bash
# .env
DATABASE_URL="postgresql://davinci:davinci@localhost:55434/davinci_platform_dev"
PORT=3001
NODE_ENV=development
DAVINCI_MODE=mothership
ENABLE_API_DOCS=true
```
### 3. Generate Prisma Client & Push Schema
```bash
DATABASE_URL=postgresql://davinci:davinci@localhost:55434/davinci_platform_dev \
pnpm --filter @davinci/api prisma:generate
DATABASE_URL=postgresql://davinci:davinci@localhost:55434/davinci_platform_dev \
pnpm --filter @davinci/api exec prisma db push
```
### 4. Start the Apps
```bash
# Terminal 1: Start API (watch mode — auto-restarts on file changes)
pnpm --filter @davinci/api start:dev
# Terminal 2: Start Web (Next.js dev server with HMR)
pnpm --filter @davinci/web dev
```
The API listens on `http://localhost:3001`, Web on `http://localhost:3000`.
### 5. Seed Data (Optional)
```bash
DATABASE_URL=postgresql://davinci:davinci@localhost:55434/davinci_platform_dev \
pnpm --filter @davinci/api exec prisma db seed
```
### VS Code Debugger
With `start:dev --debug`, attach VS Code's debugger:
```jsonc
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Attach to Davinci API",
"type": "node",
"request": "attach",
"port": 9229,
"restart": true,
"skipFiles": ["<node_internals>/**"]
}
]
}
```
## Architecture Overview
```
┌──────────────────────────────────────────────────┐
│ Davinci Platform Monorepo │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌────────┐ │
│ │ packages/api │ │ packages/web │ │ shared │ │
│ │ NestJS 11 │ │ Next.js 15.5 │ │ types │ │
│ │ Fastify 5 │ │ App Router │ │ consts │ │
│ │ Prisma 7 │ │ Tailwind v4 │ │ │ │
│ └──────┬───────┘ └──────┬───────┘ └────────┘ │
│ │ │ │
│ /api/v1/* /* (frontend) │
│ │ │ │
│ ┌──────┴──────────────────┴──────┐ │
│ │ PostgreSQL │ │
│ └────────────────────────────────┘ │
└──────────────────────────────────────────────────┘
```
**Key design decisions:**
- **DAVINCI_MODE** — single codebase operates as either `mothership` (control plane) or `community` (per-tenant) via env flag
- **NestJS + Fastify** — high-throughput backend with URI-based API versioning (`/api/v1/...`)
- **Prisma 7** — type-safe ORM with `@prisma/adapter-pg` driver
- **Next.js 15.5** — App Router with `next-intl` for i18n, Tailwind CSS v4, `shadcn/ui`
- **Turborepo** — monorepo build orchestration with caching
- **Docker multi-stage** — production image includes nginx reverse proxy on port 80
### API Versioning
All business endpoints are served under `/api/v1/...`. Operational endpoints are unversioned:
| Endpoint | Versioned | Description |
|----------|-----------|-------------|
| `/health` | No | Liveness probe |
| `/api-reference` | No | Scalar API documentation |
| `/api/v1/*` | Yes | Business endpoints |
### API Documentation
Interactive API documentation powered by [Scalar](https://scalar.com) is available at `/api-reference` in development and test environments. Controlled via `ENABLE_API_DOCS` env var (defaults to enabled in dev/test, disabled in production).
### Dependency Injection
The API uses NestJS's built-in DI container. Key global providers:
| Module | Provider | Description |
|--------|----------|-------------|
| `ConfigModule` | `ConfigService` | Environment variable access (global) |
| `PrismaModule` | `PrismaService` | Database access via `prisma.client` (global) |
Usage in feature modules:
```typescript
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service.js';
@Injectable()
export class MyFeatureService {
constructor(private readonly prisma: PrismaService) {}
async findAll() {
return this.prisma.client.myModel.findMany();
}
}
```
## Project Structure
```
davinci-platform/
├── packages/
│ ├── api/ # NestJS 11 backend (Fastify 5 adapter)
│ │ ├── src/
│ │ │ ├── main.ts # Bootstrap: NestJS + Fastify + versioning + Scalar
│ │ │ ├── app.module.ts # Root module (Config, Prisma, Health)
│ │ │ ├── docs/ # API documentation (Scalar + OpenAPI)
│ │ │ │ └── api-docs.setup.ts # Multi-version Scalar setup
│ │ │ ├── health/ # Health module (liveness probe)
│ │ │ └── prisma/ # Database module (global PrismaService)
│ │ ├── prisma/
│ │ │ ├── schema.prisma # Prisma schema
│ │ │ └── seed.ts # Idempotent dev seed script
│ │ ├── prisma.config.ts # Prisma 7 config (datasource URL, seed)
│ │ └── generated/prisma/ # Generated Prisma client (git-ignored)
│ ├── web/ # Next.js 15.5 frontend (App Router)
│ │ ├── src/
│ │ │ ├── app/ # App Router layout + pages
│ │ │ ├── i18n/ # next-intl routing + request config
│ │ │ ├── middleware.ts # Locale detection middleware
│ │ │ └── lib/utils.ts # cn() utility (clsx + tailwind-merge)
│ │ ├── messages/en.json # i18n messages
│ │ └── components.json # shadcn/ui config
│ └── shared/ # Shared types + constants
│ └── src/
│ ├── index.ts # Barrel export
│ └── constants/index.ts # APP_NAME, etc.
├── Dockerfile # Multi-stage production build
├── nginx.conf # Reverse proxy: /api/* → :3001, /* → :3000
├── entrypoint.sh # Production entrypoint
├── docker-compose.yml # Dev environment
├── docker-compose.test.yml # Isolated test environment
├── Makefile # Developer command interface
├── turbo.json # Turborepo task config
├── smoke-test.sh # 6-endpoint smoke test
└── .env.example # Environment template
```
## Makefile Commands
Run `make help` for the full list. Key commands:
### Development
| Command | Description |
|---------|-------------|
| `make dev` | Start Postgres + API + Web in watch mode (local apps) |
| `make dev-build` | Build and start full dev stack (Docker) |
| `make dev-logs` | Tail API container logs |
| `make dev-stop` | Stop containers (data preserved) |
| `make dev-reset` | Destroy data + rebuild from scratch |
| `make dev-seed` | Seed dev database with test data |
### Testing
| Command | Description |
|---------|-------------|
| `make test` | Full isolated smoke test (build → run → test → teardown) |
| `make test:unit` | Unit tests only |
| `make test:e2e` | E2E tests only |
| `make test:cov` | Tests with coverage report |
| `make smoke` | Smoke tests against running dev stack |
### Code Quality
| Command | Description |
|---------|-------------|
| `make lint` | Lint all packages |
| `make format` | Format all files with Prettier |
| `make format:check` | Check formatting without writing |
### Database
| Command | Description |
|---------|-------------|
| `make db:migrate` | Create a new Prisma migration |
| `make db:push` | Push schema changes (quick prototyping) |
| `make db:studio` | Open Prisma Studio |
### Docker
| Command | Description |
|---------|-------------|
| `make docker:build` | Build production Docker image |
| `make docker:up` | Run full stack via docker-compose |
| `make down` | Stop everything (dev + test) |
## Port Architecture
Standard ports are used inside containers (Dockerfile, entrypoint, nginx). Docker Compose maps to non-common host ports to avoid conflicts:
| Stack | API (host → container) | Web (host → container) | Postgres (host → container) |
|-------|----------------------|----------------------|---------------------------|
| Dev | 4300 → 3001 | 4000 → 3000 | 55434 → 5432 |
| Test | 4301 → 3001 | 4001 → 3000 | 45434 → 5432 |
| Local (no Docker) | 3001 (direct) | 3000 (direct) | 55434 → 5432 |
| Production (nginx) | :80 (reverse proxy) | :80 (reverse proxy) | — |
> **Ports are chosen to avoid conflicts:** Common tools (React, Vite, Next.js) use 3000/3001/5173. The Docker dev stack uses 4300/4000/55434.
## Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `DATABASE_URL` | *(required)* | PostgreSQL connection string |
| `PORT` | `3001` | API server port |
| `NODE_ENV` | `development` | `development`, `production`, or `test` |
| `DAVINCI_MODE` | `mothership` | Operating mode (`mothership` or `community`) |
| `ENABLE_API_DOCS` | auto | Scalar API docs (`true`/`false`; defaults enabled in dev/test) |
| `CORS_ORIGIN` | `*` (dev) | Comma-separated allowed origins |
| `LOG_LEVEL` | auto | Pino log level (`debug` in dev, `info` in prod) |
| `NEXT_PUBLIC_API_URL` | — | Frontend API URL (set in docker-compose) |
See [`.env.example`](.env.example) for a starter template.
## Tech Stack
| Layer | Technology | Version |
|-------|------------|---------|
| Runtime | Node.js | 22 LTS |
| Package Manager | pnpm | 10.32.0 |
| Build Orchestration | Turborepo | 2.x |
| Backend Framework | NestJS | 11.x |
| HTTP Server | Fastify | 5.x |
| ORM | Prisma | 7.x |
| Frontend Framework | Next.js | 15.5.x |
| CSS | Tailwind CSS | v4 |
| UI Components | shadcn/ui | — |
| i18n | next-intl | 4.x |
| Testing | Vitest | 3.x |
| Linting | ESLint (flat config) | 9.x |
| Formatting | Prettier | 3.4.x |
| API Docs | Scalar + @nestjs/swagger | — |
| Database | PostgreSQL | 16 |
| Container | Docker (multi-stage) | — |
| Reverse Proxy | nginx | — |

82
docker-compose.test.yml Normal file
View File

@@ -0,0 +1,82 @@
# Standalone test compose for smoke tests.
# Fully self-contained — not an overlay of docker-compose.yml.
# Uses a separate project name (-p davinci-platform-test) so test volumes,
# containers, and networks are fully isolated from the persistent dev stack.
#
# Usage (via Makefile):
# make test # Builds, runs smoke, tears down
# make test-up # Start test stack only
# make test-down # Tear down test stack
#
# Port mapping: test → 4301/4001/45434, dev → 4300/4000/55434
services:
db:
image: postgres:16-alpine
container_name: davinci-platform-test-db
ports:
- '45434:5432'
environment:
POSTGRES_USER: davinci
POSTGRES_PASSWORD: davinci
POSTGRES_DB: davinci_platform_test
volumes:
- platform-test-pgdata:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U davinci -d davinci_platform_test']
interval: 5s
timeout: 5s
retries: 5
networks:
- davinci-platform-test
api:
build:
context: .
dockerfile: Dockerfile
target: api-builder
container_name: davinci-platform-test-api
command: ['node', 'packages/api/dist/main.js']
ports:
- '4301:3001'
depends_on:
db:
condition: service_healthy
environment:
DATABASE_URL: postgresql://davinci:davinci@db:5432/davinci_platform_test?schema=public
PORT: 3001
NODE_ENV: test
DAVINCI_MODE: mothership
healthcheck:
test: ['CMD-SHELL', 'wget -qO- http://127.0.0.1:3001/health || exit 1']
interval: 10s
timeout: 5s
retries: 3
networks:
- davinci-platform-test
web:
build:
context: .
dockerfile: Dockerfile
target: web-builder
container_name: davinci-platform-test-web
command: ['node', 'packages/web/.next/standalone/packages/web/server.js']
ports:
- '4001:3000'
depends_on:
api:
condition: service_healthy
environment:
NEXT_PUBLIC_API_URL: http://localhost:4301/api
HOSTNAME: '0.0.0.0'
PORT: '3000'
networks:
- davinci-platform-test
volumes:
platform-test-pgdata:
networks:
davinci-platform-test:
name: davinci-platform-test

71
docker-compose.yml Normal file
View File

@@ -0,0 +1,71 @@
services:
db:
image: postgres:16-alpine
container_name: davinci-platform-db
ports:
- '55434:5432'
environment:
POSTGRES_USER: davinci
POSTGRES_PASSWORD: davinci
POSTGRES_DB: davinci_platform_dev
volumes:
- platform-pgdata:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U davinci -d davinci_platform_dev']
interval: 5s
timeout: 5s
retries: 5
networks:
- davinci-platform
api:
build:
context: .
dockerfile: Dockerfile
target: api-builder
container_name: davinci-platform-api
command: ['node', 'packages/api/dist/main.js']
ports:
- '4300:3001'
depends_on:
db:
condition: service_healthy
environment:
DATABASE_URL: postgresql://davinci:davinci@db:5432/davinci_platform_dev?schema=public
PORT: 3001
NODE_ENV: development
DAVINCI_MODE: mothership
healthcheck:
test: ['CMD-SHELL', 'wget -qO- http://127.0.0.1:3001/health || exit 1']
interval: 10s
timeout: 5s
retries: 3
volumes:
- ./packages/api/src:/app/packages/api/src
networks:
- davinci-platform
web:
build:
context: .
dockerfile: Dockerfile
target: web-builder
container_name: davinci-platform-web
command: ['node', 'packages/web/.next/standalone/packages/web/server.js']
ports:
- '4000:3000'
depends_on:
- api
environment:
NEXT_PUBLIC_API_URL: http://localhost:4300/api
HOSTNAME: '0.0.0.0'
PORT: '3000'
networks:
- davinci-platform
volumes:
platform-pgdata:
networks:
davinci-platform:
name: davinci-platform

0
docs/.gitkeep Normal file
View File

35
entrypoint.sh Normal file
View File

@@ -0,0 +1,35 @@
#!/bin/sh
set -e
# Known limitation: Multi-process under a single shell entrypoint means if one
# process crashes, the container may remain partially alive. Production-grade
# process supervision is a future concern, not a scaffold blocker.
cleanup() {
echo "Shutting down..."
kill "$NGINX_PID" "$API_PID" "$WEB_PID" 2>/dev/null || true
wait "$NGINX_PID" "$API_PID" "$WEB_PID" 2>/dev/null || true
exit 0
}
trap cleanup SIGTERM SIGINT
# Start nginx
nginx -g 'daemon off;' &
NGINX_PID=$!
# Run Prisma migrations
cd /app/packages/api
npx prisma migrate deploy
# Start NestJS API
PORT=3001 node dist/main.js &
API_PID=$!
# Start Next.js standalone
cd /app/packages/web/standalone
PORT=3000 HOSTNAME=0.0.0.0 node packages/web/server.js &
WEB_PID=$!
# Wait for any process to exit
wait -n "$NGINX_PID" "$API_PID" "$WEB_PID"

22
eslint.config.mjs Normal file
View File

@@ -0,0 +1,22 @@
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import eslintConfigPrettier from 'eslint-config-prettier';
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
eslintConfigPrettier,
{
ignores: ['dist/', '.next/', 'node_modules/', 'coverage/', 'prisma/generated/', '.turbo/', '**/next-env.d.ts'],
},
{
files: ['packages/*/src/**/*.{ts,tsx}'],
rules: {
'@typescript-eslint/no-unused-vars': [
'warn',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
'@typescript-eslint/no-explicit-any': 'warn',
},
},
);

30
nginx.conf Normal file
View File

@@ -0,0 +1,30 @@
server {
listen 80;
server_name _;
# API routes
location /api/ {
proxy_pass http://127.0.0.1:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
# Frontend routes
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}

51
package.json Normal file
View File

@@ -0,0 +1,51 @@
{
"name": "davinci-platform",
"version": "0.1.0-alpha.0",
"private": true,
"description": "Davinci Platform — unified monorepo (mothership + community server)",
"author": "Galaxis",
"license": "UNLICENSED",
"packageManager": "pnpm@10.32.0",
"scripts": {
"dev": "turbo run dev",
"build": "turbo run build",
"test": "turbo run test",
"test:unit": "turbo run test:unit",
"test:e2e": "turbo run test:e2e",
"test:cov": "turbo run test:cov",
"lint": "turbo run lint",
"format": "prettier --write \"packages/*/src/**/*.{ts,tsx}\"",
"format:check": "prettier --check \"packages/*/src/**/*.{ts,tsx}\"",
"prisma:generate": "pnpm --filter @davinci/api prisma:generate",
"prisma:migrate": "pnpm --filter @davinci/api prisma:migrate",
"prepare": "husky"
},
"devDependencies": {
"@eslint/js": "^9.15.0",
"eslint": "^9.15.0",
"eslint-config-prettier": "^10.0.0",
"typescript-eslint": "^8.15.0",
"prettier": "^3.4.0",
"turbo": "^2.4.0",
"husky": "^9.1.0",
"lint-staged": "^15.4.0"
},
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
]
},
"pnpm": {
"onlyBuiltDependencies": [
"@nestjs/core",
"@parcel/watcher",
"@prisma/client",
"@prisma/engines",
"@swc/core",
"esbuild",
"prisma",
"sharp"
]
}
}

View File

@@ -0,0 +1,10 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true,
"webpack": false,
"tsConfigPath": "tsconfig.build.json"
}
}

51
packages/api/package.json Normal file
View File

@@ -0,0 +1,51 @@
{
"name": "@davinci/api",
"version": "0.1.0-alpha.0",
"description": "Davinci Platform — NestJS API backend (Fastify adapter)",
"author": "Galaxis",
"private": true,
"license": "UNLICENSED",
"main": "dist/main.js",
"scripts": {
"build": "nest build",
"start": "node dist/main.js",
"start:dev": "nest start --watch",
"test": "vitest run",
"test:unit": "vitest run --config vitest.config.ts",
"test:e2e": "vitest run --config vitest.e2e.config.ts",
"test:cov": "vitest run --coverage",
"lint": "eslint \"{src,test}/**/*.ts\"",
"prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev"
},
"dependencies": {
"@davinci/shared": "workspace:*",
"@nestjs/common": "^11.0.0",
"@nestjs/config": "^4.0.0",
"@nestjs/core": "^11.0.0",
"@nestjs/platform-fastify": "^11.0.0",
"@nestjs/swagger": "^11.2.6",
"@prisma/adapter-pg": "^7.5.0",
"@prisma/client": "^7.0.0",
"@scalar/fastify-api-reference": "^1.48.5",
"dotenv": "^16.4.0",
"joi": "^17.13.0",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.0"
},
"devDependencies": {
"@nestjs/cli": "^11.0.0",
"@nestjs/testing": "^11.0.0",
"@swc/core": "^1.10.0",
"@types/node": "^22.10.0",
"pino-pretty": "^13.1.3",
"prisma": "^7.0.0",
"tsx": "^4.21.0",
"typescript": "^5.7.0",
"unplugin-swc": "^1.5.0",
"vitest": "^3.0.0"
},
"prisma": {
"seed": "npx tsx prisma/seed.ts"
}
}

View File

@@ -0,0 +1,13 @@
import 'dotenv/config';
import { defineConfig } from 'prisma/config';
export default defineConfig({
schema: 'prisma/schema.prisma',
migrations: {
path: 'prisma/migrations',
seed: 'npx tsx prisma/seed.ts',
},
datasource: {
url: process.env['DATABASE_URL'],
},
});

View File

@@ -0,0 +1,14 @@
datasource db {
provider = "postgresql"
}
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
moduleFormat = "cjs"
}
// Placeholder model — removed when real models are added in feature plans
model Placeholder {
id String @id @default(uuid())
}

View File

@@ -0,0 +1,43 @@
/**
* Prisma seed script — creates deterministic test data for local development.
*
* Run: npx prisma db seed
* make dev-seed
*
* This is idempotent — safe to run multiple times.
*
* Replace/extend with real seed data when feature models are added.
*/
import { PrismaClient } from '../src/generated/prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
const adapter = new PrismaPg({ connectionString: process.env['DATABASE_URL']! });
const prisma = new PrismaClient({ adapter });
async function main(): Promise<void> {
console.log('');
console.log('🌱 Seeding davinci-platform dev database...');
console.log('');
// Placeholder — replace when real models exist
const existing = await prisma.placeholder.findFirst();
if (existing) {
console.log(' ⏭ Placeholder record already exists');
} else {
await prisma.placeholder.create({ data: {} });
console.log(' ✅ Placeholder record created');
}
console.log('');
console.log('✅ Seed complete');
console.log('');
}
main()
.catch((e) => {
console.error('❌ Seed failed:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

View File

@@ -0,0 +1,23 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import * as Joi from 'joi';
import { HealthModule } from './health/health.module.js';
import { PrismaModule } from './prisma/prisma.module.js';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
validationSchema: Joi.object({
DATABASE_URL: Joi.string().uri().required(),
PORT: Joi.number().default(3001),
NODE_ENV: Joi.string().valid('development', 'production', 'test').default('development'),
DAVINCI_MODE: Joi.string().valid('mothership', 'community').default('mothership'),
ENABLE_API_DOCS: Joi.string().valid('true', 'false', '1', '0').optional(),
}),
}),
PrismaModule,
HealthModule,
],
})
export class AppModule {}

View File

@@ -0,0 +1,52 @@
import { afterEach, describe, it, expect } from 'vitest';
import { resolveEnableApiDocs, API_VERSIONS } from '../api-docs.setup';
describe('resolveEnableApiDocs', () => {
const originalEnv = process.env.ENABLE_API_DOCS;
afterEach(() => {
if (originalEnv === undefined) {
delete process.env.ENABLE_API_DOCS;
} else {
process.env.ENABLE_API_DOCS = originalEnv;
}
});
it('returns true when ENABLE_API_DOCS=true', () => {
process.env.ENABLE_API_DOCS = 'true';
expect(resolveEnableApiDocs('production')).toBe(true);
});
it('returns true when ENABLE_API_DOCS=1', () => {
process.env.ENABLE_API_DOCS = '1';
expect(resolveEnableApiDocs('production')).toBe(true);
});
it('returns false when ENABLE_API_DOCS=false', () => {
process.env.ENABLE_API_DOCS = 'false';
expect(resolveEnableApiDocs('development')).toBe(false);
});
it('defaults to true in development when not set', () => {
delete process.env.ENABLE_API_DOCS;
expect(resolveEnableApiDocs('development')).toBe(true);
});
it('defaults to false in production when not set', () => {
delete process.env.ENABLE_API_DOCS;
expect(resolveEnableApiDocs('production')).toBe(false);
});
});
describe('API_VERSIONS', () => {
it('has at least one version defined', () => {
expect(API_VERSIONS.length).toBeGreaterThanOrEqual(1);
});
it('has v1 as the default version', () => {
const defaultVersion = API_VERSIONS.find((v) => v.default);
expect(defaultVersion).toBeDefined();
expect(defaultVersion!.version).toBe('1');
expect(defaultVersion!.slug).toBe('v1');
});
});

View File

@@ -0,0 +1,152 @@
import { NestFastifyApplication } from '@nestjs/platform-fastify';
import { ConfigService } from '@nestjs/config';
import {
DocumentBuilder,
OpenAPIObject,
SwaggerModule,
SwaggerDocumentOptions,
} from '@nestjs/swagger';
export interface ApiVersionDefinition {
version: string;
title: string;
slug: string;
default?: boolean;
documentOptions?: SwaggerDocumentOptions;
}
export const API_VERSIONS: ApiVersionDefinition[] = [
{
version: '1',
title: 'API v1',
slug: 'v1',
default: true,
},
];
/**
* Configure multi-version OpenAPI and Scalar API reference UI.
*
* Generates OpenAPI docs via @nestjs/swagger, serves per-version JSON at
* `/api-reference/{slug}/openapi.json`, and registers Scalar UI at `/api-reference`.
*
* The `/api-reference` endpoint is operational (like `/health`) and is NOT
* subject to `/api/v{major}` versioning.
*/
export async function setupApiDocs(
app: NestFastifyApplication,
configService: ConfigService,
): Promise<void> {
const enableDocs = resolveEnableApiDocs(
configService.get<string>('NODE_ENV') || process.env.NODE_ENV || 'development',
);
if (!enableDocs) {
return;
}
const versionDocs = buildVersionedDocuments(app);
registerOpenApiEndpoints(app, versionDocs);
await registerScalarReference(app, versionDocs);
}
/**
* Determine whether API docs should be enabled.
*
* 1. ENABLE_API_DOCS env var (explicit override)
* 2. Default: enabled in development/test, disabled in production
*/
export function resolveEnableApiDocs(nodeEnv: string): boolean {
const explicit = process.env.ENABLE_API_DOCS;
if (explicit !== undefined) {
return explicit === 'true' || explicit === '1';
}
return nodeEnv !== 'production';
}
export function buildVersionedDocuments(app: NestFastifyApplication): Map<string, OpenAPIObject> {
const docs = new Map<string, OpenAPIObject>();
for (const version of API_VERSIONS) {
docs.set(version.slug, buildOpenApiDocument(app, version));
}
return docs;
}
export function buildOpenApiDocument(
app: NestFastifyApplication,
version?: ApiVersionDefinition,
): OpenAPIObject {
const v = version?.version ?? '1';
const config = new DocumentBuilder()
.setTitle('Davinci Platform API')
.setDescription(
'Unified API for the Davinci platform.\n\n' +
'## Versioning\n\n' +
`All business endpoints are served under \`/api/v${v}\`. ` +
'Operational endpoints (`/health`, `/api-reference`) are unversioned.',
)
.setVersion('0.1.0')
.setContact('Davinci', '', '')
.setLicense('UNLICENSED', '')
.addBearerAuth(
{
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
description:
'JWT access token. Obtain via POST /api/v1/auth/login or /api/v1/auth/register.',
},
'bearer',
)
.build();
return SwaggerModule.createDocument(app, config, version?.documentOptions);
}
function registerOpenApiEndpoints(
app: NestFastifyApplication,
versionDocs: Map<string, OpenAPIObject>,
): void {
const fastify = app.getHttpAdapter().getInstance();
for (const [slug, doc] of versionDocs) {
fastify.get(
`/api-reference/${slug}/openapi.json`,
{ schema: { hide: true } as Record<string, unknown> },
async () => doc,
);
}
}
async function registerScalarReference(
app: NestFastifyApplication,
versionDocs: Map<string, OpenAPIObject>,
): Promise<void> {
const fastify = app.getHttpAdapter().getInstance();
// Dynamic import — @scalar/fastify-api-reference is ESM-only
const { default: scalarFastifyApiReference } = await import('@scalar/fastify-api-reference');
const sources = API_VERSIONS.filter((v) => versionDocs.has(v.slug)).map((v) => ({
title: v.title,
slug: v.slug,
url: `/api-reference/${v.slug}/openapi.json`,
...(v.default && { default: true }),
}));
await fastify.register(scalarFastifyApiReference, {
routePrefix: '/api-reference',
configuration: {
sources,
theme: 'kepler',
hideClientButton: true,
showDeveloperTools: 'never',
hideDownloadButton: true,
agent: { disabled: true },
} as Record<string, unknown>,
});
}

View File

@@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { describe, it, expect, beforeEach } from 'vitest';
import { HealthController } from './health.controller.js';
describe('HealthController', () => {
let controller: HealthController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [HealthController],
}).compile();
controller = module.get<HealthController>(HealthController);
});
it('should return ok status with timestamp', () => {
const result = controller.check();
expect(result.status).toBe('ok');
expect(result.timestamp).toBeDefined();
expect(typeof result.timestamp).toBe('string');
});
});

View File

@@ -0,0 +1,12 @@
import { Controller, Get, VERSION_NEUTRAL } from '@nestjs/common';
@Controller({ path: 'health', version: VERSION_NEUTRAL })
export class HealthController {
@Get()
check() {
return {
status: 'ok',
timestamp: new Date().toISOString(),
};
}
}

View File

@@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { HealthController } from './health.controller.js';
@Module({
controllers: [HealthController],
})
export class HealthModule {}

57
packages/api/src/main.ts Normal file
View File

@@ -0,0 +1,57 @@
import { NestFactory } from '@nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
import { VersioningType, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { AppModule } from './app.module.js';
import { setupApiDocs } from './docs/api-docs.setup.js';
async function bootstrap() {
const logger = new Logger('Bootstrap');
const isDev = process.env.NODE_ENV !== 'production';
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter({
logger: {
level: process.env.LOG_LEVEL || (isDev ? 'debug' : 'info'),
...(isDev && {
transport: {
target: 'pino-pretty',
options: { colorize: true, singleLine: true },
},
}),
},
}),
);
// Global prefix — all routes under /api, except operational endpoints
app.setGlobalPrefix('api', {
exclude: ['health', 'health/ready', 'api-reference', 'api-reference/{*path}'],
});
// URI-based versioning: /api/v1/..., /api/v2/...
app.enableVersioning({
type: VersioningType.URI,
defaultVersion: '1',
});
// CORS — env-driven: permissive in dev, strict in production
const corsOrigin = process.env.CORS_ORIGIN;
app.enableCors({
origin: corsOrigin ? corsOrigin.split(',').map((o) => o.trim()) : true,
credentials: true,
});
// Interactive API documentation (Scalar) — conditional on ENABLE_API_DOCS
const configService = app.get(ConfigService);
await setupApiDocs(app, configService);
// Graceful shutdown
app.enableShutdownHooks();
const port = process.env.PORT ?? 3001;
await app.listen(port, '0.0.0.0');
logger.log(`API running on http://localhost:${port}`);
}
bootstrap();

View File

@@ -0,0 +1,33 @@
import { Test, TestingModule } from '@nestjs/testing';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { PrismaService } from '../prisma.service.js';
import { ConfigService } from '@nestjs/config';
describe('PrismaService', () => {
let service: PrismaService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
PrismaService,
{
provide: ConfigService,
useValue: {
getOrThrow: vi.fn().mockReturnValue('postgresql://test:test@localhost:5432/test'),
},
},
],
}).compile();
service = module.get<PrismaService>(PrismaService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('should expose PrismaClient methods directly (extends pattern)', () => {
expect(typeof service.$connect).toBe('function');
expect(typeof service.$disconnect).toBe('function');
});
});

View File

@@ -0,0 +1,9 @@
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service.js';
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}

View File

@@ -0,0 +1,33 @@
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PrismaClient } from '../generated/prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
/**
* NestJS-managed Prisma service for Prisma 7.
*
* Follows the official NestJS + Prisma 7 recipe:
* - Uses `prisma-client` generator with `moduleFormat = "cjs"`
* - Extends PrismaClient directly for full type-safe API access
* - Uses PrismaPg driver adapter with connectionString
*
* Usage in feature modules:
* constructor(private readonly prisma: PrismaService) {}
* await this.prisma.placeholder.findMany();
*/
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
constructor(configService: ConfigService) {
const databaseUrl = configService.getOrThrow<string>('DATABASE_URL');
const adapter = new PrismaPg({ connectionString: databaseUrl });
super({ adapter });
}
async onModuleInit(): Promise<void> {
await this.$connect();
}
async onModuleDestroy(): Promise<void> {
await this.$disconnect();
}
}

View File

@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["dist", "node_modules", "**/*.spec.ts", "**/*.test.ts", "test"]
}

View File

@@ -0,0 +1,15 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"declaration": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"paths": {
"@davinci/shared": ["../shared/src/index.ts"]
}
},
"include": ["src"],
"exclude": ["dist", "node_modules"]
}

View File

@@ -0,0 +1,18 @@
import swc from 'unplugin-swc';
import { defineConfig } from 'vitest/config';
import path from 'path';
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['src/**/*.spec.ts', 'src/**/*.test.ts'],
exclude: ['src/**/*.e2e-spec.ts'],
},
plugins: [swc.vite()],
resolve: {
alias: {
'@davinci/shared': path.resolve(__dirname, '../shared/src/index.ts'),
},
},
});

View File

@@ -0,0 +1,17 @@
import swc from 'unplugin-swc';
import { defineConfig } from 'vitest/config';
import path from 'path';
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['test/**/*.e2e-spec.ts', 'src/**/*.e2e-spec.ts'],
},
plugins: [swc.vite()],
resolve: {
alias: {
'@davinci/shared': path.resolve(__dirname, '../shared/src/index.ts'),
},
},
});

View File

@@ -0,0 +1,20 @@
{
"name": "@davinci/shared",
"version": "0.1.0-alpha.0",
"description": "Davinci Platform — shared types, DTOs, and constants",
"author": "Galaxis",
"private": true,
"license": "UNLICENSED",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc -p tsconfig.json",
"test": "vitest run",
"test:unit": "vitest run",
"test:cov": "vitest run --coverage"
},
"devDependencies": {
"typescript": "^5.7.0",
"vitest": "^3.0.0"
}
}

View File

@@ -0,0 +1,9 @@
import { describe, it, expect } from 'vitest';
import { APP_NAME } from '../index.js';
describe('@davinci/shared', () => {
it('exports APP_NAME constant', () => {
expect(APP_NAME).toBeDefined();
expect(APP_NAME).toBe('Davinci Platform');
});
});

View File

@@ -0,0 +1 @@
export const APP_NAME = 'Davinci Platform';

View File

@@ -0,0 +1 @@
export { APP_NAME } from './constants/index.js';

View File

@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"declaration": true,
"composite": true
},
"include": ["src"],
"exclude": ["dist", "node_modules", "**/*.test.ts"]
}

View File

@@ -0,0 +1,8 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
},
});

View File

@@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View File

@@ -0,0 +1,5 @@
{
"app": {
"title": "Davinci Platform"
}
}

6
packages/web/next-env.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -0,0 +1,10 @@
import createNextIntlPlugin from 'next-intl/plugin';
import type { NextConfig } from 'next';
const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts');
const nextConfig: NextConfig = {
output: 'standalone',
};
export default withNextIntl(nextConfig);

40
packages/web/package.json Normal file
View File

@@ -0,0 +1,40 @@
{
"name": "@davinci/web",
"version": "0.1.0-alpha.0",
"description": "Davinci Platform — Next.js frontend (App Router)",
"author": "Galaxis",
"private": true,
"license": "UNLICENSED",
"scripts": {
"dev": "next dev --port 3000",
"build": "next build",
"start": "next start",
"test": "vitest run",
"test:unit": "vitest run",
"test:cov": "vitest run --coverage",
"lint": "eslint \"src/**/*.{ts,tsx}\""
},
"dependencies": {
"@davinci/shared": "workspace:*",
"clsx": "^2.1.0",
"next": "^15.5.10",
"next-intl": "^4.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwind-merge": "^3.0.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.0.0",
"@testing-library/jest-dom": "^6.6.0",
"@testing-library/react": "^16.1.0",
"@types/node": "^22.10.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.3.0",
"jsdom": "^28.1.0",
"postcss": "^8.4.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.7.0",
"vitest": "^3.0.0"
}
}

View File

@@ -0,0 +1,7 @@
const config = {
plugins: {
'@tailwindcss/postcss': {},
},
};
export default config;

View File

@@ -0,0 +1,17 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import HomePage from '../app/[locale]/page';
vi.mock('next-intl', () => ({
useTranslations: () => (key: string) => {
const messages: Record<string, string> = { title: 'Davinci Platform' };
return messages[key] ?? key;
},
}));
describe('HomePage', () => {
it('renders the app title', () => {
render(<HomePage />);
expect(screen.getByText('Davinci Platform')).toBeInTheDocument();
});
});

View File

@@ -0,0 +1 @@
import '@testing-library/jest-dom/vitest';

View File

@@ -0,0 +1,27 @@
import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';
import { notFound } from 'next/navigation';
import { routing } from '@/i18n/routing';
type Props = {
children: React.ReactNode;
params: Promise<{ locale: string }>;
};
export default async function LocaleLayout({ children, params }: Props) {
const { locale } = await params;
if (!routing.locales.includes(locale as (typeof routing.locales)[number])) {
notFound();
}
const messages = await getMessages();
return (
<html lang={locale}>
<body>
<NextIntlClientProvider messages={messages}>{children}</NextIntlClientProvider>
</body>
</html>
);
}

View File

@@ -0,0 +1,11 @@
import { useTranslations } from 'next-intl';
export default function HomePage() {
const t = useTranslations('app');
return (
<main className="flex min-h-screen flex-col items-center justify-center">
<h1 className="text-4xl font-bold">{t('title')}</h1>
</main>
);
}

View File

@@ -0,0 +1,82 @@
@import "tailwindcss";
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.965 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.965 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.965 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--radius: 0.625rem;
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.145 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.145 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.396 0.141 25.723);
--destructive-foreground: oklch(0.637 0.237 25.331);
--border: oklch(0.269 0 0);
--input: oklch(0.269 0 0);
--ring: oklch(0.439 0 0);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -0,0 +1,11 @@
import type { Metadata } from 'next';
import './globals.css';
export const metadata: Metadata = {
title: 'Davinci Platform',
description: 'Davinci Platform — community and mothership',
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return children;
}

View File

@@ -0,0 +1,15 @@
import { getRequestConfig } from 'next-intl/server';
import { routing } from './routing';
export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale;
if (!locale || !routing.locales.includes(locale as (typeof routing.locales)[number])) {
locale = routing.defaultLocale;
}
return {
locale,
messages: (await import(`../../messages/${locale}.json`)).default,
};
});

View File

@@ -0,0 +1,6 @@
import { defineRouting } from 'next-intl/routing';
export const routing = defineRouting({
locales: ['en'],
defaultLocale: 'en',
});

View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@@ -0,0 +1,8 @@
import createMiddleware from 'next-intl/middleware';
import { routing } from './i18n/routing';
export default createMiddleware(routing);
export const config = {
matcher: ['/', '/(en)/:path*'],
};

View File

@@ -0,0 +1,18 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "esnext",
"moduleResolution": "bundler",
"jsx": "preserve",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"noEmit": true,
"plugins": [{ "name": "next" }],
"paths": {
"@/*": ["./src/*"],
"@davinci/shared": ["../shared/src/index.ts"]
}
},
"include": ["src", "next-env.d.ts", ".next/types/**/*.ts"],
"exclude": ["dist", "node_modules"]
}

View File

@@ -0,0 +1,18 @@
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/__tests__/setup.ts'],
},
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
'@davinci/shared': path.resolve(__dirname, '../shared/src/index.ts'),
},
},
});

7783
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

10
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,10 @@
packages:
- 'packages/*'
allowBuilds:
'@nestjs/core': false
'@parcel/watcher': false
'@prisma/engines': false
'@swc/core': false
esbuild: false
prisma: false
sharp: false

0
scripts/.gitkeep Normal file
View File

108
smoke-test.sh Normal file
View File

@@ -0,0 +1,108 @@
#!/bin/bash
# Smoke tests for davinci-platform.
# Validates core endpoints are responding correctly.
#
# Ports are configurable via environment variables so the smoke tests
# can run against either the dev stack (3001/3000) or the isolated test
# stack (3101/3100).
#
# Usage:
# bash smoke-test.sh # Against dev stack
# API_PORT=4301 WEB_PORT=4001 bash smoke-test.sh # Against test stack
# SKIP_WEB=1 bash smoke-test.sh # API only (no web container)
# make smoke # Against dev stack (via Makefile)
API="http://localhost:${API_PORT:-4300}"
WEB="http://localhost:${WEB_PORT:-4000}"
PASS=0
FAIL=0
RESULTS=""
smoke() {
local label="$1"
local expect_code="$2"
local actual_code="$3"
local body="$4"
if [ "$actual_code" = "$expect_code" ]; then
PASS=$((PASS + 1))
RESULTS="$RESULTS\n ✅ $label$actual_code"
else
FAIL=$((FAIL + 1))
RESULTS="$RESULTS\n ❌ $label$actual_code (expected $expect_code)"
if [ -n "$body" ]; then
RESULTS="$RESULTS\n Body: $(echo "$body" | head -c 200)"
fi
fi
}
echo ""
echo "========================================="
echo " DAVINCI PLATFORM SMOKE TESTS"
echo " API: $API"
echo " Web: $WEB"
echo "========================================="
echo ""
# ─── API Tests ────────────────────────────────────────────────────
# 1. Health endpoint
RESP=$(curl -s -w "\n%{http_code}" "$API/health")
BODY=$(echo "$RESP" | sed '$d')
CODE=$(echo "$RESP" | tail -1)
smoke "GET /health" "200" "$CODE" "$BODY"
# 2. Health response contains status:ok
if echo "$BODY" | grep -q '"status":"ok"'; then
smoke "Health body contains status:ok" "200" "200"
else
smoke "Health body contains status:ok" "200" "FAIL" "$BODY"
fi
# 3. Unknown API route returns 404
RESP=$(curl -s -w "\n%{http_code}" "$API/api/nonexistent")
CODE=$(echo "$RESP" | tail -1)
smoke "GET /api/nonexistent" "404" "$CODE"
# ─── Web Tests ────────────────────────────────────────────────────
if [ -n "$SKIP_WEB" ]; then
echo " ⏭ Skipping web tests (SKIP_WEB is set)"
else
# 4. Web root redirects to locale
RESP=$(curl -s -o /dev/null -w "%{http_code}" "$WEB/")
smoke "GET / (redirect to locale)" "307" "$RESP"
# 5. Web /en returns 200
RESP=$(curl -sL -w "\n%{http_code}" "$WEB/en")
BODY=$(echo "$RESP" | sed '$d')
CODE=$(echo "$RESP" | tail -1)
smoke "GET /en" "200" "$CODE"
# 6. Web page contains expected content
if echo "$BODY" | grep -q "Davinci Platform"; then
smoke "Page contains 'Davinci Platform'" "200" "200"
else
smoke "Page contains 'Davinci Platform'" "200" "FAIL" "$BODY"
fi
fi # end SKIP_WEB
# ─── Results ──────────────────────────────────────────────────────
echo ""
echo "========================================="
echo " RESULTS: $PASS passed, $FAIL failed"
echo "========================================="
echo -e "$RESULTS"
echo ""
if [ "$FAIL" -gt 0 ]; then
echo "❌ SMOKE TESTS FAILED"
exit 1
else
echo "✅ ALL SMOKE TESTS PASSED"
exit 0
fi

21
tsconfig.base.json Normal file
View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"declaration": true,
"removeComments": true,
"target": "ES2022",
"sourceMap": true,
"incremental": true,
"skipLibCheck": true,
"isolatedModules": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"resolveJsonModule": true,
"esModuleInterop": true
}
}

29
turbo.json Normal file
View File

@@ -0,0 +1,29 @@
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"dev": {
"persistent": true,
"cache": false
},
"lint": {
"dependsOn": ["^build"]
},
"test": {
"dependsOn": ["^build"]
},
"test:unit": {
"dependsOn": ["^build"]
},
"test:e2e": {
"dependsOn": ["^build"]
},
"test:cov": {
"dependsOn": ["^build"]
},
"format:check": {}
}
}