Skip to main content
Owner: Backend Engineering Last reviewed: 2026-09-24 Next review: 2026-12-24 The certificate backend fills the supplied Learnille certificate design for a learner who completes a course. It owns eligibility, text layout and PDF storage. The frontend lists certificates and fetches authenticated PDF blobs.

Lifecycle and eligibility

  1. The progress controller resolves the JWT account to its student profile. Legacy userId parameters 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.
  2. Final lesson/video progress queues generate-certificate on the certificate-generation queue. Jobs carry studentId and courseId; legacy jobs named certificate-generation with a student-profile ID in userId still work. A stable job ID deduplicates outstanding work.
  3. 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.
  4. Under a PostgreSQL student-row write lock, issuance reads the learner’s latest course enrollment. Only approved/completed enrollments qualify. New issuance also requires unexpired access and all active sections to be accessible.
  5. 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.
  6. The PDF uploads to private storage. The certificate snapshot and a COURSE_CERTIFICATE_ISSUED outbox event commit in the same database transaction. The unique (user_id, course_id) index prevents duplicate certificates.
  7. 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.
A retry reuses the original certificate, name, title, issuance timestamp and PDF. The existing record remains downloadable by its owner after profile/course edits, access expiry or refund; these routes do not implement revocation. Reissuing via POST still rejects a currently refunded/rejected/pending enrollment. No public verification, administrator download override, manual content override, signature upload, or consultation/space certificate is exposed.

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 existing SimpleStorageService 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 CreateCertificates1790200000000 before deploying the routes and worker in production. It is exported by src/migrations/migrations.ts for 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_BUCKET is 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-certificate screen to the API; its current placeholder data is not changed by this backend implementation.
The public integration guide is the contract source for envelopes, examples, errors and authenticated downloads. The endpoint map and certificate QA checklist cover discovery and acceptance. Documentation must be published separately to appear on the live Mintlify site. Template version 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.