Separate four contracts
- Email href
- A message for a person should open an HTML page, not a JSON representation intended for an API client.
- Page retrieval
- Loading or previewing the page should be safe. It should not silently consume a single-use capability.
- Verification mutation
- One deliberate action can submit the token and classify success, already handled, expired, invalid, or unresolved.
- Product continuation
- Email verified can mean active, or it can mean a separate domain-ownership gate remains.
Browser-local landing planner
Choose the first repair boundary
No email address, organization ID, verification token, full action URL, API response, cookie, authorization header, provider payload, database row, or raw log is requested or transmitted.
- First boundary
- Decision
- Next action
- Exit condition
- Safety rule
What the reviewed target actually does
At reviewed NANDA Index commit 5b8b0d8, sendVerificationEmail builds its href from oauth.callbackBaseUrl plus /api/v1/verify-email?token=.... The same file correctly builds the password-reset href from frontendUrl plus a real page. The mismatch is therefore visible in one source file.
The open nanda-index-v2 issue 17 reports that clicking the message verifies the address and displays the serialized index record. That observation matches the Fastify route: it performs the verification mutation in a GET handler and returns INDEX_RECORD_SCHEMA as JSON.
Do not preserve the mutation-on-GET contract
A frontend page is necessary, but a page that automatically calls the same GET endpoint on load still leaves the one-time action coupled to retrieval. RFC 9110 section 9.2.1 defines GET as a safe, essentially read-only method so crawlers, link checkers, and prefetchers can retrieve it without causing harm. A verification endpoint that clears a token is not read-only.
Email href
https://nandaindex.org/verify-email?token=...
GET /verify-email
render HTML only; do not mutate
POST /api/v1/verify-email
consume the token once
return only the safe fields needed for the next state
Success
replace the token-bearing URL
render active or pending-domain continuation
The planner on this page never receives a real token. The example uses an ellipsis as a placeholder and makes no network request.
Design repeat clicks before shipping
The reviewed markEmailVerifiedByToken query sets verify_token and its expiry to null after success. The route then uses one 404 for a token that is invalid, expired, or already used. That is a reasonable privacy boundary, but raw NOT_FOUND is not useful completion copy.
Render a shared state such as: "This link has already been handled or is no longer valid." Offer a safe status route, sign-in, or a bounded resend action. Do not publicly distinguish which condition applied.
Route from the returned gate, not from a generic success
The target query activates a personal organization with no domain, but a domain-based organization remains pending until domain ownership is verified. The completion page needs two explicit outcomes:
- Active: "Email verified" with Sign in or Open dashboard.
- Pending domain: "Email verified" with Continue domain verification. Do not say the organization is active.
Return only the categorical fields needed for this branch. Do not render the whole index record just because the API already has it.
Keep the capability URL out of secondary systems
- Serve the token-bearing page with
Cache-Control: no-storeandReferrer-Policy: no-referrer. - Avoid third-party scripts, pixels, fonts, or images on that page. The W3C no-referrer policy omits referrer information, but fewer outbound requests are still a smaller exposure surface.
- Never place the token or full URL in analytics, browser error reports, console output, access-log query strings, screenshots, or support tickets.
- After a successful POST, use history replacement or a server redirect to a clean completion URL.
Regression contract
- The delivered href uses the frontend origin and a human page, never the JSON API route.
- GET of the human page performs no verification mutation.
- One explicit action performs one POST; duplicate clicks are disabled while pending.
- A 200 result renders branded success and chooses active or pending-domain continuation correctly.
- Refresh, repeat click, expired token, and malformed token render deliberate recovery states without raw JSON.
- Timeout or 5xx remains unresolved until server state is reconciled; the UI does not claim failure or success prematurely.
- The token is absent from telemetry, third-party requests, logs, copied evidence, and the URL after completion.
- The programmatic API can retain a JSON representation for intentional API clients, but the email does not link to it.
Classify message generation, receiver visibility, first open, token use, and final account state separately.
Primary references
- nanda-index-v2 issue 17: verification succeeds but the browser receives raw JSON
- Reviewed verification email URL builder at commit 5b8b0d8
- Reviewed state-changing GET route and JSON response
- Reviewed single-use token clearing and activation gate
- RFC 9110 section 9.2.1: safe HTTP methods
- W3C Referrer Policy: no-referrer