Wiring World ID into this app.
Five values, two of them secret. Here's what each one is, where to get it from the Worldcoin developer portal, and where it lives in this project.
App ID
A unique identifier for your app. Anyone who inspects the page can see it — it's not a secret, it just tells the World App which project a proof is for.
developer.worldcoin.org → your app → Overviewsrc/config/worldid.ts · WORLDID_CONFIG.appIdapp_de8f579bf9212968228f2989974a8d77Action
A named event you're verifying a human for — one proof per user per action. Must match exactly on the widget and on the portal, or verification fails.
Each action has a verification level: Device (any World App user) or Orb (Orb-verified humans only). This project uses the World ID 4.0 docs default, orbLegacy(), because this app is configured as an Orb-backed external app.
developer.worldcoin.org → Actions → Createsrc/config/worldid.ts · WORLDID_CONFIG.actionproof-of-hackerRP (Relying Party)
World ID v4 replaces the old WalletConnect deep-link with a signed handshake. You register an RP in the portal, which gives you two things:
rp_id— public identifier for the relying party.signing key— a hex-encoded private key. Never ships to the browser.
developer.worldcoin.org → Relying Parties → CreateWORLDID_RP_IDWORLDID_RP_SIGNING_KEYPOST /api/public/idkit/rp-signaturerp_context (the signed handshake)
When the user taps Verify with World ID, the widget needs a short-lived signature proving this app is a registered RP for the given action. Here's the round-trip:
browser server Worldcoin
│ │ │
│ POST rp-signature │ │
│─────────────────────▶│ signRequest(action, key) │
│ {rp_id, sig, nonce} │ │
│◀─────────────────────│ │
│ │
│ IDKit widget opens · World App scans QR · proof │
│ │
│ POST /api/…/verify │ │
│─────────────────────▶│ POST v4/verify/{rp_id} ───▶│
│ {verified} │◀─────────── {verified} ─────│
│◀─────────────────────│ │The signature expires after 5 minutes, so the widget fetches a fresh one each time it opens.
Two IDs, two jobs
World ID v4 uses app_id and rp_id for different things. The current v4 verify API prefers the RP ID; the App ID is still accepted only for backward compatibility. They look similar, they're both public, they're not interchangeable.
app_id— identifies the app in the widget'sapp_idprop and in the browser's request to this app's verify proxy.rp_id— used to sign therp_contexton the server and as the preferred ID in the upstream verify URL (.../api/v4/verify/{rp_id}).
If this app verifies against a different ID than the signed RP context belongs to, World App can report success while the browser flashes red "Verification declined" a second later.
Allow legacy proofs
The client passes allow_legacy_proofs={true} alongside deviceLegacy(). Modern World ID 4.0 apps ship with legacy proofs implicitly enabled, so there's usually no toggle to flip.
Only revisit this if verify keeps returning invalid_proof despite the client flag being set — then check Settings → Advanced in the portal.
developer.worldcoin.org → your app → Settings → AdvancedSecrets checklist
Both are stored in Lovable Cloud (Project Settings → Secrets):
WORLDID_RP_ID— the public rp_id string from the portal.WORLDID_RP_SIGNING_KEY— the hex private key you generated when creating the RP.
Both are read server-side only, inside src/routes/api/public/idkit/rp-signature.ts. Never hardcode them in src/config/worldid.ts or any client-imported module.
When it doesn't work
"Verification successful" in World App → red "Verification declined" in the browser
Two independent bugs produce this exact symptom. Check in order: (1) the upstream v4 verify URL uses the configured rp_id for this RP context; (2) the server forwards body.idkitResponse (unwrapped), not the whole { app_id, idkitResponse } wrapper, to Worldcoin.
"Open World App" does nothing / silent
Production app_ids no longer accept the classic worldapp:// deep link. You need the full v4 RP flow (this page), and the widget preset must match the action's credential level in the portal.
Proof received → server verify returns 403
The World App proof reached this app, but the World verify API rejected the server-to-server request. Copy the debug block from the demo; it includes the protocol version, action, response type, verify host, and upstream status without exposing proof data.
rp-signature 500 no_rp_config
One or both secrets are missing. Add them and redeploy — secrets are read at request time, not build time.
verify 400 invalid_proof
Either the action string doesn't match what's registered in the portal, or "Allow legacy proofs" is off.
verify 400 action_not_found
The action string exists in src/config/worldid.ts but hasn't been created in the portal yet. Create it under Actions with the same name.
This page is maintained alongside the code. If you change the IDKit flow, update src/routes/worldid-setup.tsx to match.