2026-04-17 17:25:57 +02:00

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
  • Admin settings: /apps/ai/settings/admin (provider, credentials, test connection); /apps/documents/settings/admin (upload limits only)
  • Config stored in shared Docker volume: /config/ai_service_config.json and /config/doc_service_config.json
  • /apps launcher hub — one card per installed app with Open + Settings links
  • 5 separate Docker containers: db, backend, ai-service, doc-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)
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
  • doc_data — uploaded PDF files (mounted into doc-service at /data/documents)
  • app_config — per-service runtime config JSON files (mounted into backend, ai-service, and doc-service at /config)

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

git clone <repo>
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)

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

docker compose up db -d

2. Backend

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

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

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
APP_CONFIG_DIR /config Directory for per-service runtime config JSON files
DOC_SERVICE_URL http://doc-service:8001 Internal URL of the doc-service (set by docker-compose)

doc-service reads DATABASE_URL, DATA_DIR, and CONFIG_PATH from its own environment (set in docker-compose.yml).

Development

# 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
S
Description
No description provided
Readme 1.4 MiB
Languages
Python 53.2%
TypeScript 44.1%
Dockerfile 1%
CSS 0.9%
Shell 0.5%
Other 0.2%