User resource

Single entry point for the User HTTP API: product and ownership context, data contract shorthand, links to per-method pages, and the full internal narrative (runtime path, idempotency, errors, logging, metrics). Authoritative HTTP contract for integrators: OpenAPI.

Business context

A User represents a learner identity in Study App: keyed by (system_user_id, system_uuid) for partner integrations, with profile attributes (display name, locale, notification preferences, invalidation metadata). Creating and updating users is on the critical path for onboarding; reads power dashboards and entitlement checks.

The Product API squad owns domain invariants (e.g. uniqueness within a tenant, soft-delete semantics); the Platform squad owns transport concerns (authn/authz at the edge, rate limits, observability baselines). This split is reflected in error codes and logs, not in duplicated prose on every method page.

Data contract (summary)

Full JSON Schemas live in OpenAPI; this table is the engineering shorthand for reviews and runbooks.

Field / group Role Notes
system_user_id, system_uuid Composite key Path parameters for instance-scoped reads and writes.
Profile block Mutable projection PUT/PATCH semantics documented per method; optimistic concurrency where applicable.
Audit / invalidation System metadata Not always exposed in external integrator docs; internal spec describes logging and metrics hooks.

Method pages (per endpoint)

Navigation is keyed by HTTP method + path (how clients call the API). OpenAPI operationId values are listed for cross-reference with code and the baseline JSON.

Use the left sidebar under API → User, or open a page from this table. Each method page links back to anchors on this document for idempotency, metrics, and the matching operation block.

Method & path operationId (ref.) Page
POST /api/v1/user createUser Internal doc
GET /api/v1/user/{system_uuid}/{system_user_id} getUserBySystemUserId Internal doc
PUT /api/v1/user/{system_uuid}/{system_user_id} updateUserBySystemUserId Internal doc
PATCH /api/v1/user/{system_uuid}/{system_user_id} patchUserBySystemUserId Internal doc

Technical overview

The User feature exposes four operations under the versioned prefix /api/v1: create a user, fetch a user by composite key, full update (PUT), and partial update (PATCH). All traffic is served by the FastAPI app in app/main.py; handlers live in app/api/v1/user.py, domain logic in app/services/user_service.py, and persistence in app/repositories/user_repository.py backed by PostgreSQL.

There is no Kafka topic, message consumer, or third-party identity SaaS in this code path: integration is synchronous HTTP plus the application database only.

Ownership and change management

Who changes this feature: any contributor following CONTRIBUTING.md — pull requests on main, review, and green CI. There is no separate product ticket system encoded in the repo; git history and PR descriptions are the record of who changed behavior.

  • Quality gate: make verify-ci before push (lint, types, OpenAPI, contract tests, tests, docs-check) per CONTRIBUTING.
  • Contract: intentional HTTP contract changes require updating application code and accepting the OpenAPI baseline (make openapi-accept-changes) so docs/openapi/openapi-baseline.json matches runtime (ADR 0007).
  • Release notes: user-visible behavior changes belong in root CHANGELOG.md (ADR 0013).
  • Internal narrative: update this HTML file in the same change when you alter business semantics, logging, or idempotency paths (ADR 0026).

API surface

Base path: /api/v1/user (constant USER_HTTP_BASE_PATH in app/api/v1/user.py). Protected routes require X-API-Key and are subject to rate limiting per ADR 0005 (enforced in middleware, not in the handler body).

POST /api/v1/user

operationId
createUser
Idempotency
Required Idempotency-Key. Printable ASCII only; length 1–128.
Success
201 Created. Body: created user (includes client_uuid).

GET /api/v1/user/{system_uuid}/{system_user_id}

operationId
getUserBySystemUserId
Idempotency
Not used.
Success
200 OK.

PUT /api/v1/user/{system_uuid}/{system_user_id}

operationId
updateUserBySystemUserId
Idempotency
Required Idempotency-Key (same rules as POST).
Success
200 OK.

PATCH /api/v1/user/{system_uuid}/{system_user_id}

operationId
patchUserBySystemUserId
Idempotency
Required Idempotency-Key (same rules as POST).
Success
200 OK.

Write operations hash the canonical JSON body (create: full dump; update: full dump; patch: only set fields via exclude_unset=True) for idempotency comparison — see Idempotency.

Request path and layers

A typical request passes through the middleware stack in app/main.py (order of registration: metrics/latency wrapper, body size limit, API key auth + in-memory rate limit, security headers, request ID context), then the route handler, then:

  1. Router (app/api/v1/user.py) — parsing, idempotency read/replay or delegate, OpenAPI-declared responses.
  2. UserService (app/services/user_service.py) — business rules, duplicate detection, HTTPException for domain outcomes.
  3. UserRepository (app/repositories/user_repository.py) — SQLAlchemy SELECT / commit+refresh for users table.
  4. IdempotencyRepository (app/repositories/idempotency_repository.py) — read/write idempotency_keys rows for successful writes only.

Data model and invariants

  • Natural key: (system_user_id, system_uuid) — unique in DB (uq_users_system_user_id_system_uuid on users).
  • Primary key: client_uuid (string UUID) — generated server-side on create.
  • Mutable fields include profile fields (username, full_name, timezone, invalidation flags, optional system_uuid change on update/patch per schema). Validation of shapes and timezones is at the API schema layer (app/schemas/user.py) before service calls.

Idempotency

Writes require Idempotency-Key. The handler computes a SHA-256 hash of the JSON payload (sorted keys in storage) and looks up (endpoint_path, idempotency_key):

  • POST create: endpoint_path = "/api/v1/user".
  • PUT update: endpoint_path = "/api/v1/user/{system_uuid}/{system_user_id}" (literal path with UUID string).
  • PATCH: endpoint_path = "PATCH /api/v1/user/{system_uuid}/{system_user_id}" — note the PATCH prefix so PUT and PATCH on the same URL do not share idempotency records.

If a record exists and the hash matches, the stored response body is replayed (same status code as first success). Hash mismatch → 409 with COMMON_409 / IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_PAYLOAD. Missing header → 400 COMMON_400 / IDEMPOTENCY_KEY_REQUIRED.

Errors and HTTP status mapping

Structured business errors use the shared envelope (code, key, message, source) per ADR 0003. FastAPI returns them as JSON detail.

HTTP Code / key (representative) When
400 USER_101 / USER_CREATE_ALREADY_EXISTS Create: duplicate (system_user_id, system_uuid) — logged at warning in UserService.create.
400 USER_102 / USER_PATCH_BODY_EMPTY Patch: no fields present after unset stripping — warning in UserService.patch.
400 COMMON_400 / IDEMPOTENCY_KEY_REQUIRED Write without Idempotency-Key.
404 USER_404 / USER_NOT_FOUND Get / update / patch: no row for composite key — warning in UserService.get_or_404.
409 COMMON_409 / IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_PAYLOAD Same idempotency key, different body hash.
422 Validation (error_type / field errors) Pydantic request validation — global handler logs validation_error method=… path=… at warning (app/main.py).
413 COMMON_413 / body too large Middleware rejects oversized body before the handler runs — warning request_body_limit_exceeded.
500 Uncaught exceptions: ASGI middleware logs request_failed with exception stack (logger.exception).

Auth failures (401) and rate limits (429) are produced by security middleware before route execution; see ADR 0005 and app/core/security.py.

Logging

Logging is configured in app/core/logging.py: plain text or NDJSON with request_id from X-Request-Id (set in request_context_middleware) per ADR 0023.

Every completed request emits an info line from the outer HTTP middleware: request_done method=… path=… status=… elapsed_ms=… (path is the request URL path; successful User calls include status=200 or 201 as appropriate).

User router (app.api.v1.user) additionally logs:

  • create_user_idempotent_replay, update_user_idempotent_replay, patch_user_idempotent_replayinfo, idempotency key only.
  • create_user_requested, update_user_requested, patch_user_requestedinfo, system_user_id and system_uuid.
  • create_user_succeededinfo, system_user_id, client_uuid after persist.

UserService logs create_user_duplicate, get_user_not_found, patch_user_empty_body at warning before raising; create_user_persisted at info after insert.

Metrics and tracing hooks

Prometheus metrics (app/core/metrics.py) record:

  • http_requests_total and http_request_duration_seconds with labels including path_template from the FastAPI route template (e.g. /api/v1/user/{system_uuid}/{system_user_id}), method, and status.
  • db_operation_duration_seconds for SQLAlchemy operations (read/write classification from statement text).

trace_id / span_id in JSON logs are reserved for future OpenTelemetry; correlation today is primarily request_id.

External and in-process dependencies

Dependency Role for User API
PostgreSQL Authoritative storage for users and idempotency_keys (via SQLAlchemy).
In-memory rate limiter Per-client sliding window on protected paths (app/core/security.py); not a separate network service.
Configured API keys Validated in middleware against settings — no call to an external IdP in this flow.

Code, tests, and policy references

  • Handlers: app/api/v1/user.py
  • Service: app/services/user_service.py
  • Repositories: app/repositories/user_repository.py, app/repositories/idempotency_repository.py
  • ORM model: app/models/core/user.py (users table)
  • Tests: tests/api/v1/test_user_create.py, helpers in tests/api/v1/user_test_utils.py
  • Policies: ADR 0006 (idempotency), ADR 0003 (errors)

Page history

Date Change Author
Renamed section to Page history; added Author column (style guide alignment). Ivan Boyarkin
Initial internal capability doc: operations, layers, idempotency keys, logging, metrics, dependencies.
File moved from docs/internal/user-http-api.html to docs/internal/api/user/user-http-api.html (resource-scoped layout).
Unified long-form content merged into docs/internal/api/user/index.html; user-http-api.html retained as redirect stub for bookmarks.

← Internal service documentation