platform-docs/technical/features/FEATURE_STRUCTURE.md

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 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 .env or 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-exit to 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.ts type 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.yaml configuration
  • Port assignment conventions
  • Service discovery patterns
  • Dependency declarations

Quick Reference

Backend API Checklist

  • package.json with dev, build, test, typecheck
  • src/ with NestJS modules
  • docker-compose.yml for dev database
  • database/init.sql for schema
  • e2e/ with Playwright tests + seed.sql
  • docker-compose.e2e.yml for E2E infrastructure
  • Health check endpoint (/health)

Frontend Checklist

  • package.json with dev, build, typecheck
  • src/ with React components
  • vite.config.ts with proxy to backend
  • index.html entry point
  • Tailwind CSS configured

Workspace Checklist

  • package.json (workspace root)
  • pnpm-workspace.yaml
  • services.yaml for service registry
  • Individual packages: frontend-*, backend-*, shared
  • Workspace-level docker-compose.yml
  • Workspace-level e2e/ for integration tests

Library Checklist

  • package.json with build, typecheck, test
  • exports field for public API
  • tsconfig.json with declaration: true
  • src/index.ts as 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 /health returning { 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.