Skip to main content

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.

PageWho
VerifierAnyone who checks a credential. No wallet needed. Also covers a relying party that proves access through the access gate.
HolderThe student a credential is about. Keeps the credential package, opens and shares the document, shows selected fields.
IssuerA registered institution wallet. Issues and manages its own credentials, invites students, defines record types.
GuardianAn emergency key that can pause new issuance and suspend an issuer at once.
AdminThe holder of the admin role on the registries: a 2-of-3 Safe on Sepolia, with a timelock ready to take over.
OperatorWhoever runs a deployment: contracts, server keys, monitor, storage, support and onboarding.
Legacy v0.2 rolesSuper 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, isAdmin means "may open the governance screen". It is IssuerRegistry.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 /admin is the guardian.
  • A retired issuer key is not an issuer. isIssuer is false when IssuerRegistry.successorOf names a successor (contract version 0.5.0 onward). Signing in with a retired key lands on /student; opening /dashboard directly shows a Retired key page that names the key now in charge.
  • The sign-in redirect in src/app/login/page.tsx is isAdmin ? "/admin" : isIssuer ? "/dashboard" : "/student". A wallet that is both guardian and issuer goes to /admin and still sees both workspaces in the top bar.
  • A failed read is not a "no". accessOf throws on an RPC failure, and the pages show a "could not check" notice instead of redirecting.
  • The top bar is built by navigationFor in src/components/layout/navigation.ts: Verify always; Issue when isIssuer; Admin when isAdmin or isSuperAdmin; My credentials for any other connected wallet. It changes by itself when the wallet account changes, because useWalletSession follows the wallet's accountsChanged events.

Page gates​

RouteGate (v0.3)Where a wallet without the role is sent
/, /verify, /present, /open, /about, legal pagesnone(open to everyone, no wallet)
/walletnone to add and show codes; signing a verifier's challenge uses the injected wallet(open)
/accessnone to read; proving sends a transaction from the connected wallet(open)
/studentconnected walletnot connected: /login; a registered issuer: /dashboard
/dashboardisIssuernot connected: /login; retired key: Retired key page; otherwise /student
/records/add, /records/importisIssuernot connected: /login; otherwise /records, which on v0.3 redirects to /dashboard and from there to /student
/students/invite, /records/typesisIssuer (v0.3 only)not connected: /login; on v0.2: /dashboard; otherwise /records
/adminaccessOf(...).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​

ActionVerifierHolderIssuerGuardianAdmin SafeTimelockOperator
Look up a credential and its status on /verifyYesYesYesYesYes
Check a document, package, verifiable credential or disclosure fileYesYesYesYesYes
Scan a phone code and send a challenge on /presentYesYesYesYesYes
Call the verification API or use the SDKYes, once the operator enables the APIYesYesYesEnables it
Open a document from a capability link on /openAnyone holding the linkYes
Prove access through the access gate (proveAccess)An admitted requester; blocked by a pause

Holding credentials​

ActionVerifierHolderIssuerGuardianAdmin SafeTimelockOperator
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 packageAnyone with the packageYes, from the dashboard
Share the document by share code or linkAnyone with the package
Create a selective disclosure fileYes (signs with the bound wallet)
Export a verifiable credential from the packageYesYes
Add a credential to a phone and sign a verifier's challengeYes
Admit or remove access group members (admit, removeMember)The bound holder walletYes (admit needs a registered issuer)

Issuing and managing credentials​

ActionVerifierHolderIssuerGuardianAdmin SafeTimelockOperator
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 credentialYes, also while pausedSuspend as an admin holdSame, after handover
Lift an admin holdYesAfter 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 cohortYes
Replace a document key, remove old stored copiesYes, with a sign-in session

Running the system​

ActionVerifierHolderIssuerGuardianAdmin SafeTimelockOperator
Pause new issuance (IssuerRegistry.pause)YesYesAfter handover
UnpauseOn sets from contract version 0.5.0YesAfter handover
Suspend an issuer (setIssuerSuspended(issuer, true, ...))YesYesAfter handover
Lift an issuer suspensionYes (StatusRegistry admin)After handover
Add, remove or rename an issuerYesAfter handoverProposes through the Safe
Retire an issuer key in favour of a successorYes (0.5.0 sets)After handover
Grant or revoke the guardian roleYesAfter handover
Begin, accept or cancel an admin transferYes (the nominee accepts)Yes, as nominee
Schedule or cancel a timelock operationYes (proposer and canceller)
Execute a ready timelock operationAnyoneAnyoneAnyoneAnyoneAnyoneAnyone
Announce a contract set (VersionAnnouncer.announce)The announcer's admin
Deploy contracts, set server keys, run the monitor, repair pinsYes

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.ts
  • src/components/layout/navigation.ts, src/components/layout/Header.tsx
  • src/app/login/page.tsx and the page gates under src/app/
  • src/lib/server/issuer-session.ts
  • blockchain/contracts/v03/IssuerRegistry.sol, StatusRegistry.sol, CredentialRegistry.sol, HolderRegistry.sol, RecordTypeRegistry.sol, CertichainTimelock.sol, VersionAnnouncer.sol
  • blockchain/contracts/zk/CertichainAccessGate.sol

Tests​

  • src/components/layout/navigation.test.ts, src/components/layout/Header.test.tsx
  • src/services/domain/issuer-access-service.test.ts
  • blockchain/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