Roles
Certichain has no user accounts and no login database. A person's role is whatever the chain says about the wallet address they connect, plus whatever files they hold. The same browser can be a verifier one minute and an issuer the next, depending on which MetaMask account is selected.
This section describes each role: how the app recognises it, what it sees, every action it can take and where that action is explained in full.
| Page | Who |
|---|---|
| Verifier | Anyone who checks a credential. No wallet needed. Also covers a relying party that proves access through the access gate. |
| Holder | The student a credential is about. Keeps the credential package, opens and shares the document, shows selected fields. |
| Issuer | A registered institution wallet. Issues and manages its own credentials, invites students, defines record types. |
| Guardian | An emergency key that can pause new issuance and suspend an issuer at once. |
| Admin | The holder of the admin role on the registries: a 2-of-3 Safe on Sepolia, with a timelock ready to take over. |
| Operator | Whoever runs a deployment: contracts, server keys, monitor, storage, support and onboarding. |
| Legacy v0.2 roles | Super admin, admin, university and student on the older single-contract deployment. |
How the app decides a role
Every role check reads the chain through the configured RPC endpoint; none is stored by the server. The decision is made in one place, IssuerAccessService.accessOf in src/services/domain/issuer-access-service.ts, and used by the header, the sign-in page and the page gates.
Details that matter:
- On v0.3,
isAdminmeans "may open the governance screen". It isIssuerRegistry.isGuardian(address), which is true for a guardian and also for the admin (DEFAULT_ADMIN_ROLE). In practice the admin is a Safe, which cannot connect to the site as a browser wallet, so the account that reaches/adminis the guardian. - A retired issuer key is not an issuer.
isIssueris false whenIssuerRegistry.successorOfnames a successor (contract version 0.5.0 onward). Signing in with a retired key lands on/student; opening/dashboarddirectly shows a Retired key page that names the key now in charge. - The sign-in redirect in
src/app/login/page.tsxisisAdmin ? "/admin" : isIssuer ? "/dashboard" : "/student". A wallet that is both guardian and issuer goes to/adminand still sees both workspaces in the top bar. - A failed read is not a "no".
accessOfthrows on an RPC failure, and the pages show a "could not check" notice instead of redirecting. - The top bar is built by
navigationForinsrc/components/layout/navigation.ts: Verify always; Issue whenisIssuer; Admin whenisAdminorisSuperAdmin; My credentials for any other connected wallet. It changes by itself when the wallet account changes, becauseuseWalletSessionfollows the wallet'saccountsChangedevents.
Page gates
| Route | Gate (v0.3) | Where a wallet without the role is sent |
|---|---|---|
/, /verify, /present, /open, /about, legal pages | none | (open to everyone, no wallet) |
/wallet | none to add and show codes; signing a verifier's challenge uses the injected wallet | (open) |
/access | none to read; proving sends a transaction from the connected wallet | (open) |
/student | connected wallet | not connected: /login; a registered issuer: /dashboard |
/dashboard | isIssuer | not connected: /login; retired key: Retired key page; otherwise /student |
/records/add, /records/import | isIssuer | not connected: /login; otherwise /records, which on v0.3 redirects to /dashboard and from there to /student |
/students/invite, /records/types | isIssuer (v0.3 only) | not connected: /login; on v0.2: /dashboard; otherwise /records |
/admin | accessOf(...).isAdmin (guardian or admin) | /login |
/records, /records/[id] | v0.2 pages; on v0.3 they redirect to /dashboard and /verify?id= |
The server routes have their own gates, independent of the pages: /api/upload and /api/documents require a Sign-In with Ethereum session for a wallet that the server itself confirms is a registered issuer (src/lib/server/issuer-session.ts), and /api/ops/* require the monitor secret. See Sign-in and sessions.
Permission matrix
The matrix is built from the access checks in the v0.3 contracts (blockchain/contracts/v03/, blockchain/contracts/zk/CertichainAccessGate.sol) and the app's gates. "Yes" means the contract or the app lets that role do it; a blank cell means it cannot. Notes say where a pause blocks the action.
Legend for the admin columns: Admin Safe is the holder of DEFAULT_ADMIN_ROLE today (a 2-of-3 Safe on Sepolia, after the deployer handed over). Timelock is CertichainTimelock, which is deployed and becomes the admin once the registries accept it; after that every admin action is proposed by the Safe, waits the delay, and is executed by anyone. Operator covers off-chain powers only.
Checking credentials
| Action | Verifier | Holder | Issuer | Guardian | Admin Safe | Timelock | Operator |
|---|---|---|---|---|---|---|---|
Look up a credential and its status on /verify | Yes | Yes | Yes | Yes | Yes | ||
| Check a document, package, verifiable credential or disclosure file | Yes | Yes | Yes | Yes | Yes | ||
Scan a phone code and send a challenge on /present | Yes | Yes | Yes | Yes | Yes | ||
| Call the verification API or use the SDK | Yes, once the operator enables the API | Yes | Yes | Yes | Enables it | ||
Open a document from a capability link on /open | Anyone holding the link | Yes | |||||
Prove access through the access gate (proveAccess) | An admitted requester; blocked by a pause |
Holding credentials
| Action | Verifier | Holder | Issuer | Guardian | Admin Safe | Timelock | Operator |
|---|---|---|---|---|---|---|---|
Accept an invitation (HolderRegistry.redeem) | The wallet named in the invitation; blocked by a pause | ||||||
Move a binding to a new wallet (rebind) | The new wallet, with a fresh invitation; blocked by a pause | ||||||
| See credentials delivered to the wallet (inbox) | Yes | ||||||
| Open the document from the credential package | Anyone with the package | Yes, from the dashboard | |||||
| Share the document by share code or link | Anyone with the package | ||||||
| Create a selective disclosure file | Yes (signs with the bound wallet) | ||||||
| Export a verifiable credential from the package | Yes | Yes | |||||
| Add a credential to a phone and sign a verifier's challenge | Yes | ||||||
Admit or remove access group members (admit, removeMember) | The bound holder wallet | Yes (admit needs a registered issuer) |
Issuing and managing credentials
| Action | Verifier | Holder | Issuer | Guardian | Admin Safe | Timelock | Operator |
|---|---|---|---|---|---|---|---|
Upload an encrypted document (/api/upload) | Yes, with a sign-in session | ||||||
Issue a credential (CredentialRegistry.issue) | Active issuer only; blocked by a pause | ||||||
Re-issue and supersede in one transaction (issueReplacing) | Active issuer; blocked by a pause | ||||||
| Suspend, reinstate, revoke or supersede an own credential | Yes, also while paused | Suspend as an admin hold | Same, after handover | ||||
| Lift an admin hold | Yes | After handover | |||||
| Create an enrolment invitation (off-chain signature) | Yes | ||||||
Unlink a student wallet, cancel invitations (unbind, revokeInvitations) | Yes, key not retired | ||||||
Register a record type (registerType) | Active issuer; blocked by a pause | ||||||
Turn an own record type off or on (setTypeActive) | Yes, key not retired | ||||||
| Bulk import a cohort | Yes | ||||||
| Replace a document key, remove old stored copies | Yes, with a sign-in session |
Running the system
| Action | Verifier | Holder | Issuer | Guardian | Admin Safe | Timelock | Operator |
|---|---|---|---|---|---|---|---|
Pause new issuance (IssuerRegistry.pause) | Yes | Yes | After handover | ||||
| Unpause | On sets from contract version 0.5.0 | Yes | After handover | ||||
Suspend an issuer (setIssuerSuspended(issuer, true, ...)) | Yes | Yes | After handover | ||||
| Lift an issuer suspension | Yes (StatusRegistry admin) | After handover | |||||
| Add, remove or rename an issuer | Yes | After handover | Proposes through the Safe | ||||
| Retire an issuer key in favour of a successor | Yes (0.5.0 sets) | After handover | |||||
| Grant or revoke the guardian role | Yes | After handover | |||||
| Begin, accept or cancel an admin transfer | Yes (the nominee accepts) | Yes, as nominee | |||||
| Schedule or cancel a timelock operation | Yes (proposer and canceller) | ||||||
| Execute a ready timelock operation | Anyone | Anyone | Anyone | Anyone | Anyone | Anyone | |
Announce a contract set (VersionAnnouncer.announce) | The announcer's admin | ||||||
| Deploy contracts, set server keys, run the monitor, repair pins | Yes |
What a pause stops and what it never stops is pinned by blockchain/test/v03/PauseClassification.test.ts: issuing, accepting or re-linking an invitation, registering a record type, admitting to an access group and proving access stop; revoking, suspending, reinstating, superseding, unlinking, cancelling invitations, switching a type off and every read keep working.
Source files
src/services/domain/issuer-access-service.ts,src/services/domain/role-service.tssrc/components/layout/navigation.ts,src/components/layout/Header.tsxsrc/app/login/page.tsxand the page gates undersrc/app/src/lib/server/issuer-session.tsblockchain/contracts/v03/IssuerRegistry.sol,StatusRegistry.sol,CredentialRegistry.sol,HolderRegistry.sol,RecordTypeRegistry.sol,CertichainTimelock.sol,VersionAnnouncer.solblockchain/contracts/zk/CertichainAccessGate.sol
Tests
src/components/layout/navigation.test.ts,src/components/layout/Header.test.tsxsrc/services/domain/issuer-access-service.test.tsblockchain/test/v03/PauseClassification.test.ts,blockchain/test/v03/Governance.test.ts,blockchain/test/v03/SafeGovernance.test.ts- E2E:
e2e/journeys/pause-and-restart.journey.ts,e2e/journeys/key-retirement.journey.ts