DOCS · ADMIN · BILLING

Charging for tunnels.

Billing is handled by Dodo Payments, a merchant-of-record that settles sales tax and VAT on your behalf. The runtime reads its config from environment variables in /etc/qnt/qnt-server.env — set a key, two product IDs, and a webhook secret, and the upgrade flow comes alive. This whole section is optional: skip it if you don't charge your users.

1 Dodo Payments overview

21tunnel bills through Dodo Payments — it replaced Stripe. Dodo is a merchant-of-record, so it collects and remits sales tax and VAT for you rather than leaving that to your own accounting.

When you need this

Billing is entirely optional. If you run 21tunnel for your own team and don't charge anyone, leave it unconfigured and move on. You only need this page if you want to sell the Pro and Team plans to your users.

  • Hobby — the free tier; no payment needed.
  • Pro and Team — the paid plans, each backed by a Dodo product.
  • A 7-day trial grants Pro entitlements before any charge.

Plans gate features. Custom domains are one example: Free allows 0, Pro allows 1, and Team is unlimited. The entitlement follows the user's active plan.

Dashboard steps live in Dodo's own docs

Creating products, finding API keys, and registering webhooks all happen inside the Dodo dashboard. Those screens change, so this guide points you at the values 21tunnel reads rather than re-drawing Dodo's UI. For the dashboard steps themselves, follow Dodo Payments' own documentation.

2 The env vars in qnt-server.env

Billing config is read from environment variables, not from server.toml. They live in /etc/qnt/qnt-server.env, which the systemd unit loads via EnvironmentFile=-/etc/qnt/qnt-server.env.

The five variables

  • QNT_DODO_API_KEY — your Dodo API key. A test key keeps billing in test mode; a live key is for production.
  • QNT_DODO_WEBHOOK_SECRET — the webhook signing secret from the Dodo dashboard, used to verify inbound webhooks.
  • QNT_DODO_PRODUCT_PRO — the Dodo product ID for the Pro plan.
  • QNT_DODO_PRODUCT_TEAM — the Dodo product ID for the Team plan.
  • QNT_DODO_BASE_URL — the Dodo API base: https://test.dodopayments.com for test mode, https://live.dodopayments.com for live mode.

A sample env block

Drop these lines into /etc/qnt/qnt-server.env with your own values. The base URL must match the mode of your key — test key with the test base, live key with the live base.

QNT_DODO_API_KEY=...
QNT_DODO_BASE_URL=https://test.dodopayments.com
QNT_DODO_PRODUCT_PRO=prod_...
QNT_DODO_PRODUCT_TEAM=prod_...
QNT_DODO_WEBHOOK_SECRET=whsec_...

Switch to live by swapping in your live key and setting QNT_DODO_BASE_URL=https://live.dodopayments.com. Live products and a live webhook secret are distinct from their test-mode counterparts.

3 Products

Each paid plan maps to one Dodo product. You create the products in the Dodo dashboard, then copy their IDs into the QNT_DODO_PRODUCT_* variables.

Create Pro and Team in Dodo

In the Dodo dashboard, create a product for the Pro plan and a product for the Team plan, following Dodo's own docs for the product-creation flow and pricing setup. Each product gets a unique product ID.

  • Copy the Pro product ID into QNT_DODO_PRODUCT_PRO.
  • Copy the Team product ID into QNT_DODO_PRODUCT_TEAM.

The customer portal works automatically once your key and products are set — the server creates a Dodo customer-portal session for upgrades and plan management, with the return URL passed as a query parameter (a Dodo API requirement). There's nothing for you to configure there beyond the env vars on this page.

4 Webhooks

Dodo notifies the server of payment events over a webhook. You register the URL in the Dodo dashboard and copy back the signing secret so the server can verify each inbound request.

Register the endpoint

The inbound path is /webhooks/dodo (POST). Register your public URL for it in the Dodo dashboard:

https://login.example.com/webhooks/dodo

Copy the signing secret

Dodo signs webhooks per the Standard Webhooks spec (HMAC-SHA256). The dashboard gives you a signing secret when you create the endpoint — copy it into QNT_DODO_WEBHOOK_SECRET. The server verifies every inbound signature against this value and rejects anything that doesn't match.

A mismatched or missing secret means valid webhooks fail verification, so plan changes won't be applied. Make sure the secret matches the mode (test vs live) of your API key.

5 Apply + smoke

Environment changes are picked up at startup, so restart the service after editing the env file, then run a smoke test.

Restart to pick up the env vars

systemctl restart qnt-server

Run the billing smoke test

The repo ships a smoke script at scripts/dodo-billing-smoke.sh that exercises the billing endpoints end to end. Run it to confirm the key, products, and base URL line up before you flip to live mode.

scripts/dodo-billing-smoke.sh

Without QNT_DODO_API_KEY set, the billing endpoints are inactive — billing runs in a disabled mock mode. The dashboard still works and users can sign in; upgrades simply don't charge anyone until you supply a key.

Next

Billing wired up. Carry on through the admin guide.