Custodian Onboarding
Custodian is a fourth platform role alongside Issuer, Holder, and Verifier: an organization that has already collected a large, unorganized dump of third-party credential documents (PDFs/images) for the people it works with, and wants those cataloged and claimable into each person’s wallet — without ever claiming to be the original issuing authority.
A Custodian-signed credential’s type array carries CustodianAttestationCredential, and its
wallet.credentials row carries evidence_object_key/evidence_sha256 — never the document
itself — so a verifier can always tell a Tier-2 attestation apart from a Tier-1 original issuance
(GET /api/v1/credentials/verify’s response includes provenanceTier/attestedBy).
Roles, compared
| Role | DID? | Where |
|---|---|---|
| Issuer | Real did:key/did:web, accredited, scoped to catalog credential types | issuer-registry |
| Holder | Placeholder (did:attestpro:pending:<email>) until claimed, then a real on-device did:key | Core Engine |
| Verifier | None — an approved account, not a signer | Core Engine |
| Custodian | Real did:key, never accredited/scoped — attests, never issues | custodian-registry |
A Custodian is never linked to a Holder’s DID directly. The relationship (a custodian attesting a
document for a specific person) is recorded as data — a custodian_intake_items row referencing a
matched_holder_did — not by merging or chaining DIDs.
Intake → review → attest flow
Design decisions
- A Custodian is not an Issuer. It never goes through accreditation and is never scoped to a
catalog credential type. Attestation runs through a dedicated
IssuanceOrchestrator#attestCredentialAsCustodianpath, not asigner_typebranch through ordinary issuance, so accreditation-scope checks, issuer-active validation, and JWT-based caller authorization (all Issuer-specific) never apply. - A Custodian still needs real DID/key custody, because it signs its own attestations —
unlike a Verifier.
custodian-registry(gRPC:50056) mirrorsissuer-registry’s registration/rotation/status/SignPayloadshape almost exactly (did:keygeneration, BYOK/ custodial split, encrypt-at-rest key custody via a sharedKeyEncryptionService). - No general-purpose document storage — one narrow exception for human review. A custodian’s raw documents are never served as original bytes, cached, or downloadable. Vision extraction takes a single transient in-memory read (never a field, never logged). Upload is server-proxied straight into cold storage, never a presigned browser-direct upload. The one exception is a watermarked, non-downloadable preview for a reviewer who needs to double-check an extraction (see below).
- Extraction is exhaustive, not a fixed checklist. The model extracts every distinct labeled field on the document (capped at 250 fields), not a fixed subset — a Custodian’s documents are credential records, not identity documents, so losing a detail to a too-narrow schema means losing it permanently, since the preview is the only fallback a human reviewer has.
- Large multi-page documents are handled explicitly. Pages render one at a time (JPEG, quality 0.82), stopping once a 12MB combined-payload budget would be exceeded, rather than blowing past a self-hosted model server’s request-size limit and failing the whole extraction.
- Holder matching and catalog suggestion are both best-effort and always overridable. An
extracted
holder_emailis checked first; failing that, a fuzzy name match is accepted only if it resolves to exactly one holder. A catalog-entry suggestion reuses the same embedding-based search the Issue Credential form uses. Neither suggestion is ever trusted directly — review is a mandatory human confirmation step. - Not every custodian-attested document belongs in the shared Program Catalog. Review accepts
exactly one of three catalog decisions: a confirmed catalog entry,
noCatalogMatch(flags a gap, auto-submitting aCatalogEntryProposal), or a free-textcustodianInternalCodein the custodian’s own namespace — carried into the issued VC ascustodian_internal_codeso a verifier sees exactly what internal record it corresponds to. - Cold storage is a LocalStack-style AWS-compatible emulator locally, real AWS S3 in production — the same convention the sibling Meridian stack already uses.
Multi-document files
Scanning often merges several separate documents into one PDF. A multi-page PDF is segmented first: cheap vision calls locate where each document starts and ends (new form/title/number, a restarted page counter, its own signature block). Anything doubtful (model unavailable, overlapping pages, too many documents/pages) falls back to reading the file whole, recording why.
When a file splits, custodian_intake_items becomes a two-level tree:
| Row | Meaning |
|---|---|
| Uploaded file | A container only (extraction_status = SPLIT) — never reviewed, attested, or rejected directly |
| Document (child) | A page range of the same cold-storage object — its own extraction, holder match, catalog suggestion, sensitivity, review, and attestation |
Duplicates, discarding, and erasure
Three layers guard against re-uploading or wrongly attesting the same document:
- Upload time — the file is hashed (SHA-256, streamed) before storage; an exact duplicate is refused before it ever reaches cold storage.
- Attestation time —
duplicateOfcompares a reviewed item against the same holder’s live attestations (same kind + same credential number, or same issue date when neither prints a number). A renewal is never flagged; an unidentifiable item is never flagged. - Deletion —
rejectkeeps the document; discard (pre-attestation only) deletes the stored document; erasure request (post-attestation) requires a different Admin user to approve, revokes the credential, and archives it — the credential row and audit history are kept, just flaggedARCHIVED.
Document preview & second-level authorization
An LLM extraction can miss or misread a field, so a reviewer needs a way to check the source — without turning that into a general document-storage feature.
- Never the original bytes. Every preview is a fresh re-render from cold storage, watermarked once at extraction time (a translucent tiled mark plus an Attest ID seal), never at view time.
- Never cached, never downloadable (
Cache-Control: no-store, inline JPEG only). - Second-level authorization for elevated-sensitivity documents. A classifier tags a document
(currently only
MEDICAL) at extraction time from its already-extracted fields. A tagged document requires the caller’s role to be Admin and a non-blankreason. Every access attempt, granted or denied, is audited. - Classification is advisory, a coarse first line of defense — not a substitute for a custodian’s own data-handling policy.
- Capture deterrents. The portal hides a preview page on a screen-capture shortcut, print, or loss of window focus, and reports the attempt to the audit trail (never the content).
Team roles
A custodian’s team has three roles — Viewer, Operator, Admin — checked server-side before any mutating call touches a service or writes an audit event. A magic-link session is always Admin.
| Action | Viewer | Operator | Admin |
|---|---|---|---|
| List/read, preview a non-sensitive document | yes | yes | yes |
| Preview a sensitive document | — | — | yes, with a reason |
| Upload, extract, review, attest, reject, discard, request erasure | — | yes | yes |
| Revoke an attestation, approve/decline an erasure request | — | — | yes |
Onboarding leads
Two admin-facing lead queues recruit new organizations onto the platform, both NEW → CONTACTED → ONBOARDED/DISMISSED:
- Issuer Onboarding Leads — auto-created, one per institution (not per document): at extraction,
an unmatched
issuing_authoritycreates or joins a lead keyed by(authority_key, jurisdiction). A background reconciliation job resolves a lead automatically once exactly one active issuer is the same institution, re-pointing every open document to it. - Custodian Onboarding Leads — admin-created only (support conversation, sales lead, referral), since a prospective Custodian is never referenced by name inside another actor’s workflow the way an issuing authority is.
Both queues’ loop-closing action registers the organization directly, landing it in the same pending-approval status as self-registration.
Tier-2 issuance
credential.proto’sIssueCredentialRequestcarries asigner_typeenum (ISSUERdefault /CUSTODIAN) plusevidence_object_key/evidence_sha256.CredentialIssuanceServiceImplresolves and signs via the Custodian Registry instead of the Issuer Registry whensigner_type=CUSTODIAN, and stamps the credential’stypearray withCustodianAttestationCredential.wallet.credentialscarriescustodian_id,signer_type,provenance_tier(ISSUED/CUSTODIAN_ATTESTED),evidence_object_key,evidence_sha256—issuer_idis nullable, exactly one signer per row.- Verification Engine resolves a Custodian’s public key as a third fallback branch (after Issuer
Registry, after federated
did:web); canonicalization and Ed25519 verification are unchanged. The REST verify response exposes flatprovenanceTier/attestedByfields.
Wallet, portal & claim
- Custodian Portal (
frontends/custodian-portal) — its own micro-frontend, magic-link/session pattern mirroring the verifier portal. Own visual identity (“Custody Amber”), deliberately distinct from the verifier portal’s emerald so the two trust tiers are never visually confused. Page layout, breadcrumbs, and action placement are specified in Custodian Portal UX. - Credential Wallet — a
provenanceblock (tier, attester) appears on anyCUSTODIAN_ATTESTEDcredential, with the disclosure: “Attest ID has not independently verified this against the original issuing authority — this record reflects what your organization reviewed from the source document.” The claim flow itself is unchanged — a Custodian attestation issued against a placeholder DID claims through the existing magic-link claim mechanism. See Holder Claim Integrity for how a claim is verified as belonging to the right person.
What’s next
The chain-of-custody arc past this document is specified in Chain of Custody Overview: holder claim-ownership hardening, cross-custodian convergence when a second party touches the same record, and verifier-facing provenance disclosure.