Issuer
An issuer is an institution (a university, college or school) whose wallet address is registered in the IssuerRegistry with a name. Every credential it issues carries that address and the issuer's EIP-712 signature, so a verifier can see who stands behind it. The issuer manages the whole life of its own credentials and of the links to its students' wallets, and nothing that belongs to another issuer.
Words used here: the issuer key is the wallet address that signs. A key can be retired in favour of a successor key (contract version 0.5.0 onward); the successor is then the key in charge of everything the retired key issued. See Key retirement.
How the app recognises an issuer
| Where | Check | Source |
|---|---|---|
| Pages (v0.3) | IssuerRegistry.isIssuer(address) is true and successorOf(address) is the zero address | src/services/domain/issuer-access-service.ts |
Server routes /api/upload, /api/documents | A Sign-In with Ethereum session cookie, and IssuerRegistry.isActiveIssuer(address) re-checked at least every 5 minutes (registries of older sets without key lineage fall back to isIssuer) | src/lib/server/issuer-session.ts, src/lib/server/roles.ts |
| Contracts | isActiveIssuer(msg.sender) to issue, invite (checked when the student redeems) and register types; "key in charge" (currentIssuerOf(credential issuer) == msg.sender) to change a status | blockchain/contracts/v03/*.sol |
| Pages (v0.2) | AcademicRecords.hasRole(UNIVERSITY_ROLE, address) | src/services/domain/role-service.ts |
The admin registers an issuer with addIssuer(issuer, name); names are unique and at most 128 bytes. Onboarding is described in Operator.
A session lasts 30 minutes. A wallet that loses its registration during a session gets 403 "Wallet is no longer a registered university" at the next re-check, and an RPC outage during the check gives 503 rather than a guess. See Sign-in and sessions.
What an issuer sees
The top bar shows the Issue workspace. On v0.3 it holds:
| Link | Route | Page title |
|---|---|---|
| Overview | /dashboard | The issuer's registered name, a Registered issuer badge, the list Issued credentials and Credential tools |
| Issue a credential | /records/add | Add Academic Record |
| Bulk import | /records/import | Import credentials |
| Students | /students/invite | Students (invite and manage links) |
| Record types | /records/types | Record types |
A retired key that opens /dashboard sees the page Retired key with "This key was retired. ... is in charge of the credentials it issued, and this key can issue and change nothing more." A failed registration read shows "Could not check your issuer registration." instead of redirecting.
Actions
Issue a credential
- Open Issue a credential. The header says "Issuing on behalf of" the registered name.
- Student: fill Student Name and Student ID. Optionally press Check link to see whether the student ID is linked to a wallet (the wallet asks for a signature).
- Credential: choose a Record Type (own types are marked "(issuer-defined)"), and optionally Programme and Award. These details are not stored on chain; only salted hashes are.
- Document: choose the Document file and press Encrypt and upload. The file is encrypted in the browser and uploaded as a container. The first upload of a session asks for a Sign-In with Ethereum signature. The page shows the IPFS Hash and the Document SHA-256.
- Delivery: tick Deliver to the student's linked wallet to let the student's wallet receive it (the wallet asks for one signature). Unticked, the page asks first: "Issue without delivering to a wallet?" with Issue without delivery. This cannot be changed after issuing.
- Press Issue Record. The wallet asks for the EIP-712 credential signature, then for the
issuetransaction. - The issued card offers Download credential package (give it to the holder, keep it private), the verification link and QR, Show a QR code for the holder's phone, Download as verifiable credential, Download as Open Badge and, where available, Download as Open Badge with standard proof.
The button is disabled while the system is paused. Flows: Issuing, Document storage, Enrolment. Files: Credential package, Encrypted container.
Look after issued credentials
The Issued credentials list on /dashboard is rebuilt from chain events across every listed contract set and shows each credential's live status. Each row offers:
| Button | What it does | Wallet prompt |
|---|---|---|
| Verify | Opens /verify?id=... | none |
| Open document | Looks up the stored copies through /api/documents and decrypts the newest one the issuer's own key can open | Sign-in signature if no session; the wallet may ask for the key signature |
| Replace key | Re-encrypts the document under a new key ("Replace this document's key?", Replace key), then Save key update file for the holder and Remove old stored copies | Sign-in signature if no session |
| Manage access | Admit or remove access codes for the credential (where the access gate is deployed) | transaction per change |
| Re-issue | Opens /records/add?replaces=...; the new credential is issued and the old one superseded in one transaction (issueReplacing) | signature and transaction |
| Revoke | "Revoke this credential?" with a Reason and an optional Note, then Revoke credential | transaction |
When a re-issue crosses contract sets, it takes two transactions. If the second is rejected, the dashboard shows Replacement not finished. with Finish replacement. Removing old copies is not offered when another credential shares the same stored file. Flows: Lifecycle, Sharing, Contract set upgrade.
Suspend, reinstate or supersede
These live on the verify page. Look the credential up, press Manage as issuer, connect the issuing wallet, and the Issuer tools appear with a Reason, a Note (only its hash is stored on-chain) and buttons for the moves the current status allows: Suspend, Reinstate, Revoke and Supersede (with Replacement credential id). Each needs Confirm suspend (and so on) and sends one transaction.
| Current status | Moves the issuer can make |
|---|---|
| Active | suspend, revoke, supersede |
| Suspended by the issuer | reinstate, revoke, supersede |
| Suspended by the admin (admin hold) | revoke only |
| Revoked or superseded | none ("This status is final; no further changes are possible.") |
Reason codes 0 to 7 have names (for example Issued in error, Data correction, Holder request); 8 to 999 are reserved and refused by the contract. These moves work while the system is paused. Flow: Lifecycle.
Link students' wallets
On /students/invite:
- Invite a student: enter the Student ID, the Student wallet and Valid for (1, 7, 14 or 29 days), press Create invitation. The wallet signs an EIP-712 invitation (no transaction). Send the file to the student.
- Manage a student's link: Look up by student ID, then Unlink wallet (confirm Unlink) or Cancel invitations (confirm Cancel invitations). Each is a transaction (
unbind,revokeInvitations), allowed while paused, refused for a retired key.
Flow: Enrolment. File: Invitation file.
Define record types
On /records/types, under Add a record type, fill Name and an optional Description and press Add type (a transaction, refused while paused). In Your record types, Turn off and Turn on switch a type (allowed while paused). Ids are per issuer and start at 1000. Flow: Record types.
Import a cohort
On /records/import (Import credentials):
- Choose your files: an Export format, the Records file (CSV or JSON), optionally an Earlier result file and a Document folder. Files are read on the computer, not uploaded.
- Before you start: optionally "Replace the earlier credential of each changed row" and "Add a selective disclosure signature to each credential".
- Review the whole-file check, then Start issuing. Each row asks the wallet to sign and send; Stop after this row stops cleanly, and choosing the same file again carries on.
- At the end, Download packages (N) (keep the archive private) and Download result file, then Finish.
Flow: Bulk import. Files: Import files.
Other credential tools
Under Credential tools on the dashboard: an export panel (verifiable credential or Open Badge from a package) and Add selective disclosure to an earlier credential (choose the Credential package file; the updated package is saved for the holder). Flows: Verifiable credentials, Selective disclosure.
What an issuer cannot do
- Touch another issuer's credentials, students or types. Every contract check compares the caller with the credential's issuer key in charge.
- Lift an admin hold or reinstate a credential the admin suspended. Only the admin can.
- Register, rename or remove issuers, change roles, pause or unpause. Those are admin and guardian powers.
- Issue while paused, or issue with a reason code from the reserved range.
- Issue with a retired key. After
retireIssuer(old, new), the old key can issue and change nothing; the successor manages the old credentials. - Lift its own issuer suspension. If a guardian suspends the issuer, every credential of that issuer reads as not valid (
StatusRegistry.isValidis false) until the admin lifts it. The suspension does not block theissuecall itself; it makes the results invalid. - Change what was issued. Anchors are permanent; corrections are made by superseding with a new credential.
- Deliver an undelivered credential later. Re-issue it with delivery ticked and supersede the old one.
- Issue through the site with a contract wallet (for example a Safe). The contracts accept ERC-1271 signatures, but the site's issuing flow uses a browser wallet.
Keys an issuer holds
| Key | Holds funds | Notes |
|---|---|---|
| Issuer key | Gas only | Signs every credential and invitation and sends the transactions. Its custody is the institution's choice (see the onboarding key ceremony in docs/onboarding/02-key-ceremony.md). Loss or theft is handled by retiring it in favour of a successor. |
| Sign-in session | No | A 30 minute cookie issued after a Sign-In with Ethereum signature; it only allows upload and document management. |
Issuers also produce, and must not keep longer than needed, credential packages and import archives: each package contains the key that opens the holder's document.
Checklist
- Confirm the dashboard shows your registered name and the Registered issuer badge.
- Invite students with the wallet they gave you before issuing with delivery, or issue first and invite later (order does not matter).
- Check the record type and the document before pressing Issue Record; corrections need a re-issue.
- Hand each holder only their own package, privately.
- Revoke or supersede promptly when a credential was issued in error; use a reason code and a note.
- Keep the issuer key under the custody agreed at onboarding; report a suspected compromise to the operator at once.
Source files
src/app/dashboard/IssuerDashboardPage.tsx,IssuedCredentialsPanel.tsx,IssuedCredentialsTable.tsx,RevokeDialog.tsx,ReplaceKeyFlow.tsx,PendingReplacements.tsx,AddDisclosurePanel.tsxsrc/app/records/add/AddRecordPage.tsx,DocumentUploadField.tsx,DeliveryOption.tsx,StudentLinkCheck.tsx,CredentialIssuedCard.tsxsrc/app/verify/ManageCredential.tsx,src/lib/status-actions.ts,src/lib/status-reasons.tssrc/app/students/invite/InviteStudentPanel.tsx,ManageStudentPanel.tsxsrc/app/records/types/RecordTypesPanel.tsx,src/app/records/import/ImportWorkspace.tsxsrc/lib/server/issuer-session.ts,src/lib/server/roles.ts,src/services/upload.tsblockchain/contracts/v03/IssuerRegistry.sol,CredentialRegistry.sol,StatusRegistry.sol,HolderRegistry.sol,RecordTypeRegistry.sol
Tests
- Rendered and unit tests:
src/app/dashboard/IssuedCredentialsPanel.test.tsx,RevokeDialog.test.tsx,ReplaceKeyFlow.test.tsx,PendingReplacements.test.tsx;src/app/records/add/submit-record.test.ts,DeliveryOption.test.tsx,StudentLinkCheck.test.tsx,CredentialIssuedCard.test.tsx;src/app/verify/ManageCredential.test.tsx;src/app/students/invite/InviteStudentPanel.test.tsx,ManageStudentPanel.test.tsx;src/app/records/types/RecordTypesPanel.test.tsx;src/app/records/import/ImportWorkspace.test.tsx - Contracts:
blockchain/test/v03/CredentialIssuance.test.ts,StatusRegistry.test.ts,HolderRegistry.test.ts,RecordTypeRegistry.test.ts,KeyLineage.test.ts - E2E:
e2e/journeys/issue-and-verify.journey.ts,dashboard-actions.journey.ts,delivery.journey.ts,manage-student-link.journey.ts,bulk-import.journey.ts,revoke-and-reverify.journey.ts,replace-shared-file.journey.ts,key-retirement.journey.ts