A PoC in one session
I built a Go proof of concept of a notary service. It reviews claims about companies and signs the approved ones as Verifiable Credentials. It came from one prompt and a folder of design docs, and the day ended with it pushed to GitLab.
What runs now
- The PoC. One Go process has a package for each service in the container diagram. It has no external dependencies and keeps all state in memory.
- A console with a live sequence diagram. You start a use case and watch every hop between the services, from the first request to the signed credential.
- Nine use cases as end-to-end tests over real mTLS. Each one checks acceptance criteria and invariants from the docs. They range from a claim approved by machine to a burst of 12 claims at once.
- A Dutch console. Buttons, hints, narration and checks are in Dutch; glossary terms stay in English.
One real claim, step by step, from the PoC’s own trace
outside the notary
- notaryclient 136
- relyingparty 239
- registry 139
- portal 458
the notary service, one Go process
- gateway 375
- claimsvc 211
- store 131
- bus 181
- engine 159
- agents 167
- backoffice 113
- signing 405
- hsm 67
- audit 89
- model 375
around it, the harness, no steps
- cmd/[project] 52
- notary 134
- demo 1 220
- trace 149
- pki 110
- jose 118
A Dataspace asks the notary to attest a KvK registration. A review agent checks it against Registry X, the engine approves, the HSM signs, and a relying party checks the VC.
submit
- 1notaryclientA Dataspace, dataspace-logistics, sends
POST /v1/claimsover mTLS.with gateway - 2gatewayThe gateway checks its client certificate, role and rate limit, and forwards the claim.with claimsvc
- 3claimsvcThe claim service validates the evidence and saves the claim as
pending, with its first messages in the outbox.with store - 4gatewayThe gateway answers
202 pending. The claim exists before the reply.with claimsvc and notaryclient
review
- 5busThe bus carries
claim.pendingfrom the claim service to the review engine.with claimsvc and engine - 6engineThe review engine looks up the rules for a KvK registration: machine review. It sends
review.route.with bus - 7claimsvcThe claim service moves the claim to
machine_reviewing. No other package writes claim state.with bus and store - 8engineThe review engine reads the evidence and asks for a KvK review.with bus and store
- 9agentsA KvK agent looks up company 12345678 in Registry X, finds Acme Hosting B.V., active, and sends its judgement.with bus and registry
- 10engineThe review engine weighs the score: 0.97, at least 0.85, so approve. It sends
review.decided.with bus - 11claimsvcThe claim service moves the claim to
approved.with bus and store
sign
- 12signingThe signing service reads the approved claim, reserves its place in the status lists and builds a VC 2.0.with bus and store
- 13hsmThe HSM signs it with ES256.with signing
- 14claimsvcOn
claim.signedthe claim service moves the claim tosigned, thenactivated.with signing, bus and store
use
- 15notaryclientThe Dataspace asks again and gets
200 activated, with the signed VC.with gateway, claimsvc and store - 16relyingpartyA relying party fetches the issuer's did:web and both status lists through the gateway: the signature holds, not revoked, not suspended.with gateway
The 60 trace lines behind these steps
1 dataspace → gateway POST /v1/claims
2 gateway mTLS CN=dataspace-logistics role=dataspace ✓, rate limit ✓
gateway → claim-service forward submitClaim(KVK_REGISTRATION)
3 claim-service validate KVK_REGISTRATION evidence: ok
claim-service → state-store INSERT claim (pending) + outbox [claim.created, claim.pending]
state-store → claim-service committed v1
4 claim-service → gateway claim clm_e33a64f0025a pending
gateway → dataspace 202 status=pending
5 claim-service → message-bus publish claim.created
claim-service → message-bus publish claim.pending
message-bus → review-engine consume claim.pending
6 review-engine rules[KVK_REGISTRATION]: route=machine
review-engine → message-bus publish review.route
7 message-bus → claim-service consume review.route
claim-service → state-store UPDATE state pending → machine_reviewing WHERE version=1
state-store → claim-service committed v2
claim-service → message-bus publish claim.state_changed
8 message-bus → review-engine consume claim.state_changed (machine_reviewing)
review-engine → state-store SELECT claim evidence
state-store → review-engine KVK_REGISTRATION evidence
review-engine → message-bus publish review.requested.KVK_REGISTRATION
9 message-bus → review-agent consume review.requested.KVK_REGISTRATION
review-agent → registry Registry X: GET /companies/12345678
registry → review-agent 200 "Acme Hosting B.V." active=true
review-agent → message-bus publish claim.judgement
10 message-bus → review-engine consume claim.judgement
review-engine score 0.97 ≥ approve threshold 0.85 → approve
review-engine → message-bus publish review.decided
11 message-bus → claim-service consume review.decided
claim-service → state-store UPDATE state machine_reviewing → approved WHERE version=2
state-store → claim-service committed v3
claim-service → message-bus publish claim.state_changed
12 message-bus → signing-service consume claim.state_changed (approved)
signing-service → state-store SELECT approved claim
state-store → signing-service approved KVK_REGISTRATION
signing-service allocate status index 46017 in shard 1
signing-service build VC 2.0 payload ([VerifiableCredential KvkRegistrationCredential])
13 signing-service → hsm sign(sha256:d1b71aae15cc…, purpose=vc-issuance)
hsm → signing-service ES256 signature (#3)
14 signing-service → message-bus publish claim.signed
message-bus → claim-service consume claim.signed
claim-service → state-store UPDATE state approved → signed WHERE version=3
state-store → claim-service committed v4
claim-service → state-store UPDATE state signed → activated WHERE version=4
state-store → claim-service committed v5
claim-service → message-bus publish claim.state_changed
claim-service → message-bus publish claim.state_changed
15 dataspace → gateway GET /v1/claims/clm_e33a64f0025a
gateway mTLS CN=dataspace-logistics role=dataspace ✓, rate limit ✓
gateway → claim-service forward getClaim
claim-service → state-store SELECT claim clm_e33a64f0025a
state-store → claim-service state=activated → external activated
claim-service → gateway activated
gateway → dataspace 200 status=activated
16 dataspace → gateway GET /.well-known/did.json
gateway → dataspace 200 OK
dataspace → gateway GET /v1/credentials/status/1
gateway → dataspace 200 revocation list shard 1 (803 bytes)
dataspace → gateway GET /v1/credentials/status/suspension/1
gateway → dataspace 200 suspension list shard 1 (833 bytes)A Dataspace submits an ISO 27001 certificate that expires in 12 days. The agent is unsure, so a Verifier decides in the backoffice.
submit
- 1notaryclientA Dataspace, dataspace-energy, sends
POST /v1/claimswith an ISO 27001 certificate.with gateway - 2gatewayThe gateway checks its client certificate, role and rate limit, and forwards the claim.with claimsvc
- 3claimsvcThe claim service validates the evidence and saves the claim as
pending.with store - 4gatewayThe gateway answers
202 pending.with claimsvc and notaryclient
review
- 5busThe bus carries
claim.pendingto the review engine.with claimsvc and engine - 6engineThe rules for ISO 27001 say machine review first. The engine sends
review.route.with bus - 7claimsvcThe claim service moves the claim to
machine_reviewing.with bus and store - 8engineThe review engine reads the evidence and asks for an ISO 27001 review.with bus and store
- 9agentsThe ISO 27001 agent finds the certificate in Registry Y, valid until 2026-11-21, and sends its judgement.with bus and registry
- 10engineScore 0.60, between 0.25 and 0.85: the engine leaves it to a Verifier.with bus
- 11claimsvcThe claim service moves the claim to
human_reviewing.with bus and store
decide
- 12notaryclientVerifier alice opens the claim through the gateway; the backoffice shows it with the agent's judgement.with gateway, backoffice and store
- 13backofficeShe approves. The backoffice sends her judgement as
claim.judgementand answers202. It writes no claim state.with notaryclient, gateway, store and bus - 14engineThe review engine takes the Verifier's judgement: approve.with bus
- 15claimsvcThe claim service moves the claim to
approved.with bus and store
sign
- 16signingThe signing service reserves its place in the status lists and builds the VC.with bus and store
- 17hsmThe HSM signs it with ES256.with signing
- 18claimsvcThe claim service moves the claim to
signed, thenactivated.with signing, bus and store
The 67 trace lines behind these steps
1 dataspace → gateway POST /v1/claims
2 gateway mTLS CN=dataspace-energy role=dataspace ✓, rate limit ✓
gateway → claim-service forward submitClaim(ISO_27001)
3 claim-service validate ISO_27001 evidence: ok
claim-service → state-store INSERT claim (pending) + outbox [claim.created, claim.pending]
state-store → claim-service committed v1
4 claim-service → gateway claim clm_7a7f9c98f580 pending
gateway → dataspace 202 status=pending
5 claim-service → message-bus publish claim.created
claim-service → message-bus publish claim.pending
message-bus → review-engine consume claim.pending
6 review-engine rules[ISO_27001]: route=machine
review-engine → message-bus publish review.route
7 message-bus → claim-service consume review.route
claim-service → state-store UPDATE state pending → machine_reviewing WHERE version=1
state-store → claim-service committed v2
claim-service → message-bus publish claim.state_changed
8 message-bus → review-engine consume claim.state_changed (machine_reviewing)
review-engine → state-store SELECT claim evidence
state-store → review-engine ISO_27001 evidence
review-engine → message-bus publish review.requested.ISO_27001
9 message-bus → review-agent consume review.requested.ISO_27001
review-agent → registry Registry Y: GET /certificates/ISO-27001-NORDIC-0042
registry → review-agent 200 ISO_27001 by Kiwa Nederland B.V., valid until 2026-11-21
review-agent → message-bus publish claim.judgement
10 message-bus → review-engine consume claim.judgement
review-engine score 0.60 between 0.25 and 0.85 → escalate to Verifier
review-engine → message-bus publish review.decided
11 message-bus → claim-service consume review.decided
claim-service → state-store UPDATE state machine_reviewing → human_reviewing WHERE version=2
state-store → claim-service committed v3
claim-service → message-bus publish claim.state_changed
12 verifier → gateway GET /v1/backoffice/claims/clm_7a7f9c98f580
gateway mTLS CN=alice role=verifier ✓, rate limit ✓
backoffice → state-store SELECT claim clm_7a7f9c98f580
state-store → backoffice claim with judgements
gateway → verifier 200 OK
13 verifier → gateway POST /v1/backoffice/claims/clm_7a7f9c98f580/judgement
gateway mTLS CN=alice role=verifier ✓, rate limit ✓
gateway → backoffice forward judgement: approve
backoffice → state-store SELECT claim clm_7a7f9c98f580
state-store → backoffice claim with judgements
backoffice → message-bus publish claim.judgement
backoffice → gateway judgement dispatched
gateway → verifier 202 Accepted
14 message-bus → review-engine consume claim.judgement
review-engine Verifier judgement: approve
review-engine → message-bus publish review.decided
15 message-bus → claim-service consume review.decided
claim-service → state-store UPDATE state human_reviewing → approved WHERE version=3
state-store → claim-service committed v4
claim-service → message-bus publish claim.state_changed
16 message-bus → signing-service consume claim.state_changed (approved)
signing-service → state-store SELECT approved claim
state-store → signing-service approved ISO_27001
signing-service allocate status index 43728 in shard 1
signing-service build VC 2.0 payload ([VerifiableCredential Iso27001Credential])
17 signing-service → hsm sign(sha256:bcc667a821ac…, purpose=vc-issuance)
hsm → signing-service ES256 signature (#4)
18 signing-service → message-bus publish claim.signed
message-bus → claim-service consume claim.signed
claim-service → state-store UPDATE state approved → signed WHERE version=4
state-store → claim-service committed v5
claim-service → state-store UPDATE state signed → activated WHERE version=5
state-store → claim-service committed v6
claim-service → message-bus publish claim.state_changed
claim-service → message-bus publish claim.state_changedA KvK claim, approved and activated as in the first walk, and checked as valid by a relying party, is revoked by a Verifier. The same VC then fails, and the revocation can't be undone.
revoke
- 1notaryclientVerifier alice asks to revoke the claim, through the gateway.with gateway and backoffice
- 2backofficeThe backoffice reads the claim, sends
claim.lifecycle_requestedand answers202.with store, bus, gateway and notaryclient - 3claimsvcThe claim service moves the claim from
activatedtorevoked.with bus and store - 4signingThe signing service sets bit 67764 in revocation list shard 1.with bus
- 5hsmThe HSM signs the new list; the signing service announces
statuslist.updated.with signing and bus
after
- 6notaryclientThe Dataspace asks for the claim and gets
200 revoked.with gateway, claimsvc and store - 7relyingpartyA relying party checks the same VC it found valid before the revocation: now it fails.with gateway
- 8backofficeAlice tries to reinstate it. The backoffice refuses with
409: a revoked claim never changes state again.with notaryclient, gateway and store
The 38 trace lines behind these steps
1 verifier → gateway POST /v1/backoffice/claims/clm_72da9e6477b8/lifecycle
gateway mTLS CN=alice role=verifier ✓, rate limit ✓
gateway → backoffice forward lifecycle: revoke
2 backoffice → state-store SELECT claim clm_72da9e6477b8
state-store → backoffice claim with judgements
backoffice → message-bus publish claim.lifecycle_requested
backoffice → gateway revoke requested
gateway → verifier 202 Accepted
3 message-bus → claim-service consume claim.lifecycle_requested
claim-service → state-store UPDATE state activated → revoked WHERE version=5
state-store → claim-service committed v6
claim-service → message-bus publish claim.state_changed
4 message-bus → signing-service consume claim.state_changed (revoked)
signing-service revocation list shard 1: bit 67764 = 1
5 signing-service → hsm sign(sha256:4b1d4d6b1807…, purpose=status-list)
hsm → signing-service ES256 signature (#6)
signing-service → message-bus publish statuslist.updated
6 dataspace → gateway GET /v1/claims/clm_72da9e6477b8
gateway mTLS CN=dataspace-logistics role=dataspace ✓, rate limit ✓
gateway → claim-service forward getClaim
claim-service → state-store SELECT claim clm_72da9e6477b8
state-store → claim-service state=revoked → external revoked
claim-service → gateway revoked
gateway → dataspace 200 status=revoked
7 dataspace → gateway GET /.well-known/did.json
gateway → dataspace 200 OK
dataspace → gateway GET /v1/credentials/status/1
gateway → dataspace 200 revocation list shard 1 (817 bytes)
dataspace → gateway GET /v1/credentials/status/suspension/1
gateway → dataspace 304 Not Modified
8 verifier → gateway POST /v1/backoffice/claims/clm_72da9e6477b8/lifecycle
gateway mTLS CN=alice role=verifier ✓, rate limit ✓
gateway → backoffice forward lifecycle: reinstate
backoffice → state-store SELECT claim clm_72da9e6477b8
state-store → backoffice claim with judgements
backoffice refused: cannot reinstate a claim in revoked; a revoked claim never changes state again (INV-trust-core-3, INV-trust-core-12)
backoffice → gateway 409 Transition not allowed
gateway → verifier 409 Transition not allowedWho takes part, with lines of Go
outside the notary
- notaryclient136 lineshow a Dataspace or Verifier calls the notary
- relyingparty239 lineschecks a VC the way anyone shown one would
- registry139 linesmock trade and certificate registers
- portal458 lineswhere Customers get claims attested
the notary service, one Go process
- gateway375 linesthe single way in
- claimsvc211 linesthe only package that writes claim state
- store131 linesclaims, with a version per change
- bus181 linescarries messages between the services
- engine159 linesroutes a claim, turns a score into a decision
- agents167 lineseach judges one claim type
- backoffice113 lineswhere Verifiers decide
- signing405 linesthe only package that signs
- hsm67 linesholds the signing key
- audit89 lineshears every message and chains them
- model375 linesclaims, states and messages, shared by all
around it, the harness, no steps
- cmd/[project]52 linesstarts it all
- notary134 lineswires the services into one process
- demo1 220 linesruns the 9 scenarios and the console
- trace149 linesrecords every hop; the steps come from it
- pki110 linesmakes the certificates
- jose118 linesbuilds and checks ES256 tokens
Five choices and why
One service changes claim state. Only the Claim Service writes state; the other services send it messages over an in-process bus. With three writers, no single place could enforce the rules. Not picked: letting the Review Engine or the Backoffice change state themselves.
Real mTLS, checked in the gateway. The PoC makes its own CA and certificates at start-up, and the portal calls the notary like any outside party. A missing certificate gets a clear 401 instead of a failed handshake. Not picked: a fake auth header, which hides half of the security cases.
The use cases are the tests. The same code drives the console and go test. A demo that passes is a test that
passes. Not picked: a separate end-to-end suite that drifts from the demo.
Where the docs are open, pick and write it down. Three design decisions were still drafts and four conflicts were open. The PoC picks an option for each, and the README lists every choice as undecided. Not picked: waiting for the decisions, which would have stopped the build on day one.
Dutch on screen, glossary terms in English. The docs require terms like Verifier, claim and Dataspace word for word. Text the notary returns, such as agent reasoning, stays English because it is API output. Not picked: translating only the HTML, which left the screen half Dutch, half English.
How I used Claude
- One prompt for the build. I wrote: "implement a poc where i can trigger claims and show the end-to-end process for a number of use cases. (golang is the preference)". Claude read the docs and the open conflicts before writing code.
- Repeated test runs found the bugs. Runs under the race detector exposed two ordering bugs. A status bit flipped before its re-signed list was published, and a credential could miss the verifier's judgement.
- The real binary found one more. A route conflict crashed it on start, because the tests never built the console's routes. Claude clicked through it in Chrome and added a smoke test.
- Skills set the style. One rewrote the README and hints for readers with ADHD: the action first, lists of at most 5. Another translated the console. When I called it without any text, I cancelled its question and named the file.
- I steered with short answers. "do that please" got the full translation. When the push failed because the group restricts private projects, I created the project and said "push again".
What I'd tell someone starting the same
- Give Claude the docs and the goal, not a task list. It found the open decisions on its own.
- Run the tests more than once, with
-race. Both ordering bugs passed on the first run. - Start the real binary once. Green tests didn't prove the server started.
Next
- Go through the open choices in the README with the architect.
- Decide whether the agents' reasoning in the verifier inbox should be in Dutch too.