Lifecycle and eligibility
- The progress controller resolves the JWT account to its student profile. Legacy
userIdparameters accept either that account ID or that profile ID, never another learner’s ID. Progress writes validate enrollment and active content belonging to the requested course and purchased section. - Final lesson/video progress queues
generate-certificateon thecertificate-generationqueue. Jobs carrystudentIdandcourseId; legacy jobs namedcertificate-generationwith a student-profile ID inuserIdstill work. A stable job ID deduplicates outstanding work. - The worker resolves the actual account ID and invokes the same issuance service
as
POST /certificate/course/:courseId/issue. The HTTP route is a synchronous, idempotent recovery path for historical completions and queue failures. - Under a PostgreSQL student-row write lock, issuance reads the learner’s latest
course enrollment. Only
approved/completedenrollments qualify. New issuance also requires unexpired access and all active sections to be accessible. - Completion is recomputed from active content IDs: every active subsection is marked complete, or every active video is completed. Empty courses, stale IDs, duplicate IDs and rounded percentages cannot satisfy this check. This follows the current lesson/video model; it does not independently prove watch duration, quiz results or assignment approval.
- The PDF uploads to private storage. The certificate snapshot and a
COURSE_CERTIFICATE_ISSUEDoutbox event commit in the same database transaction. The unique(user_id, course_id)index prevents duplicate certificates. - The outbox invokes the existing course-completion email workflow with a stable notification transaction ID. Delivery failures retry through the outbox. No additional in-app certificate workflow is introduced.
Rendering and immutable records
server/src/certificate/certificate.renderer.ts loads
src/assets/certificates/completion-v2.pdf and the project’s Proxima Nova Light,
Bold and Extrabold fonts. The bundled template is the supplied September 30 replacement PDF with an empty
date area. The renderer writes the UTC issuance date exactly once. The source Downloads files are untouched.
All static vector artwork, logos, labels and decoration are retained.
The output is one 842 × 595 point landscape page. Date, name and course text use
measured reference coordinates and 12/32/24 point starting sizes respectively.
Names wrap to two lines down to 18 points; course descriptions wrap to three lines
down to 14 points. Text is normalized to NFC, whitespace collapsed and widths
measured using the embedded font. Unbroken long words wrap by character. Text
that cannot fit legibly, unsupported glyphs and missing names/titles fail with
422; no truncation or replacement squares are allowed. Proxima Nova does not
cover every writing system. Add an approved font and visual fixtures before
claiming support for additional scripts. The original issuer name/signature areas
remain blank, matching the supplied filled reference.
The renderer embeds complete OTF fonts and corrects pdf-lib 1.17’s font dictionary
classification to OpenType CFF. Keep the rendering tests and Poppler checks when
upgrading either PDF dependency. Nest’s existing assets/**/* build rule copies
the template/fonts into dist/assets/certificates; no network font fetch occurs.
The record stores UUIDs for the account/course/enrollment, the display-name/title/
description snapshot, UTC issuance time, template version, private object key and
PDF SHA-256. These are historical snapshots, not cascading relations. Downloads
read the original bytes and verify their checksum; they never silently regenerate
using edited profile/course data. A checksum is not a digital signature.
Storage, failure handling and operations
Use the existingSimpleStorageService and PRIVATE_STORAGE_BUCKET; never make
the certificate prefix public. Object keys are deterministic per account/course
(certificates/{userId}/{courseId}.pdf) and are not returned in API metadata.
Retries after a database rollback overwrite the uncommitted object at the same
key. A successful record is never rewritten. An upload followed by a failed DB
commit can leave an unreferenced private object; reconcile such objects against
certificate records before deleting them. Do not delete objects with a live record.
Generation uses the queue’s existing five attempts with exponential backoff.
Ineligible/missing enrollment is a completed no-op; rendering/storage/database
failures remain retryable/visible as failed jobs. Queue enqueue errors leave saved
progress intact. The issue API provides recovery; no bulk backfill is run.
Notification delivery uses the existing outbox scheduler/worker and Novu workflow.
The link is {FRONTEND_URL}/my-certificate?certificateId={id}, not an unauthenticated
PDF URL. Keep the course-completion email template and workflow configured.
Deployment and integration
- Install dependencies with the project’s pinned pnpm 9.15.4 on Node 24.
- Run migration
CreateCertificates1790200000000before deploying the routes and worker in production. It is exported bysrc/migrations/migrations.tsfor the existing migration runner. Do not run its destructive down migration on issued production records without a backup/recovery plan. - Supply the existing database, Redis, storage, frontend URL and notification
settings through the selected Infisical environment. No certificate-specific
secrets are needed. Confirm
PRIVATE_STORAGE_BUCKETis private and writable. - Ensure both the certificate and outbox workers run, and packaged font/template assets are present in the deployment artifact.
- Connect the existing frontend
/my-certificatescreen to the API; its current placeholder data is not changed by this backend implementation.
completion-v2 applies to newly issued certificates after the backend is rebuilt and restarted. Existing stored PDFs remain immutable; issuing again for the same enrollment returns the existing certificate. Use a new completed course enrollment to test the replacement template.