Client ↓ FastAPI ↓ PostgreSQL ↓ Redis Queue ↓ Celery Worker ↓ Notification Processor (EmailProvider)
POST /api/v1/notifications- Creates notification record
- Enqueues Celery job
- Returns HTTP 202:
{
"success": true,
"message": "Notification queued",
"notification_id": "<uuid>"
}GET /api/v1/notifications/{id}- Returns status and persisted attempts
- Attempt #1: immediately
- Attempt #2: after 30 seconds
- Attempt #3: after 60 seconds
If all retries fail:
- Notification status becomes
FAILED - The final error is stored in
notification_attempts.
From repo root:
docker-compose -f backend/docker-compose.yml up --buildRun migrations:
docker-compose -f backend/docker-compose.yml exec backend alembic upgrade headThe Celery worker is started via docker-compose.
Logs will include:
- Notification Queued
- Notification Processing Started
- Retry Attempt
- Notification Sent
- Notification Failed
A GitHub Actions workflow runs automatically on every push and pull request to main or master.
Pipeline diagram:
Push / Pull Request
↓
GitHub Actions
↓
Ruff (lint)
↓
Black (format check)
↓
Pytest (unit & integration tests)
↓
Docker Build (build integrity)
↓
✅ Success
| Step | Tool | Purpose |
|---|---|---|
| Lint | Ruff | Enforce Python coding standards, detect unused imports, bug patterns |
| Format | Black | Ensure consistent code formatting across the entire codebase |
| Test | Pytest | Run all backend tests (async, auth, notifications, templates, tracing, metrics, etc.) |
| Build | Docker | Verify the Dockerfile builds successfully without errors |
- Ruff: Fastest Python linter. Replaces Flake8, isort, and pyflakes. Catches common bugs, unused imports, and style violations. Runs in CI as
ruff check .and fails on any lint error. - Black: Zero-configuration code formatter. Eliminates debates about code style. Runs in CI as
black --check .— it does not modify files; it only verifies formatting is correct. - Pytest: Standard Python test runner. Runs all tests in the
backend/tests/directory with async support. Fails the pipeline if any test fails. - Docker: Builds the
backend/Dockerfileto verify the image compiles correctly. This catches missing dependencies or syntax errors early, before deployment.
Developers should run these commands from the backend/ directory before pushing:
# Install development dependencies (includes Black, Ruff, Pytest)
pip install -r requirements-dev.txt
# Lint
ruff check .
# Format check
black --check .
# Run tests with coverage and JUnit report
pytest --junitxml=test-results.xml --cov=app --cov-report=term --cov-report=xml
# Build Docker image
docker build -f Dockerfile -t notifyhub:test .These commands match the CI pipeline exactly. Passing them locally guarantees the CI run will pass.
The CI workflow is defined in .github/workflows/ci.yml. It uses:
actions/checkout@v4— checkout repositoryactions/setup-python@v5— Python 3.13 with pip cachingactions/cache@v4— Docker layer caching for faster buildsdocker/setup-buildx-action@v3— BuildKit for efficient Docker builds
Jobs run sequentially:
lint— Ruff + Blacktest— Pytest with coverage + JUnitdocker— Docker build with Buildx cache
To add deployment later, create a new job:
deploy:
needs: docker
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: echo "Deploy step here"The sequential job chain (lint -> test -> docker -> deploy) keeps the pipeline modular and easy to extend without rewriting existing jobs.
Test coverage measures how much of the application source code (backend/app/) is exercised by the test suite. Coverage is generated automatically in CI and can be run locally.
From the backend/ directory:
pytest --cov=app --cov-report=term --cov-report=xmlThis generates two files:
coverage.xml— Machine-readable XML report (used by CI artifact uploads)test-results.xml— JUnit XML report (test pass/fail details)
The terminal output includes a per-module breakdown:
---------- coverage: platform linux, python 3.13.0-final-0 -----------
Name Stmts Miss Cover
------------------------------------------------------------
app/api/v1/routes_auth.py 25 0 100%
app/api/v1/routes_notifications.py 20 2 90%
app/core/config.py 10 0 100%
...
TOTAL 500 50 90%
In GitHub Actions, the pytest step runs with --cov=app --cov-report=term --cov-report=xml. The generated coverage.xml is uploaded as a build artifact for download after the pipeline completes.
| File | Format | Purpose |
|---|---|---|
coverage.xml |
XML (Cobertura) | Machine-readable coverage data for CI artifact storage |
test-results.xml |
XML (JUnit) | Test pass/fail details for CI artifact storage |
- Coverage thresholds: Fail the build if coverage drops below a configurable percentage.
- Codecov / Coveralls: Upload coverage reports to a hosted dashboard for historical tracking.
- HTML reports: Generate an interactive HTML report for detailed line-by-line coverage exploration.
- Diff coverage: Report coverage only on lines changed in a pull request.
Pre-commit hooks automatically run Ruff and Black before every git commit, catching formatting and linting issues before code reaches GitHub.
| Hook | ID | Behaviour |
|---|---|---|
| Ruff | ruff |
Lint check (ruff check --no-fix). Fails on lint errors but does not modify code |
| Black | black |
Auto-formats Python files to match CI expectation |
| End-of-file fixer | end-of-file-fixer |
Ensures every file ends with a trailing newline |
| Trailing whitespace | trailing-whitespace |
Removes trailing whitespace from lines |
| YAML validation | check-yaml |
Validates all YAML files (GitHub Actions workflows, Docker Compose, configs). Prevents broken configuration from being committed |
| Merge conflict detection | check-merge-conflict |
Scans for unresolved merge conflict markers (<<<<<<<, >>>>>>>, =======). Prevents accidental commits containing conflicts |
- YAML validation: Catches syntax errors in
.yml/.yamlfiles before they break CI pipelines or Docker Compose. A missing colon or incorrect indentation fails the commit immediately. - Merge conflict detection: After resolving a conflicted merge, it is easy to miss leftover conflict markers. This hook scans every file and blocks the commit if any unresolved markers remain.
cd backend
pip install -r requirements-dev.txt
pre-commit installAfter installation, hooks run automatically before every commit.
To run all hooks against all files without committing:
pre-commit run --all-filesTo run a specific hook:
pre-commit run ruff --all-files
pre-commit run black --all-filesThe pre-commit hooks use the exact same tools and versions as CI:
| Tool | CI command | Pre-commit command |
|---|---|---|
| Ruff | ruff check . |
ruff check --no-fix |
| Black | black --check . |
black (auto-fixes) |
The difference is that pre-commit auto-fixes formatting (Black) while CI only checks. This is intentional: pre-commit catches issues before commit, CI enforces after push.
In an emergency, skip pre-commit hooks:
git commit -m "urgent fix" --no-verifyThis should be rare — passing pre-commit locally guarantees the lint and format steps in CI will pass.
Traces flow through the system as follows:
Client
↓
FastAPI
↓
Redis
↓
PostgreSQL
↓
Celery
↓
Worker
↓
Provider
↓
OpenTelemetry
↓
Jaeger
Start all services including Jaeger:
docker-compose -f backend/docker-compose.yml up --buildThis starts:
- postgres: Database
- redis: Message broker for Celery
- jaeger: Distributed tracing backend (all-in-one)
- backend: FastAPI application
- worker: Celery worker
Run migrations:
docker-compose -f backend/docker-compose.yml exec backend alembic upgrade head