Routes
This page lists every route of the web application in src/app/. A route is a folder with a page.tsx. Server routes under src/app/api/ are listed in Server routes.
Three ideas explain most of the table:
- Wallet-free pages. Verification pages (
/verify,/present,/open) read the chain over the public RPC endpoints and never need a wallet. See Reading the chain. - Role gates are client-side redirects. A gated page waits for the wallet session, asks the chain for the wallet's role through
services.issuerAccess, and redirects when the role does not fit. The redirect is a convenience, not a security boundary: every write is still checked by the contracts, and every server route checks its own session. - Version switch.
readContractVersion()picks the v0.3 or v0.2 behaviour. Some v0.2 routes redirect on v0.3, and some v0.3 routes redirect on v0.2.
How the app recognises a role
IssuerAccessService (src/services/domain/issuer-access-service.ts) answers accessOf(address) from the chain. A failed read throws; it is never treated as "no role".
| Check | v0.3 (IssuerRegistry of the active set) | v0.2 (AcademicRecords) |
|---|---|---|
isIssuer | isIssuer(address) is true and the key is not retired (lineage read) | hasRole(UNIVERSITY_ROLE, address) |
isAdmin | isGuardian(address) | hasRole(ADMIN_ROLE, address) |
isSuperAdmin | always false | hasRole(SUPER_ADMIN_ROLE, address) |
On v0.3 the Admin workspace belongs to the guardian. Registering issuers is done by the admin Safe (and, after the handover, through the timelock), not through a page of this app. See Guardian and Admin.
Signing in
/login connects the wallet and sends the user to the workspace that fits the role.
This is not a server sign-in. The server session used by upload and document routes is created separately, on demand, when an issuer first uploads; see Sign-in and sessions.
Route table
"Wallet" means a connected wallet is required. "Redirect" lists where a gated page sends a visitor who does not fit.
Public pages
| Route | Who sees it | v0.3 | v0.2 | Purpose | Main components |
|---|---|---|---|---|---|
/ | Anyone | Same page | Same page, v0.2 copy | Home page | Hero, TrustSteps, Audiences, TrustBoundaries, ClosingCall with copy from homeCopy(version) in src/components/home/home-copy.ts |
/about | Anyone | Same page | Same page, v0.2 chapters | What Certichain is and where data lives | About with chaptersFor(version) from src/app/about/chapters.ts |
/login | Anyone | Same | Same | Connect a wallet and go to the right workspace | src/app/login/page.tsx |
/verify | Anyone, no wallet | Verify a credential: look up by id (?id= is read on load), check a document, a package, a VC or Open Badge file, a disclosure | Legacy lookup | Verification | v0.3 CredentialVerifyContent (with DocumentCheckPanel, VcCheckPanel, DisclosurePanel, HolderDetailsPanel, IssuerSignatureStatus, CredentialStatusBanner, ManageCredential); v0.2 VerifyPageContent |
/present | Anyone, no wallet | Check a credential: scan a holder's phone code with the camera or a picture, or issue a challenge | Same page | In-person verification | PresentContent, ChallengePanel, VerdictView, QrScanner |
/wallet | Anyone (the holder's phone) | Credentials on this phone: add, list and present credentials stored on the device | Same page | Phone wallet | WalletContent, AddCredentialPanel, CredentialCard, SignForVerifierPanel |
/open | Anyone with a capability link | Open a shared document | Same page | Opens a document from a link whose key is in the URL fragment | OpenLinkContent |
/access | Anyone; a wallet is needed to derive an identity from signatures and to send a proof | Prove access to a credential; shows Not on this network yet when no access gate is recorded for the chain | Same page | Zero-knowledge access requests | AccessRequestContent, AccessRequestView, IdentitySection, ProveSection |
/privacy-policy | Anyone | Same | Same | Legal text | LegalDocument |
/terms-conditions | Anyone | Same | Same | Legal text | LegalDocument |
Notes:
/opencopieswindow.location.hashinto memory and immediately removes it from the address bar withhistory.replaceState, so the key is not left in the visible address or history entry. The route is served withReferrer-Policy: no-referrerandCache-Control: no-store(see Security controls)./presentand/walletare served withReferrer-Policy: no-referrerandPermissions-Policy: camera=(self). On/wallet, SignForVerifierPanel is the only part that needs a wallet; without one it says "Signing needs a wallet. Open this page inside your wallet app's browser, then try again."- The legal pages show a fixed "last updated" date.
Holder pages
| Route | Who sees it | v0.3 | v0.2 | Redirect | Main components |
|---|---|---|---|---|---|
/student | Wallet, not an issuer | My credentials (eyebrow Holder): accept an invitation, list credentials from every contract set, open and share documents, make a disclosure, export a VC or Open Badge, manage access | My records (eyebrow Student): records issued to the wallet and records shared with it | No wallet: /login. An issuer: /dashboard | v0.3 AcceptInvitationPanel, MyCredentialsPanel, OpenDocumentPanel, SharedDocumentPanel, MyShareCodePanel, MakeDisclosurePanel, VcExportPanel; v0.2 LegacyStudentView |
Issuer pages
| Route | Who sees it | v0.3 | v0.2 | Redirect | Main components |
|---|---|---|---|---|---|
/dashboard | Issuer | Issuer dashboard: issued credentials from every set, revoke, re-issue, open document, replace a document key, add a disclosure tree, export, access management. A retired key sees Retired key and the successor's address | University dashboard: issued records | No wallet: /login. v0.3 non-issuer: /student (unless retired). v0.2 non-university: /records. A failed role read shows Could not check your issuer registration. | v0.3 IssuerDashboardPage, IssuedCredentialsPanel, IssuerActions, RevokeDialog, ReplaceKeyFlow, AddDisclosurePanel, IssuerAccessSection; v0.2 LegacyDashboardPage |
/records/add | Issuer | Add Academic Record (eyebrow Issue New Record, or Re-issue Record with ?replaces=): encrypt and upload the document, sign, anchor | Add a v0.2 record | No wallet: /login. Non-issuer: /records | IssueSession (in Suspense with IssueSkeleton), AddRecordPage, DocumentUploadField, StudentLinkCheck, DeliveryOption, CredentialIssuedCard, SendToPhone |
/records/import | v0.3 issuer | Import credentials from CSV or JSON | Redirects to /dashboard | No wallet: /login. Non-issuer: /records | ImportPage, ImportWorkspace, ImportReview, ImportProgress, ImportResults |
/records/types | v0.3 issuer | Record types: register and switch off the issuer's own types | Redirects to /dashboard | No wallet: /login. Non-issuer: /records | RecordTypesPanel |
/students/invite | v0.3 issuer | Students: create invitations and manage wallet bindings | Redirects to /dashboard | No wallet: /login. Non-issuer: /records | InviteStudentPanel, ManageStudentPanel |
/records | v0.2 | Redirects to /dashboard (RedirectTo) | Records list | v0.2 university: /dashboard | LegacyRecordsPage |
/records/[id] | v0.2 | Redirects to /verify?id=<id> (v03RecordTarget) | Record detail | No wallet: /login | LegacyRecordDetailPage |
On v0.3 a holder who lands on /records/add is sent to /records, which redirects to /dashboard, which sends a non-issuer to /student. The chain of redirects ends on the right workspace without a dedicated rule.
Admin pages
| Route | Who sees it | v0.3 | v0.2 | Redirect | Main components |
|---|---|---|---|---|---|
/admin | v0.3 guardian; v0.2 admin or super admin | Governance (section Platform governance, read from the chain when the page opened): Running or Paused, registry admins and pending admin transfers, the timelock queue (read-only), retired keys, and Pause the system while the system runs. A note points admin actions (adding or removing an issuer, restarting after a pause, lifting an issuer suspension, accepting an admin transfer) to the Safe. A failed read shows Could not read the governance state. | Legacy admin: universities, admins (super admin only), students, custom record types, pause | No wallet or no role: /login | v0.3 GovernancePage, GovernancePanel, TimelockSection, RetiredKeysSection; v0.2 LegacyAdminPage and src/app/admin/legacy/* |
Pause notice
Issuer pages on v0.3 read platform.isPaused() through usePlatformPaused(version) (src/services/react/use-platform-paused.ts). While the issuer registry is paused they show PausedNotice: "The system is paused. New credentials, invitations and record types cannot be created right now. Revoking, suspending and other corrections still work." A failed read leaves the notice hidden and logs the error; the contracts refuse paused writes anyway.
Files served with the pages
| Path | What it is |
|---|---|
/manifest.webmanifest | Web app manifest for installing the phone pages |
/sw.js | Offline shell service worker, served with Cache-Control: no-cache so a fixed worker reaches phones on the next visit |
/zk/* | Circuit files for the browser prover, copied by npm run zk:public and checked against src/lib/zk/circuit-hashes.json |
/api/* | Server routes, see Server routes |
Source files
src/app/*/page.tsxand the components next to each pagesrc/app/login/page.tsxsrc/app/records/records-routing.tssrc/app/records/add/use-issuer-setup.ts,src/app/admin/legacy/use-legacy-admin.tssrc/services/domain/issuer-access-service.tssrc/services/react/use-wallet-session.ts,src/services/react/use-platform-paused.tssrc/components/RedirectTo.tsx,src/components/PausedNotice.tsxsrc/lib/security-headers.ts,next.config.ts
Tests
src/services/domain/issuer-access-service.test.tssrc/components/RedirectTo.test.tsx,src/components/PausedNotice.test.tsx- Page tests next to their pages, for example
src/app/student/StudentPageV03.test.tsx,src/app/verify/CredentialVerifyContent.test.tsx,src/app/open/OpenLinkContent.test.tsx,src/app/present/PresentContent.test.tsx,src/app/wallet/WalletContent.test.tsx - Browser journeys in
e2e/journeys/, for exampleissue-and-verify.journey.ts,mobile-wallet.journey.ts,bulk-import.journey.ts,pause-and-restart.journey.ts