Verification Web App
The verification web app is a lightweight React application served at verify.getveylent.com. Its sole purpose is to verify document signatures when a user scans the QR code on a signed PDF.
Design Goals
- Zero friction -- No app install, no account, no login
- Mobile first -- Optimized for phone screens (QR scanning)
- Client-side decryption -- The encryption key never leaves the browser
- Fast -- Minimal bundle size, single API call
How It Works
URL Routing
HTTPS://VERIFY.GETVEYLENT.COM/V/<anchor ID, 40 digits>#<key, 40 digits>
└────── path ────────┘ └── fragment ──┘
The React Router matches /v/:anchorB64 (case-insensitively, so /V/ too) and extracts both the path parameter and the URL fragment. Earlier links carried base64url parts instead of digits; both decode. See QR Code.
Verification Sequence
The page calls verifyAnchor from libs/shared/crypto. The mobile app (apps/verify) runs the same function.
QR code ──> Browser: URL with anchor ID + key fragment
Browser: decode anchor ID (40 digits or base64url -> 0x hex, 16 bytes)
Browser ──> API: GET /api/verify/0x<anchorId>
API ──> Chain: anchorBlock(anchorId), then DocumentAnchored event in that block,
then previousAnchorId back to the first signature
API ──> Browser: on-chain record + chain + encrypted payload
Browser: AES-128-GCM decrypt(key, payload)
Browser: require SHA-256(decrypted JSON) == compositeHash
and record.a == anchor ID
Browser: display signer info
The server is not trusted on its own. The record must decrypt with the key from the QR code, hash to the composite hash on the blockchain, and name the same anchor ID. Otherwise the page shows a mismatch and hides the details.
Security: The Fragment Stays Private
The #fragment portion of a URL is never sent to the server in HTTP requests. This is a fundamental property of URLs defined in RFC 3986. This means:
- The API server never sees the decryption key
- Network logs and proxies never capture the key
- Only the browser (and by extension, the user) has access to the key
Technology
| Aspect | Choice |
|---|---|
| Framework | React 19 |
| Bundler | Vite |
| Crypto | Web Crypto API (crypto.subtle) |
| Styling | Inline styles (no CSS framework) |
| Hosting | Any static file host |
Web Crypto API Requirement
The Web Crypto API requires a secure context (HTTPS). The verification page will not work over plain HTTP. In production, this is handled by serving from an HTTPS domain. For local development, a self-signed certificate is used via @vitejs/plugin-basic-ssl.
Verification States
The page displays one of these states:
Verified (Green)
The record decrypts, matches the on-chain composite hash, and names this anchor ID. Shows:
- Signer name, email, type, company, position, trust anchor
- Document hash, signing timestamp, geolocation
- Anchor ID, transaction hash, block time, chain length
The page notes that it can't check a copy of the document. That requires the desktop app.
Partial Verification (Yellow)
The blockchain anchor exists but the URL fragment (decryption key) is missing. This can happen if:
- The QR was manually typed without the fragment
- The URL was shared without the fragment
Shows: anchor ID, transaction hash, block time, chain length.
Doesn't Match the Blockchain (Red)
The record doesn't open with the key, doesn't hash to the on-chain composite hash, or belongs to another anchor ID. The details are not shown, because they can't be trusted.
No Such Signature (Red)
No anchor with this ID exists on the blockchain. The QR code may be damaged or not made by Veylent.
Couldn't Verify (Red)
The link is malformed, or the API couldn't be reached or returned an error. Shows the error message.
Bundle Size
The verification app is intentionally minimal:
- No PDF rendering library
- No state management library
- No CSS framework
- Total bundle: ~70 KB gzipped (mostly React)