Scaffold: just new <module> now auto-registers the new
module's router in app/api.py, eliminating the manual wiring
step.
Supply chain: Dependabot keeps dependencies patched with weekly,
grouped pull requests. .github/dependabot.yml watches two
ecosystems — uv (Python dependencies in pyproject.toml +
uv.lock) and github-actions (the actions pinned in
.github/workflows/) — collapsing minor and patch bumps into a
single PR per ecosystem while keeping majors separate. The new
Dependency Scanning guide documents the cadence, how to enable
GitHub-native secret scanning and push protection (repository
settings, not files), and how enterprises layer Snyk / Black Duck /
GHAS on top.
Migrations: a zero-downtime migration playbook. The Database
Migrations guide documents the expand/contract (parallel-change)
pattern with recipes for renaming a column, dropping a column,
changing a type, adding a NOT NULL column, and building an index
concurrently. just migrate-gen now runs a non-blocking guard
(scripts/migration_guard.py) that parses the generated script's
AST and flags destructive or locking operations — drops, type
changes, NOT NULL on populated tables, non-concurrent indexes, and
destructive raw SQL — for review.
Integrations: a shared, resilient outbound HTTP client
(app.http). A single httpx.AsyncClient is lifecycle-managed in
the lifespan and injected via HTTPClientDep. Every call is guarded
by a per-host circuit breaker (purgatory) wrapping a retry loop with
exponential backoff (stamina), and is OpenTelemetry-instrumented
when QUOIN_OTEL_ENABLED. Transport failures map to
BadGatewayError (502), GatewayTimeoutError (504), and
ServiceUnavailableError (503, circuit open); response status codes
are left for callers to interpret. Tunable via
QUOIN_HTTP_TIMEOUT_SECONDS and QUOIN_HTTP_RETRY_ATTEMPTS. See
the Outbound HTTP Client guide.
Reliability: graceful shutdown drains in-flight requests before
the database engine is disposed. On shutdown the readiness probe
flips to 503 (so orchestrators stop routing new traffic),
InFlightRequestMiddleware tracks active requests, and the lifespan
handler waits for them to drain — bounded by
QUOIN_SHUTDOWN_DRAIN_TIMEOUT (default 30s; <=0 skips the wait)
— before disposing the engine. See the Graceful Shutdown section in
the deployment guide for the uvicorn relationship and Kubernetes
wiring.
Security: SecurityHeadersMiddleware emits HSTS, CSP,
X-Frame-Options, X-Content-Type-Options, Referrer-Policy, and
Permissions-Policy on every response. All values configurable via
QUOIN_SECURITY_* settings; toggle with
QUOIN_SECURITY_HEADERS_ENABLED.
Security: RequestSizeLimitMiddleware rejects oversize bodies
before they reach route handlers by checking the advertised
Content-Length. Returns 413 RFC 9457 payload_too_large.
Configurable via QUOIN_MAX_REQUEST_BODY_BYTES (default 1 MiB;
<=0 disables). Conforming clients always send Content-Length;
the uvicorn/h11 layer caps raw protocol buffers for the chunked
edge case.
Branding: project tagline updated to "The Foundation for your
Python backend API" across README, pyproject.toml, OpenAPI metadata,
docs, and Copier template defaults.
Security: CORS configuration now requires explicit allowlists
for methods and headers (QUOIN_BACKEND_CORS_ALLOW_METHODS,
QUOIN_BACKEND_CORS_ALLOW_HEADERS,
QUOIN_BACKEND_CORS_ALLOW_CREDENTIALS). Wildcard methods/headers
combined with allow_credentials=True outside development are
rejected at startup — that combination is silently ignored by
browsers and was a credentialed-CORS footgun.
Tooling: uv resolution is now bounded by a 7-day dependency
cooldown (exclude-newer = "7 days") for more reproducible
installs, and required-version pins the minimum uv version so
contributors and CI stay in sync. Ruff now lints naming conventions
(N / pep8-naming) and formats code samples embedded in docstrings
(docstring-code-format). pyproject.toml's dependency lists and
tool sections are sorted alphabetically for easier scanning and
fewer merge conflicts.
Developer Experience: Claude Code workflow improvements distilled
from recurring manual chores — three new skills (quoin-coverage
for the drive-to-100% coverage loop, quoin-deps-upgrade for the
dependency and GitHub Actions upgrade ritual, quoin-docs-audit for
docs-to-code drift sweeps), a migration-reviewer subagent that
audits autogenerated Alembic scripts, two advisory Stop hooks
(config-drift when app/core/config.py changes without matching
.env.example / docs/guides/configuration.md updates, and
migration-reminder when a models.py changes without a new
migration), a read-only postgres MCP server for live schema
introspection, a just sync-main recipe for post-merge branch
cleanup, and just test/just check now auto-start Postgres
instead of failing. quoin-pre-pr now gates on just check
passing at 100% coverage. .env.example now documents the
QUOIN_ALLOWED_HOSTS setting.
Template: copier copy now produces a clean, de-branded
starter. QuoinAPI-specific docs are excluded at copy time (the
marketing docs.md, the architecture decision log, the roadmap,
and the custom home-page JavaScript), and the post-generation
script rewrites the remaining chrome — a fresh README.md and
minimal docs/index.md, a trimmed documentation nav with the
personal social links removed, and the error-type URN namespace
rebranded from urn:quoin:error:* to urn:<project-slug>:error:*.
The guides, architecture overview, and full API reference (including
the user module) are retained.
Errors: unhandled exceptions (bare KeyError, non-transport
httpx errors, etc.) now return RFC 9457 application/problem+json
instead of Starlette's default text/plain 500. A catch-all
Exception handler logs the traceback and maps to
InternalServerError.
Template: scoped the Copier post-generation substitutions by
filename so they can no longer corrupt unrelated files. The author
email rewrite was previously applied to every file and overwrote
email = "..." values in test fixtures, breaking a freshly
generated project's test suite; the APP_DESCRIPTION rewrite
matched only a parenthesised form the source never used, so the
default API description leaked into generated projects. Both now
target their intended file.
Observability: RequestIDMiddleware propagates X-Request-ID
(configurable via QUOIN_REQUEST_ID_HEADER) and binds it to every
structlog event.
Observability: OpenTelemetry trace/log correlation — trace_id and
span_id injected into structlog events when an active span exists.
Vendor-neutral OTLP/Jaeger setup documented.
Operability: TimeoutMiddleware enforces a per-request wall-clock
timeout via anyio.fail_after(); configurable via
QUOIN_REQUEST_TIMEOUT_SECONDS (default 30 s); returns 504 RFC 9457
GatewayTimeoutError. Uses anyio cancel scopes for nested-task safety.
Errors: RFC 9457 Problem Details — all error responses use
application/problem+json with type, title, status, detail,
instance, and an errors array on 422. ProblemDetail model in
app/core/schemas.py replaces ErrorResponse.
Errors: GatewayTimeoutError (504) and ServiceUnavailableError
(503) domain exceptions; /ready now raises the latter on DB failure.
Developer Experience: Claude Code workflow integration — 6 skills in
.claude/skills/, Stop hook running just format && just lint && just
typecheck after dirty turns, PreToolUse hook blocking edits to .env,
uv.lock, and applied migrations, 5 plugins, and context7 MCP server.
Quality: Pre-push pytest gate in prek.toml; just setup installs
both commit and pre-push hooks.
Security: Full OAuth 2.0/2.1 S2S authentication stack — JWKSCache
(JWKS rotation), validate_token, get_current_caller, and require_roles
dependency factory in app/core/security.py.
Security: ServicePrincipal Pydantic model (subject, roles,
claims) as the resolved caller identity; api.superuser bypass for local
dev; UnauthorizedError (401) with RFC 6750 WWW-Authenticate: Bearer.
Security: DDD role scopes — [domain].[action] strings (e.g.
users.read, users.write) declared explicitly per route, no global roles.
OpenAPI: ErrorResponse schema in app/core/schemas.py; all users/
endpoints fully document 401, 403, 404, 409, 500 responses.
Configuration: QUOIN_OAUTH_* settings for binding to any OIDC
provider; mock-oauth2-server Docker service + just token --roles <roles>
for local RS256 JWT generation.
Developer Experience: just dev — starts DB (with healthcheck), applies
migrations, and runs the server in one command. just reset-db purges
volumes with docker compose down -v for a clean slate.
Testing: Dual-layer auth — live tokens via mock-oauth2-server;
ServicePrincipal fixture injection via dependency_overrides for
zero-container unit tests. DB isolation fix prevents test teardown from
wiping the dev app_db schema.
Copier: Added copier.yml and scripts/copier_setup.py.jinja.
Documentation: docs/guides/authentication.md covering DDD scopes,
api.superuser bypass, dependency graph, and both testing layers.