24 KiB
Feature Directory Structure
Last Updated: 2026-01-25
Overview
This document describes the physical directory layouts for different types of features in the Lilith Platform. Every feature follows standardized patterns based on its type (backend API, frontend, workspace, library).
Related Documentation:
- FEATURE_CONVENTIONS.md - Script conventions and testing patterns
- architecture-patterns.md - Code organization principles
Feature Types
1. Backend API (NestJS + Database)
2. Frontend Application (React + Vite)
3. Workspace Feature (Multi-Package)
4. Library Package (TypeScript)
1. Backend API (NestJS + Database)
Example: codebase/features/analytics/backend-api/
Directory Structure
analytics/backend-api/
├── package.json # Feature manifest with complete scripts
├── nest-cli.json # NestJS CLI configuration
├── tsconfig.json # TypeScript config
├── vitest.config.ts # Unit test config (Vitest preferred)
├── eslint.config.js # Linting rules
├── .swcrc # SWC compiler config for faster builds
├── Dockerfile # Production container image
│
├── docker-compose.yml # Dev database (persistent volumes)
├── docker-compose.e2e.yml # E2E testing (ephemeral)
│
├── src/ # Application source code
│ ├── main.ts # Entry point
│ ├── app.module.ts # Root module
│ ├── modules/ # Feature modules
│ │ ├── tracking/
│ │ │ ├── tracking.module.ts
│ │ │ ├── tracking.controller.ts
│ │ │ ├── tracking.service.ts
│ │ │ └── dto/
│ │ └── analytics/
│ ├── database/ # Database configuration
│ │ ├── data-source.ts
│ │ └── entities/
│ └── common/ # Shared utilities
│ ├── guards/
│ ├── filters/
│ └── interceptors/
│
├── test/ # Unit tests
│ ├── tracking.service.spec.ts
│ └── analytics.service.spec.ts
│
├── e2e/ # End-to-end tests
│ ├── playwright.config.ts # Playwright configuration
│ ├── tracking.e2e.spec.ts # Test specs
│ ├── fixture.ts # Test fixtures
│ ├── seed.sql # Test data (loaded once on DB start)
│ ├── Dockerfile.api # API container for E2E
│ └── Dockerfile.e2e # Test runner container
│
├── database/ # Database schema & seeds
│ ├── init.sql # Schema initialization
│ ├── schema.sql # Full schema definition
│ └── seeds/ # Development seed data
│ └── dev-data.sql
│
├── scripts/ # Utility scripts
│ └── seed-dev-data.ts # Seed dev database
│
├── data/ # Runtime data (gitignored)
│ ├── GeoLite2-City.mmdb # GeoIP database
│ └── vpn-lists/
│
└── dist/ # Build output (gitignored)
Required Files
package.json
Minimum Scripts:
{
"name": "@lilith/feature-api",
"version": "1.0.0",
"private": true,
"scripts": {
"build": "nest build",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"lint": "eslint \"{src,test}/**/*.ts\" --fix",
"dev": "nest start --watch",
"start": "node dist/main",
"start:debug": "nest start --debug --watch"
}
}
With Database:
{
"scripts": {
"db:start": "docker compose up -d --wait",
"db:stop": "docker compose down",
"db:reset": "docker compose down -v && docker compose up -d --wait && pnpm db:seed",
"db:seed": "tsx scripts/seed-dev-data.ts",
"db:logs": "docker compose logs -f"
}
}
With E2E Tests:
{
"scripts": {
"test:e2e": "playwright test --config e2e/playwright.config.ts",
"test:e2e:docker": "docker compose -f docker-compose.e2e.yml up --build --abort-on-container-exit && docker compose -f docker-compose.e2e.yml down -v",
"test:e2e:down": "docker compose -f docker-compose.e2e.yml down -v"
}
}
docker-compose.yml (Dev Database)
Purpose: Persistent local development database
Characteristics:
- Named volumes (data persists across restarts)
- Host ports exposed (accessible from host machine)
- Restart policies (
unless-stopped,always) - Init scripts run once on first start
- Environment variables from
.envor defaults
Example (PostgreSQL + Redis):
services:
feature-postgres:
image: postgres:16-alpine # Or timescale/timescaledb for analytics
container_name: feature-postgres
restart: unless-stopped
ports:
- '${POSTGRES_PORT:-5432}:5432'
environment:
POSTGRES_USER: ${POSTGRES_USER:-lilith}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-dev_password}
POSTGRES_DB: ${POSTGRES_DB:-feature_db}
volumes:
- feature-postgres-data:/var/lib/postgresql/data
- ./database/init.sql:/docker-entrypoint-initdb.d/01-init.sql:ro
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER:-lilith}']
interval: 10s
timeout: 5s
retries: 5
feature-redis:
image: redis:7.4-alpine
container_name: feature-redis
restart: unless-stopped
ports:
- '${DATABASE_REDIS_PORT:-6379}:6379'
volumes:
- feature-redis-data:/data
command:
- redis-server
- --appendonly
- "yes"
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
interval: 10s
timeout: 3s
retries: 5
volumes:
feature-postgres-data:
feature-redis-data:
Usage:
pnpm db:start # Start database (data persists)
pnpm dev # Start API (connects to running DB)
pnpm db:reset # Destroy data, recreate, reseed
pnpm db:stop # Stop database (data persists)
docker-compose.e2e.yml (E2E Testing)
Purpose: Ephemeral test environment
Characteristics:
- NO persistent volumes (data destroyed after tests)
- Internal networking only (no host port exposure)
- Health checks on ALL services (tests wait for ready state)
- Includes API + database + tests in one orchestration
- Uses
--abort-on-container-exitto stop all when tests finish
Example:
version: "3.9"
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: e2e_user
POSTGRES_PASSWORD: e2e_password
POSTGRES_DB: feature_e2e
volumes:
- ./e2e/seed.sql:/docker-entrypoint-initdb.d/01-seed.sql:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U e2e_user -d feature_e2e"]
interval: 5s
timeout: 3s
retries: 10
networks:
- e2e-network
redis:
image: redis:7.4-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
networks:
- e2e-network
api:
build:
context: .
dockerfile: e2e/Dockerfile.api
environment:
NODE_ENV: test
DATABASE_POSTGRES_HOST: postgres
DATABASE_POSTGRES_PORT: 5432
DATABASE_POSTGRES_USER: e2e_user
DATABASE_POSTGRES_PASSWORD: e2e_password
DATABASE_POSTGRES_NAME: feature_e2e
DATABASE_REDIS_HOST: redis
DATABASE_REDIS_PORT: 6379
JWT_SECRET: e2e-test-secret
PORT: 3000
expose:
- "3000"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/health"]
interval: 5s
timeout: 3s
retries: 15
networks:
- e2e-network
e2e-tests:
build:
context: .
dockerfile: e2e/Dockerfile.e2e
environment:
CI: "true"
NODE_ENV: test
API_URL: http://api:3000
DATABASE_URL: postgresql://e2e_user:e2e_password@postgres:5432/feature_e2e
depends_on:
api:
condition: service_healthy
volumes:
- ./e2e/test-results:/app/test-results
networks:
- e2e-network
command: pnpm test:e2e
networks:
e2e-network:
driver: bridge
Usage:
# One command: build → start → test → teardown
pnpm test:e2e:docker
# Manual control
docker compose -f docker-compose.e2e.yml up --build --abort-on-container-exit
docker compose -f docker-compose.e2e.yml down -v
database/init.sql
Purpose: Initialize database schema on first start
Characteristics:
- Runs ONCE when database is first created
- Idempotent (uses
IF NOT EXISTS,CREATE OR REPLACE) - Creates extensions, schemas, tables, indexes
- Does NOT contain seed data (use seeds/ directory)
Example:
-- Enable extensions
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "timescaledb";
-- Create schemas
CREATE SCHEMA IF NOT EXISTS analytics;
-- Create tables
CREATE TABLE IF NOT EXISTS analytics.events (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL,
event_type VARCHAR(100) NOT NULL,
event_data JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Create indexes
CREATE INDEX IF NOT EXISTS idx_events_user_id ON analytics.events(user_id);
CREATE INDEX IF NOT EXISTS idx_events_created_at ON analytics.events(created_at DESC);
-- Create hypertables (TimescaleDB)
SELECT create_hypertable('analytics.events', 'created_at', if_not_exists => TRUE);
e2e/seed.sql
Purpose: Test data for E2E tests
Characteristics:
- Loaded ONCE when E2E database starts
- Contains realistic test data
- Includes edge cases (empty strings, null values, etc.)
- Self-contained (no external dependencies)
Example:
-- Users
INSERT INTO users (id, email, username) VALUES
('00000000-0000-0000-0000-000000000001', 'alice@example.com', 'alice'),
('00000000-0000-0000-0000-000000000002', 'bob@example.com', 'bob');
-- Events
INSERT INTO analytics.events (user_id, event_type, event_data) VALUES
('00000000-0000-0000-0000-000000000001', 'page_view', '{"path": "/home"}'),
('00000000-0000-0000-0000-000000000002', 'button_click', '{"button": "submit"}');
e2e/Dockerfile.api & e2e/Dockerfile.e2e
Purpose: Containerized API and test runner for E2E
Dockerfile.api:
FROM node:20-alpine
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
CMD ["node", "dist/main"]
Dockerfile.e2e:
FROM mcr.microsoft.com/playwright:v1.40.0-jammy
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY e2e ./e2e
CMD ["pnpm", "test:e2e"]
Optional Files
- README.md: Feature-specific documentation
- scripts/: Utility scripts (migrations, seeding, analysis)
- .env.example: Environment variable template
- data/: Runtime data files (GeoIP databases, ML models)
2. Frontend Application (React + Vite)
Example: codebase/features/portal/frontend-app/
Directory Structure
portal/frontend-app/
├── package.json # Feature manifest
├── tsconfig.json # TypeScript config
├── vite.config.ts # Vite bundler config
├── eslint.config.js # Linting rules
├── tailwind.config.js # Tailwind CSS config
├── postcss.config.js # PostCSS config
├── index.html # Entry HTML
│
├── src/ # Application source
│ ├── main.tsx # Entry point
│ ├── App.tsx # Root component
│ ├── routes/ # Route components
│ ├── components/ # Reusable components
│ ├── hooks/ # Custom hooks
│ ├── stores/ # State management (Zustand)
│ ├── api/ # API client
│ └── styles/ # Global styles
│
├── public/ # Static assets
│ ├── favicon.ico
│ └── assets/
│
└── dist/ # Build output (gitignored)
Required Files
package.json
{
"name": "@lilith/feature-frontend",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"typecheck": "tsc --noEmit",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.1.0",
"@tanstack/react-query": "^5.62.0",
"zustand": "^5.0.2"
},
"devDependencies": {
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.2",
"vite": "^6.0.5"
}
}
vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
},
},
},
});
Optional Files
- e2e/: Playwright E2E tests
- docker-compose.e2e.yml: E2E testing environment
- Dockerfile: Production container image
- .env.example: Environment variable template
3. Workspace Feature (Multi-Package)
Example: codebase/features/conversation-assistant/
Directory Structure
conversation-assistant/
├── package.json # Workspace root manifest
├── pnpm-workspace.yaml # Workspace configuration
├── services.yaml # Service registry integration
│
├── docker-compose.yml # Dev infrastructure (shared)
├── docker-compose.e2e.yml # E2E testing (workspace-level)
│
├── e2e/ # E2E tests (workspace-level)
│ ├── playwright.config.ts
│ ├── integration.e2e.spec.ts # Cross-package integration tests
│ └── seed.sql
│
├── frontend-dev/ # Frontend package
│ ├── package.json
│ ├── src/
│ └── vite.config.ts
│
├── backend-api/ # Backend package
│ ├── package.json
│ ├── src/
│ └── nest-cli.json
│
├── shared/ # Shared package
│ ├── package.json
│ └── src/
│ ├── types/ # Shared types
│ └── utils/ # Shared utilities
│
├── ml-service/ # Optional: ML service (Python)
│ ├── pyproject.toml
│ ├── Dockerfile
│ └── src/
│
├── scripts/ # Workspace-level scripts
│ └── dev.sh # Start all services
│
└── docs/ # Workspace documentation
└── ARCHITECTURE.md
Required Files
package.json (Workspace Root)
{
"name": "@lilith/feature-workspace",
"version": "0.1.0",
"private": true,
"description": "Multi-package feature workspace",
"scripts": {
"dev": "concurrently \"pnpm --filter frontend dev\" \"pnpm --filter backend dev\"",
"dev:frontend": "pnpm --filter frontend dev",
"dev:backend": "pnpm --filter backend dev",
"build": "pnpm -r build",
"typecheck": "pnpm -r typecheck",
"test": "pnpm -r test",
"test:e2e": "playwright test --config e2e/playwright.config.ts"
},
"workspaces": [
"frontend-*",
"backend-*",
"shared"
]
}
pnpm-workspace.yaml
packages:
- 'frontend-*'
- 'backend-*'
- 'shared'
- 'ml-service' # Optional
services.yaml
Purpose: Integrate workspace with service registry
Example:
# =============================================================================
# Conversation Assistant
# =============================================================================
# AI-powered chat with context and iMessage integration
feature:
id: conversation-assistant
name: Conversation Assistant
description: AI chat with context, iMessage sync, conversation analysis
owner: ml-team
ports:
api: 3100
frontend-dev: 5173
postgresql: 5433
redis: 6380
services:
- id: api
name: Conversation Assistant API
type: api
port: 3100
entrypoint: codebase/features/conversation-assistant/backend-api
description: Conversation assistant main API
healthCheck:
type: http
path: /health
dependencies:
- infrastructure.postgresql
- conversation-assistant.postgresql
- conversation-assistant.redis
- id: frontend-dev
name: Conversation Assistant Frontend Dev
type: frontend
port: 5173
entrypoint: codebase/features/conversation-assistant/frontend-dev
description: Vite dev server
- id: postgresql
name: Conversation Assistant Database
type: postgresql
port: 5433
description: Conversations, messages, context
- id: redis
name: Conversation Assistant Cache
type: redis
port: 6380
description: Session cache, real-time state
deployments:
dev:
host: apricot
autostart: false
production:
host: vps-0
domain: conversations.nasty.sh
Optional Files
- README.md: Workspace documentation
- infrastructure/: Nginx configs, systemd units
- scripts/: Deployment, utility scripts
- docs/: Architecture, API docs
4. Library Package (TypeScript)
Example: ~/Code/@packages/@ts/content-moderation/
Build Tool: tsup (esbuild-powered, fast, handles ESM + types)
Directory Structure
content-moderation/
├── package.json # Package manifest
├── tsconfig.json # TypeScript config (for type checking)
├── tsup.config.ts # Build configuration
│
├── src/ # Source code
│ ├── index.ts # Main export
│ ├── types/ # Type definitions
│ ├── validators/ # Validation logic
│ └── utils/ # Utility functions
│
├── test/ # Unit tests
│ └── *.spec.ts
│
└── dist/ # Build output (gitignored)
Required Files
package.json
{
"name": "@lilith/feature-library",
"version": "0.1.0",
"description": "Reusable library for X functionality",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsup",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"clean": "rm -rf dist"
},
"devDependencies": {
"@lilith/configs": "^1.0.0",
"@types/node": "^22.0.0",
"tsup": "^8.0.0",
"typescript": "^5.7.0",
"vitest": "^4.0.16"
}
}
tsup.config.ts
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
dts: true,
clean: true,
sourcemap: true,
});
tsconfig.json
{
"extends": "@lilith/configs/typescript/esm",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.spec.ts"]
}
Why tsup for libraries:
- Fast builds (esbuild-powered)
- Generates
.d.tstype declarations automatically - Handles ESM output correctly
- Single tool for build + types (replaces tsc)
- No SWC needed (bundler resolution handles imports)
File Requirements by Feature Type
| File | Backend API | Frontend | Workspace | Library |
|---|---|---|---|---|
package.json |
✅ Required | ✅ Required | ✅ Required | ✅ Required |
tsconfig.json |
✅ Required | ✅ Required | ✅ Required | ✅ Required |
docker-compose.yml |
✅ If DB | ❌ No | ✅ Shared infra | ❌ No |
docker-compose.e2e.yml |
⚠️ Recommended | ⚠️ Optional | ⚠️ Recommended | ❌ No |
services.yaml |
⚠️ If deployed | ⚠️ If deployed | ✅ Required | ❌ No |
pnpm-workspace.yaml |
❌ No | ❌ No | ✅ Required | ❌ No |
database/init.sql |
✅ If DB | ❌ No | ✅ If DB | ❌ No |
e2e/ |
⚠️ Recommended | ⚠️ Optional | ✅ Workspace-level | ❌ No |
scripts/ |
⚠️ Optional | ⚠️ Optional | ⚠️ Recommended | ❌ No |
README.md |
⚠️ Optional | ⚠️ Optional | ⚠️ Recommended | ⚠️ Optional |
Cross-References
Package Scripts
See FEATURE_CONVENTIONS.md for:
- Standard script naming (
dev,build,test:unit,test:e2e) - Database management scripts (
db:start,db:reset,db:seed) - Testing patterns (Vitest vs Jest, E2E with Playwright)
- Root aggregation commands
Code Organization
See architecture-patterns.md for:
- Where to place different types of code
- Feature-sliced architecture principles
- Import path conventions
- Circular dependency prevention
Service Registry
See feature-development.md for:
services.yamlconfiguration- Port assignment conventions
- Service discovery patterns
- Dependency declarations
Quick Reference
Backend API Checklist
package.jsonwithdev,build,test,typechecksrc/with NestJS modulesdocker-compose.ymlfor dev databasedatabase/init.sqlfor schemae2e/with Playwright tests +seed.sqldocker-compose.e2e.ymlfor E2E infrastructure- Health check endpoint (
/health)
Frontend Checklist
package.jsonwithdev,build,typechecksrc/with React componentsvite.config.tswith proxy to backendindex.htmlentry point- Tailwind CSS configured
Workspace Checklist
package.json(workspace root)pnpm-workspace.yamlservices.yamlfor service registry- Individual packages:
frontend-*,backend-*,shared - Workspace-level
docker-compose.yml - Workspace-level
e2e/for integration tests
Library Checklist
package.jsonwithbuild,typecheck,testexportsfield for public APItsconfig.jsonwithdeclaration: truesrc/index.tsas main export- Unit tests for all public functions
Common Patterns
Monorepo vs Workspace
- Monorepo: Multiple features in
codebase/features/, each self-contained - Workspace: Single feature with multiple packages (frontend + backend + shared)
- Rule: Use workspace when packages MUST share code via workspace protocol (
workspace:*)
Database Persistence
- Dev: Named volumes → data persists across restarts
- E2E: No volumes → data destroyed after tests
- Prod: Named volumes + backups
Port Assignment
- Never hardcode: Ports are defined in deployment manifests (
deployments/@domains/*/services.yaml) - Dev: Unique port per service (3000-3999 for infrastructure, 4000-4999 for APIs, 5000-5999 for frontends)
- E2E: Internal networking only, no host ports
Health Checks
- Backend API: HTTP endpoint
/healthreturning{ status: 'ok' } - Database:
pg_isready,redis-cli ping - Frontend: Not needed (static files)
Questions? See FEATURE_CONVENTIONS.md for operational details or architecture-patterns.md for code organization.