Backend Architecture¶
Overview¶
The backend is a FastAPI application providing email services and API endpoints.
Technology Stack¶
- Framework: FastAPI
- Language: Python 3.11
- ASGI Server: Uvicorn
- Validation: Pydantic v2 + pydantic-settings
- Email: SMTP (Gmail) via Jinja2-rendered HTML templates
- Rate Limiting: slowapi (in-memory, keyed by IP; single-replica only)
- Database ORM: SQLAlchemy 2 (async), asyncpg driver
- Migrations: Alembic
- Logging: loguru (structured JSON in production, pretty in dev)
Project Structure¶
The backend follows an ArjanCodes-inspired layered architecture (PRs #370, #375, #377, #379, #380, #381):
core → schemas → repositories → services → routers. main.py is now a
~150-line create_app() factory that registers routers, middleware, and exception handlers.
See Architecture Pattern below for the design rationale.
api/
├── main.py # ~150-line create_app() factory
├── feature_flags.py # Azure App Config feature flag cache (30 s TTL)
├── requirements.txt # Python dependencies
├── __init__.py
├── core/
│ ├── config.py # Settings (pydantic-settings); `settings` singleton imported everywhere
│ ├── exceptions.py # PortfolioError, NotFoundError, ConflictError, ServiceUnavailableError
│ │ # + register_exception_handlers()
│ ├── limiter.py # Shared slowapi Limiter instance (in-memory, single-replica)
│ ├── logging.py # configure_logging() — loguru + InterceptHandler for stdlib/uvicorn
│ ├── middleware.py # RequestIdMiddleware — reads/generates X-Request-Id header
│ └── openapi.py # custom_openapi() — x-uigen-id / x-uigen-view annotations
├── db/
│ ├── engine.py # Async engine + get_db() + get_optional_db()
│ └── models.py # Project, ProjectPhase, WorkEntry, SkillCategory, Skill,
│ # Award, Certification, UigenToken
├── routers/
│ ├── admin.py # Write endpoints (bearer-token protected), mounted at /api
│ ├── auth.py # POST /auth/login/aad, GET /auth/me — registered at /api/auth AND /auth
│ ├── awards.py # GET /api/awards
│ ├── certifications.py # GET /api/certifications
│ ├── contact.py # POST /api/contact
│ ├── cv.py # GET /api/cv/status, PATCH /api/cv/generate
│ ├── experience.py # GET /api/experience[/{index}]
│ ├── feature_flags.py # GET + PATCH /api/feature-flags
│ ├── health.py # GET /health (infra probe), GET /api/health
│ ├── projects.py # GET /api/projects[/{slug}]
│ ├── skills.py # GET /api/skills
│ ├── sync.py # POST /api/admin/sync-certifications
│ └── uigen.py # GET /api/uigen-token
├── repositories/
│ ├── awards.py # list_awards(db) — DB or seed fallback
│ ├── certifications.py # list_certifications(db) — DB or seed fallback
│ ├── experience.py # list_entries(db), get_by_id(db, id) — DB or seed fallback
│ ├── projects.py # list_cards(db), get_by_slug(db, slug) — selectinload(Project.phases)
│ └── skills.py # list_categories(db) — DB or seed fallback
├── schemas/
│ ├── experience.py # Create/update request schemas for experience
│ ├── projects.py # Create/update request schemas + ProjectPhaseCreate
│ ├── responses.py # 11 Pydantic v2 response schemas (from_attributes=True):
│ │ # ProjectCard, ProjectDetail, ProjectPhase, WorkEntry, Skill,
│ │ # SkillCategory, Award, Certification, ContactResponse,
│ │ # HealthResponse, FeatureFlagsResponse
│ └── skills.py # Create/update request schemas for skills
├── services/
│ ├── credentials_sync.py # Credly / Databricks / MS Learn fetch + upsert
│ ├── cv.py # generate_cv() via asyncio.create_subprocess_exec
│ ├── email.py # EmailService (Jinja2 + SMTP)
│ └── uigen_tokens.py # issue(), validate(), revoke() — SHA-256 hashed, DB-backed
├── templates/
│ ├── contact_admin.html # Jinja2 template — notification email to portfolio owner
│ └── contact_user.html # Jinja2 template — confirmation email to form submitter
├── jobs/
│ └── sync_certifications.py # ACA job entrypoint
└── data/
├── seed.json # Canonical static fallback for all content (projects, experience,
│ # skills, awards, certifications)
└── seed.py # lru_cache loader: load_projects(), load_work_entries(), etc.
alembic/
└── versions/
├── 0001_initial_schema.py
├── 0002_seed_initial_data.py
├── 0003_add_iac_platform_project.py
├── 0004_add_awards_certifications.py
├── 0005_project_phases_table.py # Replaces timeline_encoded ARRAY with child table
├── 0006_uigen_tokens_table.py # uigen_tokens table for DB-backed token store
├── 0007_add_metrics_counters.py # CV download + generation OTel counters
├── 0008_update_experience_wb_hai.py # Add W+B DevOps role, update Databricks to completed
├── 0009_split_albron_hai_update_wb_devops.py # Split Albron/HAI, expand W+B DevOps entry
├── 0010_update_wb_role_title.py
└── 0011_add_certification_sync_fields.py
API Endpoints¶
Health Check¶
Contact Form¶
CV Download¶
CV PDFs are copied into the container at build time (client/public/*.pdf → /app/public/). The lang query parameter selects English or Dutch. Defaults to english if omitted or unrecognised.
Projects¶
Experience¶
Skills¶
Awards and Certifications¶
Data for all groups is read from PostgreSQL via the SQLAlchemy async ORM (see Database section below). See API Endpoints for full request/response shapes.
Certification responses include source (credly/databricks/microsoft/manual), badgeImageUrl, issuedAt, and expiresAt for synced entries. Sort order: manual entries first (by sort_order), then synced by source group, each sorted by issued_at DESC.
Admin API¶
The admin write endpoints are protected by a bearer token enforced at the FastAPI router level via dependencies=[Depends(_require_admin)]. They share the same /api/ prefix as the read endpoints — there is no separate /api/admin/ prefix.
# Projects (admin)
POST /api/projects # create a project record
PUT /api/projects/{slug} # replace a project (full update)
PATCH /api/projects/{slug} # partial update a project
DELETE /api/projects/{slug} # delete a project
PATCH /api/projects/reorder # set sort order for all projects
# Experience (admin)
POST /api/experience # create a work entry
PUT /api/experience/{entry_id} # replace a work entry (full update)
PATCH /api/experience/{entry_id} # partial update a work entry
DELETE /api/experience/{entry_id} # delete a work entry
PATCH /api/experience/reorder # set sort order for all entries
# Skills (admin)
POST /api/skills/categories # create a skill category
PUT /api/skills/categories/{category_id} # replace a skill category
PATCH /api/skills/categories/{category_id} # partial update a skill category
DELETE /api/skills/categories/{category_id} # delete a skill category
POST /api/skills/categories/{category_id}/skills # add a skill to a category
PUT /api/skills/{skill_id} # replace a skill
PATCH /api/skills/{skill_id} # partial update a skill
DELETE /api/skills/{skill_id} # delete a skill
# All admin routes require: Authorization: Bearer <api-admin-token>
The admin token is stored in Azure Key Vault (api-admin-token secret) and injected as an environment variable via Bicep. These endpoints are used for seeding and updating database content without direct DB access.
The entry_id path parameter for experience is a UUID (matching the database primary key), not a 0-based index. The read endpoint GET /api/experience/{index} still uses a 0-based integer index.
Credential Sync¶
POST /api/admin/sync-certifications triggers a full credential sync from three sources:
| Source | Method | Auth |
|---|---|---|
| Credly | Public JSON API (credly.com/users/sven-relijveld/badges.json) |
None |
| Databricks | Playwright headless Chromium intercepts Accredible wallet API response | None (browser UA + request signing handled by browser) |
| Microsoft Learn | OAuth refresh token stored in Key Vault (mslearn-refresh-token) |
Entra delegated — see scripts/get_mslearn_token.py for one-time setup |
Sync runs automatically every Monday 06:00 UTC via an ACA scheduled job (dna-{env}-portfolio-we-job-cert-sync) that reuses the backend image with entrypoint python -m api.jobs.sync_certifications. Rows with sync_enabled=False are never overwritten (used for the three manual fallback entries).
UIGen Token¶
The AdminModal in the frontend calls this endpoint to get a scoped token, then opens https://admin.sven-relijveld.com/auth?token=<token>. The UIGen auth shim writes the token to sessionStorage["uigen_auth"] and redirects to UIGen root. Guest tokens expire after 1 hour.
Token storage is handled by api/services/uigen_tokens.py (issue(), validate(), revoke()). Tokens are stored as SHA-256 hashes in the uigen_tokens database table (migration 0006) when a DB is available, falling back to an in-process dict when the DB is unavailable (local dev / DB down). The DB-backed store survives container restarts and works correctly with multi-replica deployments; the in-process fallback is single-replica only.
Feature Flags¶
GET /api/feature-flags
# Response: {"cvGeneration": true/false, "portfolioContentApi": true/false, "heroRoleCycler": true/false, "projectPageV2": true/false}
PATCH /api/feature-flags
# Request: {"cvGeneration": true/false, "portfolioContentApi": true/false, "heroRoleCycler": true/false, "projectPageV2": true/false}
# Requires: Authorization: Bearer <api-admin-token>
# Response: updated flag state
GET reads current flag values from Azure App Configuration (dna-portfolio-we-appcs). The backend uses managed identity (App Configuration Data Reader RBAC) with a 30-second TTL cache. Labels are environment-specific (dev/prod). Falls back to the FEATURE_CV_GENERATION env var if App Config is unavailable.
PATCH writes flag updates back to Azure App Configuration via ff.set_flag(). The admin bearer token is required. Both flags are optional in the request body — only supplied fields are updated. The backend MI requires App Configuration Data Owner RBAC (not just Data Reader) on the App Configuration store for writes to succeed. The 30-second TTL cache is explicitly busted after a successful PATCH so the next GET reflects the updated values immediately.
| Flag | Key in App Config | Frontend effect |
|---|---|---|
cvGeneration |
cv-generation |
Shows CV generate button in CV download dialog |
portfolioContentApi |
portfolio-content-api |
Fetches projects/experience/skills from the API instead of static data |
heroRoleCycler |
hero-role-cycler |
Enables the hero section role cycler UI feature |
projectPageV2 |
project-page-v2 |
Gates project-page-v2 route (V2 project detail pages backed by Strapi CMS) |
CV Generation (feature-flagged)¶
The CV generation pipeline (cv/build.ts) uses Bun + pagedjs-cli to render a custom Nunjucks template into PDF. The PATCH method is used rather than GET because UIGen's view-hint classifier maps PATCH without a path parameter to an action button, which renders correctly as a generate button in the admin portal. Controlled by the cv-generation feature flag in Azure App Configuration (dev=on, prod=on as of v0.10.0).
Rate Limiting¶
All endpoints are rate-limited via slowapi (keyed by client IP). Limits: contact 5/min, CV download 10/min, read endpoints 60/min. Exceeding a limit returns 429 {"detail": "Rate limit exceeded"}.
Database¶
The backend uses Azure PostgreSQL Flexible Server (production) with SQLAlchemy 2 async and asyncpg as the driver. Alembic manages schema migrations.
Models¶
| Model | Table | Description |
|---|---|---|
Project |
projects |
Portfolio project cards and detail pages |
ProjectPhase |
project_phases |
Ordered timeline phases for a project (FK to projects, added in migration 0005) |
WorkEntry |
work_entries |
Professional experience timeline entries |
SkillCategory |
skill_categories |
Skill groups with icon names and color metadata |
Skill |
skills |
Individual skills, FK to skill_categories |
Award |
awards |
Awards and recognition entries |
Certification |
certifications |
Professional certifications with link, icon, and sync metadata (source, external_id, badge_image_url, issued_at, expires_at, last_synced_at, sync_enabled) |
UigenToken |
uigen_tokens |
SHA-256 hashed UIGen guest/admin tokens with expiry (added in migration 0006) |
All tables use UUID primary keys generated by the database (gen_random_uuid()). Skill has a cascade-delete foreign key to SkillCategory. ProjectPhase has a cascade-delete foreign key to Project. Project.phases is loaded via selectinload to avoid N+1 queries on project list endpoints.
Connection¶
api/db/engine.py creates an async SQLAlchemy engine via _make_engine(database_url, use_managed_identity):
USE_MANAGED_IDENTITY=true(Azure): usesManagedIdentityCredentialfromazure-identityto fetch a short-lived AAD token and passes it as the asyncpgpasswordcallable. TheMANAGED_IDENTITY_CLIENT_IDenv var (set by Bicep) is passed toManagedIdentityCredential(client_id=...)to target the specific user-assigned identity.USE_MANAGED_IDENTITY=false(local): uses a plain username/password fromDATABASE_URL(docker-compose postgres service).
Important: The DATABASE_URL username must be the MI's principal name (e.g. dna-prd-portfolio-we-id-be), not its client ID UUID. PostgreSQL AAD authentication matches on the principal name. The value is written to Key Vault by the deploy workflow using the MI's principal name.
Two async generator dependencies are provided for route handlers:
get_db()— raisesServiceUnavailableError(→ 503) if the engine is not configured. Used by write endpoints (admin) and any read endpoint with no static fallback.get_optional_db()— yieldsAsyncSession | None; returnsNonewhen the engine is not configured. Used by read endpoints that fall back toapi/data/seed.jsonviaapi/data/seed.py.
Migrations¶
alembic/env.py uses _make_engine() (not a bare create_async_engine) so Alembic uses the same AAD token logic as the app runtime when USE_MANAGED_IDENTITY=true.
Architecture Pattern¶
The backend uses an ArjanCodes-inspired layered architecture. The dependency flow is strictly one-directional:
core (config, exceptions, logging, middleware)
↓
schemas (Pydantic v2 request + response models)
↓
repositories (DB queries; fall back to seed.json when DB unavailable)
↓
services (EmailService, CvService, UigenTokenService)
↓
routers (thin handlers: parse → call repo/service → return schema)
Key design decisions:
- No generic
Repository[T]ABC — each aggregate has plainasync deffunctions in its own module. Generic bases only pay off with multiple backend implementations; we have one (PostgreSQL via SQLAlchemy). - No DI container — FastAPI's
Depends()covers every injection need. - Domain exceptions translate at the edge — repositories raise
NotFoundError,ConflictError, etc.;register_exception_handlers()inapi/core/exceptions.pymaps them to HTTP status codes. Handlers focus on the happy path only. - DB-or-fallback in repositories, not in handlers — each read repository function accepts
db: AsyncSession | None; whenNone, it returns data fromapi/data/seed.jsonviaseed.py. Pydantic v2from_attributes=Trueschemas validate both ORM rows and plain dicts without a special case at the router edge. main.pyis a thin factory —create_app()registers routers, middleware, and exception handlers; module-level side effects are limited toapp = create_app().
See docs/planning/plan-api-arjan-refactor.md (archived) for the full rationale and ArjanCodes reference map.
Structured Logging and Request IDs¶
Logging (Phase 6, PR #381)¶
api/core/logging.py provides configure_logging(), called once in create_app():
- Installs an
InterceptHandleron the root stdlib logger so that uvicorn, SQLAlchemy, and all third-party libraries funnel through loguru. - In production (
settings.environment != "development"): JSON sink to stdout — structured output suitable for Azure Monitor / Log Analytics. - In development: pretty sink with color and human-readable timestamps.
Request ID Middleware (Phase 6, PR #381)¶
api/core/middleware.py provides RequestIdMiddleware:
- On each incoming request: reads
X-Request-Idfrom the request headers; if absent, generates a new UUID v4. - Binds the request ID to a
contextvars.ContextVarso it is accessible anywhere in the call stack (repositories, services) without explicit threading. - Echoes the ID back in the response
X-Request-Idheader. - Loguru's context filter pulls the current request ID into every log line emitted during request handling, enabling full log correlation by request ID in Log Analytics.
Usage: clients that set X-Request-Id on outbound requests (e.g. the frontend, load
balancers) will see their ID round-tripped in the response. Without a client-supplied ID the
middleware generates one transparently.
Email Service¶
Features¶
- Dual Emails: Notification + Confirmation
- HTML Templates: Professional branded emails
- Error Handling: Graceful fallbacks
- Logging: Comprehensive error logging
Email Flow¶
SMTP Configuration¶
Security¶
Secrets Management¶
- Azure Key Vault: Production secrets
- Environment Variables: Development
- No Hardcoding: All secrets externalized
CORS Configuration¶
Docker Build¶
# Stage 1: Bun + cv/ workspace (publishpipe for on-demand CV generation)
FROM oven/bun:1-slim AS bun-stage
WORKDIR /app/cv
COPY cv/package.json cv/bun.lock* ./
RUN bun install --frozen-lockfile
# Stage 2: Python runtime
FROM python:3.11-slim
# System deps: gcc for C extensions, Chromium for pagedjs-cli and Playwright,
# plus Playwright's required system libraries, and Node.js for pagedjs-cli runtime
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
chromium \
chromium-common \
libnss3 \
libatk-bridge2.0-0 \
libdrm2 \
libxkbcommon0 \
libgbm1 \
libasound2 \
nodejs \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY api/requirements.txt .
# Install Python deps + Playwright Chromium at a fixed path accessible to appuser
ENV PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir -r requirements.txt && \
playwright install chromium && \
chmod -R 755 /opt/playwright-browsers
# Copy Bun binary and cv/ workspace from bun-stage
COPY --from=bun-stage /usr/local/bin/bun /usr/local/bin/bun
COPY --from=bun-stage /app/cv /app/cv
COPY cv/build.ts /app/cv/build.ts
COPY cv/content /app/cv/content
COPY cv/templates /app/cv/templates
COPY cv/assets /app/cv/assets
COPY api/ ./api/
# CV PDFs for /api/download-cv
COPY client/public/*.pdf ./public/
# Tell pagedjs-cli / puppeteer where the system Chromium is
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
RUN useradd -m -u 1001 appuser && chown -R appuser:appuser /app
USER appuser
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
# Skip Alembic when DATABASE_URL is not set (e.g. the cert-sync ACA job entrypoint)
CMD ["sh", "-c", "if [ -n \"$DATABASE_URL\" ]; then n=0; until alembic -c api/alembic.ini upgrade head; do n=$((n+1)); [ $n -ge 5 ] && echo 'Alembic failed after 5 attempts' && exit 1; echo \"Alembic attempt $n failed, retrying in 15s...\"; sleep 15; done; else echo 'DATABASE_URL not set — skipping Alembic migrations'; fi && uvicorn api.main:app --host 0.0.0.0 --port 8000 --workers 1"]
Performance¶
- Async/Await: Non-blocking I/O throughout (FastAPI + SQLAlchemy async + asyncpg)
- Connection Pooling: SQLAlchemy async engine manages the DB connection pool; efficient SMTP connections for email
- Monitoring: Application Insights integration
Error Handling¶
SMTPAuthenticationError is caught and logged but does not fail the request — the API always returns success: true so the contact form shows a success message regardless of email delivery. This prevents credential misconfigurations from breaking the UX.
Testing¶
Tests are split across two files and share a conftest.py that sets required environment variables before api.main is imported.
tests/conftest.py — sets EMAIL_ADDRESS and EMAIL_PASSWORD defaults so pydantic-settings can instantiate without real credentials.
tests/test_api_endpoints.py — uses FastAPI's TestClient with smtplib.SMTP mocked via unittest.mock.patch. A reset_rate_limiter autouse fixture clears slowapi state before each test. No running server is required.
| Test | What it asserts |
|---|---|
test_health |
GET /api/health → 200, status: healthy |
test_contact_valid |
POST /api/contact → 200, success: true |
test_contact_with_optional_company |
Company field accepted |
test_contact_invalid_email |
Invalid email → 422 |
test_contact_missing_* |
Missing required fields → 422 |
test_email_uses_real_credentials |
Non-placeholder creds → login() called |
test_email_demo_mode_skips_login |
Placeholder creds → login() not called |
test_email_auth_error_does_not_fail_request |
SMTPAuthenticationError → still success: true |
test_both_notification_and_confirmation_sent |
smtplib.SMTP constructed twice |
test_projects_returns_list |
GET /api/projects → list of 6 cards |
test_project_detail_valid |
GET /api/projects/genai-framework → correct detail |
test_project_detail_not_found |
Unknown slug → 404 |
test_experience_returns_list |
GET /api/experience → list of entries (count varies with seed data) |
test_experience_entry_valid |
GET /api/experience/0 → Witteveen+Bos entry |
test_experience_entry_not_found |
Out-of-range index → 404 |
test_skills_returns_list |
GET /api/skills → list of 5 categories |
test_contact_rate_limit |
6th POST /api/contact within a minute → 429 |
tests/test_cv_download.py — tests the /api/download-cv endpoint by patching CV_FILES to use a temporary PDF on disk.
| Test | What it asserts |
|---|---|
test_download_cv_english |
GET /api/download-cv → 200, application/pdf, correct filename |
test_download_cv_nederlands |
GET /api/download-cv?lang=nederlands → Dutch CV PDF |
test_download_cv_defaults_to_english_for_unknown_lang |
Unknown lang param falls back to English |
test_download_cv_returns_404_when_file_missing |
Missing file on disk → 404, CV file not found |
tests/test_cv_generation.py — tests the feature-flagged CV generation endpoints.
| Test | What it asserts |
|---|---|
test_cv_status_disabled |
GET /api/cv/status → {"enabled": false} when flag off |
test_cv_status_enabled |
GET /api/cv/status → {"enabled": true} when flag on |
test_cv_generate_disabled |
GET /api/cv/generate → 404 when flag off |
tests/test_cv_files_exist.py — validates that CV PDF files are present in the container's public/ directory.
| Test | What it asserts |
|---|---|
test_english_cv_file_exists |
public/CV_Sven_Relijveld_English.pdf is present |
test_nederlands_cv_file_exists |
public/CV_Sven_Relijveld_Nederlands.pdf is present |
Running locally:
CI: test.yml runs on PR (jobs: api, unit, typecheck, links); test-backend job in deploy-pr-preview.yml runs after each preview deploy.
See Also¶
Page history
| Field | Value |
|---|---|
| Last updated | 2026-07-03 |
Changelog
| Date | PRs | Summary |
|---|---|---|
| 2026-03-03 | #55 | Add frontmatter; fix SMTPAuthenticationError handling to show success:true (always returns success); add Testing section Incorrect error handling example that returned success:false for SMTPAuthenticationError |
| 2026-03-04 | #56 | Add GET /api/download-cv endpoint; update Docker Build snippet to include PDF COPY step |
| 2026-03-20 | #85 | Add projects, experience, skills endpoints and rate limiting; add slowapi to tech stack; expand testing table to 25 tests Inaccurate 'Caching: Response caching where applicable' from Performance section |
| 2026-03-22 | #89 | Add GET /api/health as the primary health route (proxied through nginx); GET /health retained for infra probes but excluded from OpenAPI schema; add dev server to OpenAPI servers block |
| 2026-04-12 | #181 | Add PostgreSQL database layer: SQLAlchemy async ORM, Alembic migrations, asyncpg driver; update endpoints to reflect DB-backed data; add Database section Inaccurate statement that projects/experience/skills data is hard-coded in main.py |
| 2026-04-13 | #183 | Fix Docker CMD snippet to include alembic migration step and non-root user; add test_cv_download.py and conftest.py to Testing section; update pytest run command |
| 2026-04-17 | Add Admin API section (POST /api/admin/*); add CV Generation section (feature-flagged endpoints); add test_cv_generation.py and test_cv_files_exist.py to Testing table; update CI reference from test-api.yml to test.yml | |
| 2026-04-21 | #206, #207, #247, #243, #253, #254 | Add UIGen token endpoint, feature-flags endpoint, Feature Flags section (Azure App Config); update CV Generation to reflect App Config backing; update Database connection section with MANAGED_IDENTITY_CLIENT_ID and Alembic token fix; update project structure for api/routers/ and api/feature_flags.py Inaccurate statement that CV_GENERATION_ENABLED is a direct env var (now App Config feature flag) |
| 2026-04-26 | #265, #267, #269, #270, #273, #274, #277, #299, #300, #304 | Update Database connection section: replace IMDS urllib with azure-identity ManagedIdentityCredential; fix Postgres username to MI principal name (not UUID); update Docker CMD to show Alembic retry; add API annotations note to UIGen Token section; add feature-flags retry note; update Technology Stack zod version Inaccurate description of IMDS token fetch using raw urllib (now uses azure-identity) |
| 2026-05-03 | #358, #362 | Remove /admin/ prefix from all admin write endpoints (now /api/projects, /api/experience, /api/skills/ — bearer token still enforced); add awards and certifications read endpoints; add portfolioContentApi to feature-flags; add PATCH /api/feature-flags admin endpoint; update DB models table to include Award, Certification, Skill; update project structure; update CV Generation method from GET to PATCH _Stale /api/admin/ prefix on admin endpoints; GET /api/cv/generate replaced by PATCH /api/cv/generate_ |
| 2026-05-04 | #370 | Update Project Structure section to reflect Phase 1 refactor: api/core/ (config, openapi, exception/logging placeholders), api/templates/ (Jinja2 email templates), api/repositories/, api/services/, api/data/ skeleton packages Flat project structure listing that did not show core/, templates/, repositories/, services/, data/ packages |
| 2026-05-05 | #375, #377, #379, #380, #381 | Full Arjan refactor Phases 2–6: Pydantic v2 response schemas (Phase 2); per-domain routers, domain exception hierarchy, shared Limiter (Phase 3); repository + service layers, EmailService, async CV gen, get_optional_db (Phase 4); seed.json single source of truth, ProjectPhase child table, migration 0005 (Phase 5); loguru structured logging, RequestIdMiddleware, DB-backed UIGen token store with SHA-256 hashing, migration 0006 (Phase 6) Stale Phase 1 project structure showing skeleton placeholders; static models table missing ProjectPhase and UigenToken |
| 2026-05-19 | #438, #439, #455, #456 | Add auth router registration note (root + /api/auth); update feature-flags section: App Configuration Data Owner for writes, TTL cache bust after PATCH; update Alembic migrations table to include 0008 and 0009; update experience test count |
| 2026-06-02 | #485 | Add projectPageV2 feature flag to Feature Flags section and endpoint response example |
| 2026-07-03 | #606 | Add certification sync: credentials_sync.py service, sync.py admin router, jobs/sync_certifications.py ACA job entrypoint, migration 0011 columns; update Certification model description, API endpoint docs, Docker build section; add Credential Sync section |