[Digital Certificate Wallet] Advanced: Building "Visitor-Endorsed Issuance" β A Full-Chain DID Application as Both Verifier and
Employee Access Control + Visitor Endorsement and Issuance
This lobby reception desk has two modes:
Employee Access Control / Event Registration: Employees present their employee cards using a digital wallet. The system only verifies "whether they are a valid employee." Once verified, the door opens or registration is successful. Fields like name, birthday, and number of children are not disclosed and remain in the wallet-this is selective disclosure.
Visitor Endorsement and Issuance (the protagonist of this article): An active employee presents their employee card to "endorse" the visitor. After verification, the system immediately issues a temporary visitor pass with an expiration time to the visitor's wallet.
The value of the second mode lies in connecting the two roles: First act as a Verifier (verify employee card) β then act as an Issuer (issue visitor card) only after verification. Compared to traditional paper visitor logs (copying IDs, holding physical IDs, piles of personal data at the counter requiring manual disposal), digital endorsement only leaves one piece of accountable information: "which employee endorsed it." Visitor data stays in the visitor's own wallet, and the pass can have an expiration time.
Architecture Decisions: Why not just modify the previous project?
This time, I started a brand new project and deployed it to a separate Cloud Run service instead of adding pages to the original HR project. Several considerations:
Static Frontend + JSON API: The original project used jade templates for server-side rendering. This time, it was changed to
public/static pages + a few JSON APIs (/api/access/qrcode,/api/access/status), separating the frontend and backend more cleanly.Abstracting Wallet API calls into
lib/wallet.js: In the original project, issuer/verifier calls were embedded in routes and duplicated. This time, they were extracted into three functions:requestPresentationQRCode(),getPresentationResult(), andissueCredential(), making it easier to maintain and test.State changed to Memory: The original project wrote data to a single
record.jsfile. Writing files in a stateless environment like Cloud Run causes issues. This time, a simple memory object was used (resets on restart, sufficient for demonstration purposes).Reusing the same Sandbox Account tokens: The access tokens for the issuer/verifier are the same set as in the previous article, reused directly. For the "holding an employee card" verification part, I first used the existing sports subsidy verifier ref as a fallback (if
VERIFIER_ACCESS_REFis not set, useVERIFIER_SPORT_REF), so it could run without waiting for backend configuration.
Pitfall Record 1: Presentation successful, but the screen is stuck
This is a classic one. The phone scans the code, and the wallet completes the presentation, but the desktop page just won't move forward; it keeps polling.
First, checking the Cloud Run logs, I found that /api/access/status returns every 3 seconds, each time returning "unverified". I added a log line on the backend to print the original response from the verifier. After redeploying and testing again, I caught the truth:
{
"data": [
{
"credentialType": "0028680530_line_school",
"claims": [
{ "ename": "ename", "cname": "English Name", "value": "Lub" },
{ "ename": "join_company", "cname": "Join Date", "value": "2018-10-05" }
]
}
],
"verifyResult": true,
"resultDescription": "success",
"transactionId": "8cd7f37b-..."
}
See the problem? The field in the response is verifyResult (camelCase), and there is no code field at all. But I used the old logic from the previous article:
// Old (doesn't match current response)
const verified = data.code === 0 && data.verify_result === true;
data.code is undefined, and data.verify_result is also undefined (it's called verifyResult), so it's always false, always pending. Actually, the verification had already succeeded (verifyResult: true, resultDescription: "success"), but the field names I was checking didn't match-it seems the sandbox API response format has changed from snake_case to camelCase.
The fix was to change the logic to be compatible with both formats:
const verified =
data.verifyResult === true || // New format camelCase
data.verify_result === true || // Old format compatibility
(data.code === 0 && data.verify_result === true);
TIL: When integrating third-party APIs, don't trust that "the logic that worked in the last version will work in this one." Sandboxes change. Adding a log line to print the raw response and comparing it is much faster than staring at the code and guessing.
Pitfall Record 2: Visitor card stuck at "Pending Issuance"
After the access control part worked, the visitor endorsement part got stuck-the screen showed "Visitor card pending issuance (issuer template not set)," and no card was actually issued.
I used curl to hit the issuance API /api/vc-item-data directly to see what it returned. I tested two scenarios:
Scenario A: Using employee template + correct employee fields β HTTP 200, and the full response contained these keys:
KEYS: ['id', 'content', 'pureContent', ..., 'qrCode', 'deepLink', 'expired', ...]
qrCode = data:image/png;base64,iVBOR... β QR code that can actually be scanned into the wallet
deepLink = https://frontend-uat.wallet.gov.tw/api/moda/vcqrcode?...
expired = 2027-01-09T...
Scenario B: Using employee template + visitor fields (visitor_type, endorsed_byβ¦) β HTTP 500 / 400 BAD_REQUEST.
The reason was clear: the employee template fields were isRequired: true (Name, English Nameβ¦), but I sent a bunch of visitor fields it didn't have, so it was rejected. And the successful issuance response actually includes qrCode and deepLink, which can be used directly for the visitor to scan and collect the card-my original parsing was correct; the bottleneck was purely "fields not matching the template."
So I designed two issuance modes, automatically switched by environment variables (HAS_VISITOR_TEMPLATE in the code):
| Mode | Condition | Behavior | Card Face |
|---|---|---|---|
| Option 1 (fallback) | VISITOR_VC_* not set |
Borrow the employee template, stuffing visitor info into its required fields (Name="Temporary Visitor", etc.) to issue the card | Displays as an employee card face |
| Option 2 (Formal) | VISITOR_VC_* set |
Send visitor_type / endorsed_by / valid_until to the dedicated visitor template |
Formal visitor pass card face |
The advantage of Option 1 is that a real, collectable card can be issued without waiting for backend configuration (even if the card face is borrowed), allowing the entire chain to be tested first; for a formal card face, just go with Option 2 and build a dedicated template, with no code changes required.
Pitfall Record 3: Collection QR too small + Mobile layout
In the first version, I made the visitor pass look like a pretty little ID badge, and the collection QR was only 48px-the result was that it couldn't be scanned at all. This QR is meant to be scanned by "another phone" to collect the card; if it's too small, it loses its purpose.
Later, I changed the visitor card to a vertical layout, enlarging the collection QR to be the main body of the card (max 240px, white background with padding), with "Endorsed by / Valid until" information placed below. Both QRs (for presentation and collection) were also changed to clamp() responsive sizes, so they don't break the layout on mobile and are clear enough on desktop.
TIL: As long as a QR is "for others to scan," it must be treated as the protagonist of the layout, not as a decoration.
The Truth About "Automatic Expiration"
I originally thought I could specify "this visitor pass expires in 4 hours" for each card, but testing revealed that when issuing cards via /api/vc-item-data, the actual validity of the card follows the template settings (e.g., the employee template is issuance date + about half a year); it's not possible to specify a short expiration for individual cards.
So the "Valid until HH:MM" on the card face now is a display value calculated by the application layer, not a mandatory expiration enforced by the wallet. If a truly short-term visitor pass is needed, there are two ways:
- When creating the visitor template, set the template's validity period to be short.
- Or use the platform's scheduled revocation (revoke)-the issuance response includes fields like
clearScheduleIdandscheduleRevokeMessage, implying the platform supports scheduled revocation, but this requires integrating the corresponding API separately.
Deployment: Directly to Cloud Run from Source Code
This time, I used buildpacks to deploy directly from source code without writing a Dockerfile:
gcloud run deploy did-usecase-visitor \
--source=. --region=asia-east1 --platform=managed --allow-unauthenticated \
--set-env-vars="VC_SERNUM=607861,VC_UID=0028680530_line_school, \
ISSUER_ACCESS_TOKEN=...,VERIFIER_SPORT_REF=...,VERIFIER_ACCESS_TOKEN=..., \
VISITOR_TTL_HOURS=4"
To switch to the formal visitor card in Option 2 later, just add VISITOR_VC_SERNUM=<new template vcId>,VISITOR_VC_UID=<new template vcCid> to this --set-env-vars string and redeploy; HAS_VISITOR_TEMPLATE will automatically become true.
Summary and Future Outlook
The focus this time wasn't "making another demo," but three things:
The full DID chain is feasible: Playing both Verifier and Issuer in the same scenario-verifying one card and then issuing another-connects the ecosystem chain, and the experience is smooth.
Pitfalls are in the details: Field naming (
verifyResultvsverify_result), mandatory template fields, QR size-these wouldn't be discovered without looking at the raw response and actually scanning with a phone.Fallback design allows the demo to work first: No need to wait for every template/ref to be built on the backend; use existing resources to get the whole chain running first, then gradually switch to formal settings. The development rhythm is much better.
There are truly many application scenarios for digital certificate wallets; "visitor endorsement" is just one of them. The employee card from the previous article could also be extended to commissary discount redemption, seniority milestone gifts, childcare facility access, gym point accumulation... each is a new application for a "verifier." I look forward to seeing more creative scenarios being built.
Comments
No comments yet. Start the discussion.