Skip to Content
ConceptsCustodian DomainOnboarding

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

RoleDID?Where
IssuerReal did:key/did:web, accredited, scoped to catalog credential typesissuer-registry
HolderPlaceholder (did:attestpro:pending:<email>) until claimed, then a real on-device did:keyCore Engine
VerifierNone — an approved account, not a signerCore Engine
CustodianReal did:key, never accredited/scoped — attests, never issuescustodian-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#attestCredentialAsCustodian path, not a signer_type branch 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) mirrors issuer-registry’s registration/rotation/status/SignPayload shape almost exactly (did:key generation, BYOK/ custodial split, encrypt-at-rest key custody via a shared KeyEncryptionService).
  • 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_email is 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 a CatalogEntryProposal), or a free-text custodianInternalCode in the custodian’s own namespace — carried into the issued VC as custodian_internal_code so 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:

RowMeaning
Uploaded fileA 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:

  1. Upload time — the file is hashed (SHA-256, streamed) before storage; an exact duplicate is refused before it ever reaches cold storage.
  2. Attestation time — duplicateOf compares 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.
  3. Deletion — reject keeps 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 flagged ARCHIVED.

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-blank reason. 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.

ActionViewerOperatorAdmin
List/read, preview a non-sensitive documentyesyesyes
Preview a sensitive document——yes, with a reason
Upload, extract, review, attest, reject, discard, request erasure—yesyes
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_authority creates 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’s IssueCredentialRequest carries a signer_type enum (ISSUER default / CUSTODIAN) plus evidence_object_key/evidence_sha256. CredentialIssuanceServiceImpl resolves and signs via the Custodian Registry instead of the Issuer Registry when signer_type=CUSTODIAN, and stamps the credential’s type array with CustodianAttestationCredential.
  • wallet.credentials carries custodian_id, signer_type, provenance_tier (ISSUED/CUSTODIAN_ATTESTED), evidence_object_key, evidence_sha256 — issuer_id is 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 flat provenanceTier/attestedBy fields.

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 provenance block (tier, attester) appears on any CUSTODIAN_ATTESTED credential, 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.