Skip to main content

Domain Model

:::info Source Sourced from services/certification-service/DOMAIN_MODEL.md in the documentation repo. :::

1. Aggregates

Certificate (root)

type CertificateId = Branded<string, 'CertificateId'>;

interface Certificate {
id: CertificateId;
tenantId: TenantId;
userId: UserId;
courseId: CourseId;
courseVersionId: CourseVersionId;
enrollmentId: EnrollmentId;
templateId: string;
issuedAt: ISODate;
expiresAt?: ISODate;
state: 'pending_offline_verification' | 'issued' | 'revoked';
evidence: { completionRecordId: string };
proof: JWS;
artifacts: CertificateArtifacts;
verificationToken: string;
}

interface CertificateArtifacts {
pdfUrl: string;
pngUrl: string;
openBadgesUrl?: string; // OpenBadges 3.0 Verifiable Credential
walletJwsUrl?: string; // Google/Apple Wallet pass
}

CertificateTemplate

interface CertificateTemplate {
id: string;
tenantId: TenantId | null; // null = system template
name: I18nString;
layout: TemplateLayout; // HTML+CSS with placeholders
branding: { logoAssetId?: MediaAssetId; colors: { primary: string; secondary: string }; fontFamily?: string };
signatoryBlocks: Signatory[];
legalFooter?: I18nString;
status: 'draft' | 'active' | 'archived';
}

RevocationRecord

interface RevocationRecord {
id: ULID;
certificateId: CertificateId;
tenantId: TenantId;
revokedBy: UserId;
revokedAt: ISODate;
reason: 'issued_in_error' | 'misconduct' | 'admin_request' | 'compliance';
publicReason?: I18nString; // shown to verifier
}

OfflineIssuanceClaim

interface OfflineIssuanceClaim {
id: ULID;
tenantId: TenantId;
userId: UserId;
enrollmentId: EnrollmentId;
completionEvidence: {
attemptId: AttemptId;
localSignature: JWS; // signed by bundle key
localCompletedAt: ISODate;
};
claimedAt: ISODate;
status: 'pending' | 'verified_issued' | 'rejected';
}

2. State Machine

Certificate:
[new] → pending_offline_verification (if claim-based)
pending_offline_verification → issued (after verification)
[new] → issued (online direct)
issued → revoked (terminal)

3. Invariants

  1. One active Certificate per (tenantId, enrollmentId, courseVersionId); retakes create new cert.
  2. proof JWS signature must verify against tenant signing key.
  3. verificationToken is globally unique; HMAC fingerprint included.
  4. state = revoked cannot transition back.
  5. expiresAt > issuedAt.
  6. Offline claim requires valid localSignature derived from bundle key.

4. Domain Events

  • certification.certificate.issued.v1
  • certification.certificate.revoked.v1
  • certification.certificate.verified.v1 (audit trail)
  • certification.offline_claim.submitted.v1
  • certification.offline_claim.verified.v1 / .rejected.v1

5. Diagram

progress.completion.recorded.v1 ──▶ certification-service


IssueCertificate

├─▶ Render PDF/PNG/OpenBadges
├─▶ Sign JWS proof
├─▶ Persist
└─▶ Emit certificate.issued.v1

├─▶ notification (congrats)
└─▶ analytics

Public verify (unauthenticated):
GET /api/v1/certificates/verify/{token} → return basic info + proof validity