Skip to main content
The widget runs in the browser, so it can’t hold your Rhinestone API key — the key authorizes writes against your project, from registering accounts to spending sponsorship. Every request the modal makes goes to a proxy you run, which attaches the key and forwards to the deposit processor. All three modals take it as a required backendUrl. There is deliberately no default: a Rhinestone-operated fallback would mean your users’ deposits ride our key. Two ways to get one:
  • Write your own. A route table and a header — see minimal proxy.
  • Deploy Rhinestone’s. We’re open-sourcing the proxy we run for clients, so you can deploy it as-is. Coming soon.
Before v0.9.0 backendUrl was optional and defaulted to a Rhinestone-internal service. If you never set it, you were using ours — set it now. See migration.

Minimal proxy

A proxy is an explicit route table plus a header. The allowlist is the security boundary — see the warning below.
Do not replace that loop with a wildcard passthrough (app.all("/*", …)). The proxy attaches your API key to whatever reaches it, so a wildcard hands the browser every write on the upstream — including POST /setup, which rotates your webhook secret and sponsorship config. The route list is what stops that.

Refunds are not a route-table entry

POST /deposits/refund cannot be forwarded like the routes above. Every route in that loop passes the browser’s body straight through with your API key attached — which for a refund means anyone could return any of your recoverable deposits to an address they chose. It needs a handler that authorizes the specific refund, and derives the upstream call from that authorization rather than from the request body:
  • your own route — use createRefundHandler, which checks the deposit belongs to the caller before spending the key
  • a proxy — verify a per-refund x-user-token and take every upstream field from the token, ignoring the body
Register it only when that verification is in place. See claim modal.

Required routes

Missing a route doesn’t degrade the flow — the request 404s and that part of the modal stops working.
Proxy GET /setup only, never POST /setup. The POST is an admin write — it rotates your webhook secret and sponsorship config — and must not be reachable from a browser.

CORS and the version header

All three modals send x-deposit-modal-version on every request. Browsers reject a request carrying a header the server didn’t allow on the preflight, so an explicit allow-list must include it:
Bare cors() in Hono is fine — with no allowHeaders it reflects whatever the preflight asks for.
This bites on upgrade, not on first deploy. A modal version that starts sending a new header fails the whole request at preflight against a proxy with a fixed allow-list, not just the header. Deploy proxy changes before the modal that needs them.
The header tells us which versions are live, so we can warn you about one with a known bug and reproduce issues against the build you’re running. Forwarding it upstream is optional. To read the value in your own app, for a bug report:

Webhooks

The widget’s lifecycle callbacks fire only while the modal is open, so a user who closes it mid-bridge leaves your app unaware the deposit completed. Anything that must happen regardless — crediting a balance, sending a receipt — belongs on a webhook handler. Configure it once with POST /setup.