Certification Sync — Background ACA Job¶
Automatically pull certifications from Credly, Databricks (Accredible), and Microsoft Learn into the portfolio database, replacing the current static fallback entries with live badge data.
Goals¶
- Remove manual maintenance of the certifications list
- Show badge images, issue dates, and expiry dates on the frontend
- Trigger a resync on demand from an admin endpoint
- Run automatically on a schedule via an Azure Container Apps job
Source feasibility¶
| Source | Method | Auth required | Data quality |
|---|---|---|---|
| Credly | Public JSON API — credly.com/users/sven-relijveld/badges.json |
None | Full: name, issued/expiry dates, badge image URL, issuer |
| Databricks | Accredible-hosted wallet page (credentials.databricks.com) |
None (page is public) | Requires HTML scraping — JS-rendered SPA; need Playwright or Accredible API key from account settings |
| Microsoft Learn | learn.microsoft.com/api/contentbrowser/search/credentials — undocumented public API discovered via exam-timeline |
None (User-Agent header only) | Fields vary; returns credential type, name, issued date, status |
Recommendation for v1: automate Credly and Microsoft Learn fully; attempt Databricks via HTML scrape with Playwright, fall back to manual if unreliable.
Data model changes¶
certifications table — new columns¶
| Column | Type | Notes |
|---|---|---|
source |
Text |
credly / databricks / microsoft / manual |
external_id |
Text (nullable, unique per source) |
Credly badge ID, MS credential ID |
badge_image_url |
Text (nullable) |
Remote image URL from source |
issued_at |
TIMESTAMP WITH TIME ZONE (nullable) |
|
expires_at |
TIMESTAMP WITH TIME ZONE (nullable) |
|
last_synced_at |
TIMESTAMP WITH TIME ZONE (nullable) |
Set on every successful sync |
sync_enabled |
Boolean |
Default true; set to false to pin a manual entry |
Existing columns (title, description, icon_type, link, link_text, sort_order) are kept unchanged so the frontend needs no immediate changes.
Alembic migration¶
New file: api/alembic/versions/0008_add_certification_sync_fields.py
Backend — new files¶
api/services/credentials_sync.py¶
Three async fetch functions, one per source:
async def sync_credly() -> list[CertificationUpsert]: ...
async def sync_microsoft_learn() -> list[CertificationUpsert]: ...
async def sync_databricks() -> list[CertificationUpsert]: ... # HTML scrape, may raise
Each returns a list of CertificationUpsert dataclass objects. A shared upsert_certifications(db, records) function matches on (source, external_id) and updates existing rows or inserts new ones. Records with sync_enabled=False are never touched.
Credly fetch:
GET https://www.credly.com/users/sven-relijveld/badges.json
# No auth. Returns data[].{badge_template.name, issued_at_date, expires_at_date,
# badge_template.image.url, earner_path, state}
# Filter: state == "accepted" and public == True
Microsoft Learn fetch:
POST https://learn.microsoft.com/api/contentbrowser/search/credentials
Headers: {"User-Agent": "Mozilla/5.0 (compatible; PortfolioSync/1.0)"}
# Returns credentials with name, issued date, status, type
Databricks fetch (Accredible):
- Attempt 1: check if
credentials.databricks.com/profile/svenrelijveld92543/walletexposes a JSON endpoint (.jsonsuffix or XHR observed in browser devtools) - Attempt 2: lightweight Playwright scrape (headless Chromium) — parse credential cards
- If neither works: log a warning and skip; records remain as manually-seeded entries
api/routers/sync.py¶
- Protected by EasyAuth (existing middleware — no additional auth code needed on prod; dev uses the same
X-ZUMO-AUTHheader bypass) - Calls all three sync functions, returns a summary:
{credly: N, microsoft: N, databricks: N, errors: [...]} - Rate-limited to 10/hour
ACA background job¶
A separate Container App job (not a new image — reuses the existing backend image with a different entrypoint):
New file: api/jobs/sync_certifications.py — imports the sync service, runs all three sources, exits 0 on success / non-zero on failure (ACA job retries on non-zero exit).
Bicep changes¶
New resource in infra/modules/container_apps.bicep:
resource certSyncJob 'Microsoft.App/jobs@2023-05-01' = {
name: '${prefix}-job-cert-sync'
// schedule trigger — cron expression
configuration: {
triggerType: 'Schedule'
scheduleTriggerConfig: {
cronExpression: '0 6 * * 1' // every Monday at 06:00 UTC
}
replicaTimeout: 300
replicaRetryLimit: 2
}
// same image as backend, entrypoint override
template: {
containers: [{
image: backendImage
command: ['python', '-m', 'api.jobs.sync_certifications']
env: backendEnvVars // DATABASE_URL, APP_CONFIG_ENDPOINT, etc.
}]
}
}
On-demand trigger¶
az containerapp job start \
--name dna-prd-portfolio-we-job-cert-sync \
--resource-group dna-prd-portfolio-westeurope-rg
Also wired to the POST /api/admin/sync-certifications endpoint which calls the same job start via Azure SDK, so it can be triggered from the portfolio admin UI.
Frontend changes¶
Schema additions to Certification response¶
badgeImageUrl?: string // null for manual entries without images
issuedAt?: string // ISO date string
expiresAt?: string // null if no expiry
source: string // 'credly' | 'databricks' | 'microsoft' | 'manual'
CertificationsSection component updates¶
- Show badge image (
<img src={badgeImageUrl} />) when available, fall back to current icon - Show issued/expiry date below the title
- Expired badges: dim the card (opacity-50) or show a "Expired" chip
Files to create / modify¶
| File | Action |
|---|---|
api/alembic/versions/0008_add_certification_sync_fields.py |
Create — migration |
api/db/models.py |
Modify — add columns to Certification |
api/schemas/responses.py |
Modify — add fields to Certification schema |
api/services/credentials_sync.py |
Create — fetch + upsert logic |
api/jobs/sync_certifications.py |
Create — job entrypoint |
api/routers/sync.py |
Create — admin trigger endpoint |
api/main.py |
Modify — register sync router |
api/repositories/certifications.py |
Modify — add upsert function |
api/data/seed.json |
Modify — add source, external_id, sync_enabled to existing entries |
infra/modules/container_apps.bicep |
Modify — add ACA job resource |
client/src/components/certifications-section.tsx |
Modify — show badge image + dates |
shared/api.types.ts |
Auto-regenerated via npm run types:generate after backend schema changes |
Build sequence¶
- Migration + model + schema changes (backend only, no infra yet)
- Sync service + job entrypoint — testable locally via
python -m api.jobs.sync_certifications - Admin router + registration in
main.py - Frontend: update
Certificationschema display - Bicep: add ACA job + deploy to dev, verify schedule trigger fires
- Seed existing manual entries with
source=manual,sync_enabled=false - Run first full sync, verify DB populated correctly
- Deploy to prod
Open questions¶
- Databricks: check account settings at
credentials.databricks.comfor an Accredible API key. If available, use the official API instead of scraping. - Sort order: after sync, badges are ordered by
issued_at DESC. Manual entries keep their existingsort_order. Decide if synced entries should interleave with manual ones or appear in a separate group. - Image caching: Credly image URLs are stable CDN URLs — safe to use directly. If hotlinking becomes unreliable, proxy through a backend
/api/badge-image/{id}endpoint that caches to Azure Blob Storage.