Plan: API-Backed Content — All Data Objects in the Database¶
Archived: fully implemented as of 2026-05-05. All endpoints, frontend hooks, feature flag infra, and DB models complete (PRs #178, #181, #184, #358, #362). Only Phase 8 (flip portfolioContentApi prod flag) remains, tracked separately.
Status: Complete — all phases done except Phase 8 (prod flag flip, deferred)
Scope: Replace all hardcoded client/src/data/*.ts arrays with data served from the backend API + stored in PostgreSQL
Goal¶
Move all portfolio content out of the frontend bundle into the database. Content can then be updated without a code deploy, and UIGen provides an admin CRUD interface automatically.
The migration is feature-flagged: the existing static data files remain as the fallback while the flag is off. When the flag (portfolio-content-api) is enabled, the frontend fetches from the API instead. This allows a gradual rollout and instant rollback without a redeploy.
What Already Exists (as of 2026-05-03)¶
DB models — all present in api/db/models.py¶
Project— full schema with slug, category, phase, timeline, all detail fieldsWorkEntry— full schema with responsibilities, technologies, sort_orderSkillCategory+Skill— with relationship, sort_order
Missing models: Award, Certification
Alembic migrations — api/alembic/versions/¶
0001_initial_schema.py— projects + work_entries + skill tables created0002_seed_initial_data.py— seed data for projects, work_entries, skills0003_add_iac_platform_project.py— additional project added
Missing migrations: awards + certifications tables
Backend endpoints — already in api/main.py¶
GET /api/projects✅GET /api/projects/{id}✅GET /api/experience✅GET /api/experience/{id}✅ (bonus, not in plan)GET /api/skills✅
Missing endpoints: GET /api/experience/preview, GET /api/awards, GET /api/certifications
Note: Awards and certifications exist as static AWARDS / CERTIFICATIONS lists in main.py and are used by the CV generation context only — no public endpoints.
Frontend feature flags¶
FeatureFlagsContextonly hascvGeneration: bool—portfolioContentApinot yet addeduseProjects.tshook does not exist atclient/src/data/hooks/(onlyclient/src/data/useContactForm.ts)
Infra / App Config¶
portfolio-content-apiflag not yet ininfra/modules/app_configuration.bicep
Current State — Data Objects¶
| File | Exported constant | DB model | API endpoint | Frontend hook |
|---|---|---|---|---|
data/experience.ts |
workHistory[] |
WorkEntry ✅ |
GET /api/experience ✅ |
❌ |
data/experience.ts |
sectionExperiences[] |
derived | GET /api/experience/preview ❌ |
❌ |
data/skills.ts |
skillCategories[] |
SkillCategory+Skill ✅ |
GET /api/skills ✅ |
❌ |
data/projects.ts |
projectCards[] |
Project ✅ |
GET /api/projects ✅ |
❌ |
data/projects.ts |
projectDetails{} |
Project (detail fields) ✅ |
GET /api/projects/{id} ✅ |
❌ |
data/awards.ts |
awards[] |
❌ | ❌ | ❌ |
data/certifications.ts |
certifications[] |
❌ | ❌ | ❌ |
Feature Flag Gate¶
A new flag portfolio-content-api is added to Azure App Configuration with labels dev and prod.
- dev:
on— always fetch from API in dev - prod:
offinitially — flip toononce all endpoints are live and seeded
The frontend FeatureFlagsContext already exposes flags to all components. Each hook checks this flag:
function useWorkHistory(): WorkEntry[] {
const { portfolioContentApi } = useFeatureFlags();
const [data, setData] = useState<WorkEntry[]>(workHistory); // static fallback always loads first
useEffect(() => {
if (!portfolioContentApi) return; // flag off → keep static data
fetch('/api/experience')
.then(r => r.json())
.then(setData)
.catch(() => {}); // flag on but API fails → keep static data
}, [portfolioContentApi]);
return data;
}
TanStack Query (@tanstack/react-query v5) is present — use useQuery for loading/error/stale handling instead of raw fetch + useState.
Target Architecture¶
Azure App Configuration
portfolio-content-api: dev=on, prod=off (flip when ready)
Frontend (React)
FeatureFlagsContext.portfolioContentApi
true → GET /api/experience → work_entries table
→ GET /api/experience/preview → work_entries WHERE is_current OR top 4 by sort_order
→ GET /api/skills → skill_categories + skills tables
→ GET /api/projects → projects table
→ GET /api/projects/{id} → project_details (columns on Project model)
→ GET /api/awards → awards table
→ GET /api/certifications → certifications table
false → existing static data/*.ts files (unchanged)
Remaining Work¶
Phase 1 — Infra: App Config feature flag¶
infra/modules/app_configuration.bicep:
- Add
portfolio-content-apiflag (dev=on, prod=off) - Add
content_api_prod_enabled bool = falseparam (same pattern ascv_generation_prod_enabled) - Add fallback env var
FEATURE_PORTFOLIO_CONTENT_APItofeature_flags.py_FALLBACK_ENV_VARS
Phase 2 — Backend: Awards + Certifications models + migration¶
api/db/models.py — add Award + Certification models (UUID PK, sort_order, all fields matching frontend types including icon_name/bg_color for Award and icon_type/link/link_text for Certification).
New migration 0004_add_awards_certifications.py:
- Create
awards+certificationstables - Seed from current static
AWARDS+CERTIFICATIONSlists inmain.py
Phase 3 — Backend: Missing endpoints¶
In api/main.py (or extract to api/routers/portfolio.py):
Add Award + Certification Pydantic response models with all frontend fields (including iconName/bgColor and iconType/link/linkText using camelCase to match frontend).
Update FeatureFlagsResponse + get_feature_flags() to include portfolioContentApi.
Update feature_flags.py _FALLBACK_ENV_VARS to map "portfolio-content-api" → "FEATURE_PORTFOLIO_CONTENT_API".
Phase 4 — Backend: Tests¶
New pytest tests in tests/test_portfolio_endpoints.py:
GET /api/experience— returns list, fields presentGET /api/experience/preview— max 4 itemsGET /api/skills— nested categories with skillsGET /api/projects— returns listGET /api/projects/{slug}— returns detailGET /api/awards— returns list with iconName + bgColorGET /api/certifications— returns list with iconType
All tests use FastAPI TestClient, no live DB (endpoints fall back gracefully when DATABASE_URL unset — verify this is true for new endpoints too).
Phase 5 — Frontend: Feature flag + hooks¶
client/src/contexts/feature-flags-context.tsx:
- Add
portfolioContentApi: booleantoFeatureFlagsinterface - Add to
defaultFlags - Update
data: FeatureFlagstype assertion
client/src/hooks/usePortfolioContent.ts (new file):
// One hook per data object using useQuery when flag is on, static data otherwise
useWorkHistory() → WorkEntry[]
useExperiencePreview() → SectionExperience[]
useSkillCategories() → SkillCategory[]
useProjects() → ProjectCard[]
useProject(id: string) → ProjectDetail | undefined
useAwards() → Award[]
useCertifications() → Certification[]
Use useQuery from @tanstack/react-query with placeholderData set to the static array — this gives instant render from static data and background update from API.
Phase 6 — Frontend: Update consuming components¶
| Component | Replaces | With |
|---|---|---|
pages/experience.tsx |
import { workHistory } |
useWorkHistory() |
experience-section.tsx |
import { sectionExperiences } |
useExperiencePreview() |
skills-section.tsx |
import { skillCategories } |
useSkillCategories() |
projects-section.tsx |
import { projectCards } |
useProjects() |
| project detail component | projectDetails[id] |
useProject(id) |
pages/about.tsx |
awards, certifications |
useAwards(), useCertifications() |
Find exact component paths before editing.
Phase 7 — OpenAPI type sync + verification¶
- Run pre-commit hook to regenerate
shared/api.types.ts - Run
npm run typecheck— should pass with no TS errors - Start backend + frontend locally; enable
FEATURE_PORTFOLIO_CONTENT_API=true; verify all sections load from API
Phase 8 — Flip prod flag (follow-up, after verification)¶
- Set
content_api_prod_enabled = trueininfra/parameters.prod.bicepparam - Deploy infra to prod
- Smoke test prod; if green, static fallback can be removed in subsequent PR
Tailwind Safelist Note¶
Icon names (Trophy, Medal, Users, Waves) and bg colors (bg-blue-600, bg-green-600, bg-purple-600, bg-indigo-600) are stored as strings in the DB. These must be safelisted in tailwind.config.ts so PurgeCSS doesn't strip them.
UIGen Integration¶
Once awards + certifications tables exist, UIGen auto-generates CRUD UI for all 7 tables. Editing content becomes point-and-click with no code deploy.
Phase Status¶
| Phase | Scope | Status |
|---|---|---|
| ~~1~~ | ~~Infra: App Config flag~~ | ⬜ Remaining |
| ~~2~~ | Backend: DB models + migrations for projects/work/skills | ✅ Done (PRs #178–184) |
| 2b | Backend: Award + Certification models + migration 0004 |
⬜ Remaining |
| ~~3~~ | Backend: projects/experience/skills endpoints | ✅ Done |
| 3b | Backend: /api/experience/preview, /api/awards, /api/certifications |
⬜ Remaining |
| 3c | Backend: portfolioContentApi flag in feature-flags response |
⬜ Remaining |
| 4 | Backend: pytest tests for all portfolio endpoints | ⬜ Remaining |
| 5 | Frontend: FeatureFlagsContext + usePortfolioContent.ts hooks |
⬜ Remaining |
| 6 | Frontend: update consuming components | ⬜ Remaining |
| 7 | OpenAPI type sync + local E2E verification | ⬜ Remaining |
| 8 | Flip prod flag + remove static fallback (follow-up PR) | ⬜ Later |