# destroying_sap A fullstack SaaS web application built with FastAPI, React, and PostgreSQL. ## Stack | Layer | Tech | |---|---| | Backend | FastAPI (async), SQLAlchemy 2, Alembic, PostgreSQL 16 | | Auth | JWT bearer tokens (RS256), bcrypt password hashing | | Frontend | React 18, TypeScript, Vite, React Router v6, TanStack Query | ## Current State - User registration and login (JWT RS256 auth, 8-hour expiry) - Protected dashboard with nav bar - `/api/users/me` — authenticated user info - `/api/profile/me` — GET/PUT personal profile (position, phone, date of birth, address) - Admin-only user management at `/admin`: list, add, delete, toggle active - All input sanitized before reaching the DB (null-byte rejection, length caps, format validation) - **PDF Documents app** (`/apps/documents`): upload PDFs, async text extraction (pdfplumber), AI classification via ai-service, per-user categories, file download - **AI Service** (`ai-service:8010`): shared AI intermediary container; routes prompts to Anthropic / Ollama / LM Studio; stateless; all feature containers talk to it via `POST /chat` - **Storage Service** (`storage-service:8020`): unified file/blob storage with pluggable backends (local filesystem default; S3-compatible and WebDAV built in); backend switchable via admin UI with zero-data-loss migration - Admin settings: AI provider, doc upload limits, storage backend switching with live migration progress - Config stored in storage-service (`config` bucket); PDFs stored in storage-service (`documents` bucket) — no shared filesystem volumes - `/apps` launcher hub — one card per installed app with Open + Settings links - 6 separate Docker containers: `db`, `backend`, `ai-service`, `doc-service`, `storage-service`, `frontend` - All containers run as non-root users (UID 1001 for app containers, UID 70 for db) - Network-isolated: only the frontend exposes a host port (80/5173); all backend services are unreachable from outside Docker - Dev environment seeds a test user automatically on startup (`test@example.com` / `Test123!`) - Password policy: min 8 chars, upper + lowercase, digit, special character, no common words - Pre-commit security hook (`scripts/security_check.py`) runs inside Docker on every commit ## Containers | Container | Image | Host port | Network | User (UID:GID) | Description | |---|---|---|---|---|---| | `db` | postgres:16-alpine | none | backend-net | 70:70 | PostgreSQL database | | `backend` | custom (python:3.12-slim) | none | backend-net | 1001:1001 | FastAPI management API + proxy to doc-service | | `ai-service` | custom (python:3.12-slim) | none | backend-net | 1001:1001 | Shared AI intermediary (routes to LM Studio / Ollama / Anthropic) | | `doc-service` | custom (python:3.12-slim) | none | backend-net | 1001:1001 | PDF extraction microservice (calls ai-service) | | `storage-service` | custom (python:3.12-slim) | none | backend-net | 1001:1001 | Unified file/blob storage (local / S3-compatible / WebDAV) | | `frontend` | custom (nginxinc/nginx-unprivileged:alpine) | 80 (prod) / 5173 (dev) | backend-net + frontend-net | 1001:1001 | React UI + nginx reverse proxy | **Networks:** - `backend-net` — all backend services; no host ports bound; outbound internet access allowed (needed for cloud AI API calls) - `frontend-net` — frontend only; this is where the single host port (80/5173) is bound **Volumes:** - `postgres_data` — PostgreSQL data files - `storage_data` — all file/blob storage: uploaded PDFs (`documents/` bucket) and service config JSON files (`config/` bucket); mounted into storage-service at `/data/storage` - `watch_data` — file watcher input directory; mounted into doc-service at `/data/watch` The frontend nginx proxies `/api/*` to `backend:8000` via `backend-net`. The backend proxies `/api/documents/*` and `/api/documents/categories/*` to `doc-service:8001`. The backend test-connection endpoint proxies to `ai-service:8010`. No backend service or database port is ever exposed to the host. ## Installation ### Prerequisites - Docker + Docker Compose ### Production ```bash git clone cd destroying_sap cp .env.example backend/.env python scripts/generate_jwt_keys.py # paste output into backend/.env docker compose up --build -d ``` - Frontend: http://localhost - API docs: not directly accessible from host (backend port not exposed) After first start, configure the AI provider at `/apps/documents/settings/admin` (admin login required). ### Development (hot reload) ```bash docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build ``` - Frontend (Vite): http://localhost:5173 - Backend: reachable by frontend via Docker network only (not exposed to host) ### Local (no Docker) **1. Start PostgreSQL** ```bash docker compose up db -d ``` **2. Backend** ```bash cd backend python -m venv .venv && source .venv/bin/activate pip install -e ".[dev]" cp ../.env.example .env alembic upgrade head uvicorn app.main:app --reload ``` **3. doc-service** ```bash cd features/doc-service python -m venv .venv && source .venv/bin/activate pip install -e . alembic upgrade head uvicorn app.main:app --host 0.0.0.0 --port 8001 --reload ``` **4. Frontend** ```bash cd frontend && npm install && npm run dev ``` ## Environment Variables Copy `.env.example` to `backend/.env` and adjust: | Variable | Default | Description | |---|---|---| | `DATABASE_URL` | `postgresql+asyncpg://postgres:password@localhost:5432/destroying_sap` | Async PostgreSQL URL | | `JWT_PRIVATE_KEY` | — | RS256 private key PEM (generate with `scripts/generate_jwt_keys.py`) | | `JWT_PUBLIC_KEY` | — | RS256 public key PEM (generate with `scripts/generate_jwt_keys.py`) | | `CORS_ORIGINS` | `["http://localhost:5173"]` | Allowed frontend origins | | `DOC_SERVICE_URL` | `http://doc-service:8001` | Internal URL of doc-service (set by docker-compose) | | `STORAGE_SERVICE_URL` | `http://storage-service:8020` | Internal URL of storage-service (set by docker-compose) | ## Development ```bash # Backend lint + format cd backend && ruff check . && ruff format . # Backend tests cd backend && pytest # Frontend type check + lint cd frontend && npm run typecheck && npm run lint # New DB migration — main backend cd backend && alembic revision --autogenerate -m "describe change" cd backend && alembic upgrade head # New DB migration — doc-service cd features/doc-service && alembic revision --autogenerate -m "describe change" cd features/doc-service && alembic upgrade head ```