HTTP API for Chipmo Sentry — auth, multi-tenant org/store/camera/alert CRUD, feedback, OpenAPI contract.
Python 3.11 · FastAPI · SQLAlchemy 2.0 async · Postgres · structlog · Apache 2.0
- Identity & multi-tenancy: User accounts, Organization → Store → Camera hierarchy, role-based access (owner/admin/staff + super-admin)
- JWT auth: httpOnly cookie + Bearer fallback; 15 min access + 7 day refresh
- Clip CRUD: receive uploaded mp4 clips, dispatch to sentry-ai for inference
- Alert CRUD: store AI verdicts, expose via REST + SSE real-time push
- Feedback: collect staff TP/FP marking, feed M3 auto-learner
- OpenAPI contract:
/openapi.jsonis the source of truth for sentry-frontend and Go agent codegen
Not in scope here: video ingest (see sentry-ingest), AI inference (see sentry-ai).
# 1. Install uv (one-time)
pip install uv
# 2. Sync dependencies into ./.venv
uv sync
# 3. Copy env template, fill in secrets
cp .env.example .env
# Generate JWT_SECRET: python -c "import secrets; print(secrets.token_urlsafe(48))"
# Generate SERVICE_TOKEN_SECRET: python -c "import secrets; print(secrets.token_urlsafe(48))"
# Generate RTSP_FERNET_KEY: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
# 4. Spin up Postgres (Docker example)
docker run -d --name sentry-pg \
-e POSTGRES_USER=sentry -e POSTGRES_PASSWORD=sentry -e POSTGRES_DB=sentry_dev \
-p 5432:5432 postgres:16
# 5. Generate + apply initial migration (FIRST RUN ONLY — see "Migrations" below)
uv run alembic revision --autogenerate -m "initial schema"
uv run alembic upgrade head
# 6. Run the API
uv run uvicorn sentry_backend.main:app --reload
# → http://localhost:8000/healthz
# → http://localhost:8000/docs (Swagger UI)
# → http://localhost:8000/openapi.json (contract source of truth)src/sentry_backend/
├── main.py — FastAPI app, lifespan, middleware
├── settings.py — pydantic-settings BaseSettings
├── logging_setup.py — structlog (JSON in prod, pretty in dev)
├── security.py — JWT, bcrypt, cookies, Fernet, service tokens
├── db/
│ ├── session.py — async engine, AsyncSessionLocal
│ ├── base.py — DeclarativeBase, UUIDPrimaryKeyMixin, TimestampMixin
│ └── models/ — ORM (one file per entity)
├── schemas/ — Pydantic request/response (OpenAPI source)
├── deps/ — Dependency injection (get_db, get_current_user, ...)
├── repository/ — CRUD layer (service-callable)
├── services/ — Business logic
├── api/v1/ — Versioned routers
└── api/stream.py — SSE alert push endpoint
alembic/ — Migration env + version files
tests/{unit,integration}/ — pytest suites
5-layer separation: Schema → ORM → Repository → Service → API. Routers do not write SQL.
Alembic is async-aware and reads the database URL from settings.database_url (env), not from alembic.ini. So secrets never land in the repo.
# Generate a migration from ORM changes
uv run alembic revision --autogenerate -m "add foo column to bar"
# Inspect the generated file — autogenerate misses some changes (e.g. constraint renames)
# Edit alembic/versions/<id>_<slug>.py manually if needed
# Apply to current DB
uv run alembic upgrade head
# Roll back one step
uv run alembic downgrade -1The very first migration is NOT committed in this repo. Run
alembic revision --autogenerate -m "initial schema"against a fresh Postgres to generate it, then commit. This avoids shipping a migration that hasn't been verified against a real DB.
# All tests
uv run pytest
# Unit only (fast, no DB)
uv run pytest tests/unit/
# Integration (needs real Postgres — DATABASE_URL pointed at a test DB)
uv run pytest tests/integration/
# With coverage report
uv run pytest --cov=sentry_backend --cov-report=htmluv run ruff format .
uv run ruff check .
uv run mypy src/sentry_backendCI runs all three; PR fails if any of them does.
Target: Railway Pro. Postgres is a Railway addon. CI lives in .github/workflows/ci.yml (ruff + mypy strict + pytest).
-
Create a new Railway project from this GitHub repo (Railway auto-detects
Dockerfile+railway.toml). -
Add the Postgres addon →
DATABASE_URLis wired automatically. -
Set these env vars in the Railway dashboard:
Var How to generate JWT_SECRETpython -c "import secrets; print(secrets.token_urlsafe(48))"SERVICE_TOKEN_SECRETsame RTSP_FERNET_KEYpython -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"ALLOWED_ORIGINScomma-separated, e.g. https://app.sentry.chipmo.mn,https://admin.sentry.chipmo.mnENVIRONMENTproductionLOG_LEVELINFO -
Trigger a deploy. Container
CMDrunsalembic upgrade headthen starts uvicorn. -
Expose a public domain (Railway → Settings → Networking) and CNAME
api.sentry.chipmo.mnto it.
The repo intentionally ships no initial migration (see Migrations). Once Railway Postgres is reachable from your local machine:
# Get DATABASE_URL from Railway (Settings → Variables → DATABASE_URL)
export DATABASE_URL="postgresql+asyncpg://..."
uv run alembic revision --autogenerate -m "initial schema"
# Inspect alembic/versions/<id>_initial_schema.py, then commit + push.
# Railway's next deploy will run `alembic upgrade head` automatically.docker build -t sentry-backend:dev .
docker run --rm -p 8000:8000 --env-file .env sentry-backend:dev
curl http://localhost:8000/healthz- sentry-ai — AI inference (YOLO + VLM)
- sentry-ingest — Video receive
- sentry-frontend — Customer dashboard
- sentry-ui-kit — Shared design system
Platform overview: Sentry-v.3 README (local workspace)