18a638bc3a4ff9933b281b3bf6ce14af15988fc4
- Fix: list_plugins imported _REGISTRY as a direct reference to the empty list that existed at import time; register_services() replaces _REGISTRY with a new list so the imported reference was always []. Added get_registry() helper so callers access the live list via the module namespace. GET /api/plugins now correctly returns accessible plugins for the current user. - Fix: switch watchdog from InotifyObserver to PollingObserver. Inotify events from the macOS host are not forwarded through the Docker bind mount, so new files were only detected via the startup scan. PollingObserver (1s default interval) works reliably on all platforms including macOS+Docker bind mounts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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 viaPOST /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.jsonand/config/doc_service_config.json /appslauncher 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 filesdoc_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
Description
Languages
Python
53.2%
TypeScript
44.1%
Dockerfile
1%
CSS
0.9%
Shell
0.5%
Other
0.2%