> ## Documentation Index
> Fetch the complete documentation index at: https://ihub.learnille.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Course completion certificates

> Issuance eligibility, PDF generation, storage, notifications and frontend responsibilities

**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.

```mermaid theme={null}
flowchart LR
  P[Final progress update] --> Q[Certificate queue]
  Q --> I[Eligibility and issuance]
  H[Authenticated issue request] --> I
  I --> S[Private PDF upload]
  S --> T[Certificate and outbox commit]
  T --> N[Completion email]
  T --> D[Owner lists and downloads PDF]
```

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](https://docs.learnille.com/flows/certificates) is
the contract source for envelopes, examples, errors and authenticated downloads.
The [endpoint map](https://ihub.learnille.com/engineering/api-endpoint-map/overview.md) and
[certificate QA checklist](https://ihub.learnille.com/engineering/testing-and-qa/course-certificates.md) 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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.