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
- One active
Certificateper(tenantId, enrollmentId, courseVersionId); retakes create new cert. proofJWS signature must verify against tenant signing key.verificationTokenis globally unique; HMAC fingerprint included.state = revokedcannot transition back.expiresAt > issuedAt.- Offline claim requires valid
localSignaturederived from bundle key.
4. Domain Events
certification.certificate.issued.v1certification.certificate.revoked.v1certification.certificate.verified.v1(audit trail)certification.offline_claim.submitted.v1certification.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