Local Debugging
The biggest difference between a payment system and an ordinary web app: third-party pay gateways call your service back proactively, and WeChat authorization domains, JSAPI secure domains, and Alipay app gateways all require fixed addresses that are publicly reachable and ICP-registered. In local development your machine has no public IP — callbacks cannot get in, and phones cannot reach the H5 on your machine. That is where a local debugging setup comes in.
One-click installer users can skip this page
The server provisioned by the one-click installer already has a public address, so the problems on this page do not apply. This page targets local source-code development.
Choosing a Setup
| Setup | Prerequisites | Phone access | Server callbacks | Best for |
|---|---|---|---|---|
| LAN domain direct access | ICP-registered domain + phone and computer on the same LAN | Yes | No | Phone-side frontend flows: H5 cashier page, in-WeChat payment, web authorization |
| Direct access + callback tunneling (recommended) | Plus a public server running frp | Yes | Yes | Day-to-day debugging: frontend traffic over the LAN, tunnel callbacks only |
| Full tunneling | Public server + ICP-registered domain | Yes | Yes | Testers outside the LAN (remote demos), see the appendix |
The split criterion is who initiates the traffic: with WeChat JSAPI / Alipay mobile web payment, the page load on the phone, the order API calls, the JSSDK payment invocation, and the post-payment redirect all originate from the phone — that part works fine over direct LAN access; only the async callback carrying the payment result comes from WeChat/Alipay servers (initiated from the public internet) and needs public reachability.
The callback URL and the frontend domain are decoupled: the async notification URL is a parameter passed to the channel at order time (it comes from the channel configuration), and WeChat/Alipay servers do not care which domain the payment page on the phone was opened from. That is why the recommended setup is "frontend over the LAN, tunnel callbacks only" — 99% of the traffic stays on the LAN (fast on the phone, zero load on the tunnel), and only occasional callback POSTs travel through the tunnel.
Option 1: LAN Domain Direct Access
How It Works
Point the A record of an ICP-registered domain's subdomain directly at the computer's LAN IP. Providers such as DNSPod allow publishing private-address (192.168.x.x) records; once the phone resolves the record, its traffic goes straight to the computer on the LAN:
Phone (same WiFi) → DNS query: local.daxpay.example.com = 192.168.1.100
→ straight to the computer's nginx (443) → local dev serverThe phone needs no hosts changes and no extra tools — the page opens directly inside WeChat too.
Preparation Steps
- Fix the computer's LAN IP: in the router admin, bind the computer's network adapter to a static DHCP lease (e.g.
192.168.1.100) so the record does not break when the IP drifts; - Add the DNS record: in the DNS provider's console, add an A record for a subdomain of the ICP-registered domain pointing at that LAN IP (e.g.
local.daxpay.example.com A 192.168.1.100); - Issue an HTTPS certificate: the WeChat/Alipay H5 flows require https. Use acme.sh / certbot with DNS-01 validation to issue a Let's Encrypt certificate — DNS-01 only needs a temporary TXT record to prove domain ownership and does not require the domain to be publicly reachable, a natural fit for domains resolved to LAN IPs:
# acme.sh example (DNSPod; get the keys from the console, export once)
acme.sh --issue --dns dns_dp -d local.daxpay.example.com --keylength ec-256
# Install into nginx with auto renewal (auto-renews at the 90-day expiry + reload)
acme.sh --install-cert -d local.daxpay.example.com --ecc \
--key-file C:/Server/nginx/conf/ssl/local.daxpay.example.com-key.pem \
--fullchain-file C:/Server/nginx/conf/ssl/local.daxpay.example.com.pem \
--reloadcmd "C:/Server/nginx/nginx.exe -s reload -p C:/Server/nginx"- Open the firewall: allow inbound 443 in Windows Firewall:
netsh advfirewall firewall add rule name="nginx-https" dir=in action=allow protocol=tcp localport=443- Connect the phone to the same WiFi; if
https://local.daxpay.example.comopens in the browser, the link works.
Nginx Configuration
Add a server block to the nginx on the computer. Watch out for client_max_body_size and the timeouts — both are easy to miss (defaults 1M / 60s: image uploads get 413, long connections get cut):
http {
# Two globals often missing from existing configs
client_max_body_size 50m; # default 1M; admin panel image uploads get rejected with 413
# Always redirect 80 to https (http links pasted into WeChat still land correctly)
server {
listen 80;
server_name local.daxpay.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
http2 on;
server_name local.daxpay.example.com; # DNS resolves to the LAN IP
ssl_certificate ssl/local.daxpay.example.com.pem; # Let's Encrypt (DNS-01)
ssl_certificate_key ssl/local.daxpay.example.com-key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
# All traffic goes to the H5 dev server; /api is forwarded to the main app by Vite's proxy
location / {
proxy_pass http://127.0.0.1:9500;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_read_timeout 300s; # default 60s; prevents drops for long polling / SSE
# Vite HMR (WebSocket)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
}Why not route /api separately
The H5 dev server already has a built-in local proxy (VITE_PROXY in dax-pay-h5/.env.development forwards /api to 127.0.0.1:9999), so nginx can send all traffic to port 9500 and Vite will route /api to the main app. To bypass Vite and hit the backend directly, add location /api/ { proxy_pass http://127.0.0.1:9999/; }.
Complete WeChat domain verification while the domain resolves publicly
When you configure JS secure domains / web authorization domains in the WeChat official account console, WeChat servers fetch MP_verify_xxxx.txt from the domain root over the public internet for ownership checking — if the domain resolves to a LAN IP, WeChat cannot reach it and the check never passes. The correct sequence: first point the subdomain's A record at any publicly reachable server (for example, serve a static file path for it on an existing public nginx and place the verification file there) and complete the WeChat console verification; once the domain is registered in the whitelist, switch the DNS to the LAN IP — a verification that already passed stays valid after the resolution switch, and the public side no longer needs to serve the file during debugging.
Hijacking the Production Payment Link Domain (Advanced)
Payment links generated in production point at the production redirect domain (e.g. redirect.example.cn/cashier/xxx). Resolve that domain to the LAN IP as well, issue a certificate for it, and have nginx reverse-proxy it to the local H5 dev server: the phone then opens a payment link generated by the production environment that lands in your local code — the most effective way to troubleshoot "production payment link won't open / white screen" issues. The configuration is identical to the one above; only the server_name and certificate change to the production redirect domain.
Callback Boundary
LAN direct access cannot receive async callbacks from WeChat/Alipay servers — they initiate requests from the public internet and cannot connect once they resolve the LAN IP. This does not matter when you only debug the frontend flow: the payment result falls back to order queries, and the H5 payment page can pick it up by polling. When you need to verify "did the callback arrive, did signature verification pass", add callback tunneling on top — the two can be stacked or removed at any time.
Option 2: Callback Tunneling (Callbacks Only)
frp tunnels a single port of the main app; the local nginx and the frontend dev server never touch the public internet:
# Local frpc — one tunnel to the main app only
[[proxies]]
name = "daxpay-callback"
type = "tcp"
localIP = "127.0.0.1"
localPort = 9999 # straight to the main app, bypassing the local nginx
remotePort = 18080 # port on the public serverThe public server's nginx only terminates HTTPS for the callback domain (WeChat v3 callbacks require https; the standard port 443 is recommended):
server {
listen 443 ssl;
server_name cb.daxpay.example.com;
ssl_certificate ssl/cb.daxpay.example.com.pem;
ssl_certificate_key ssl/cb.daxpay.example.com-key.pem;
location /api/ {
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_pass http://127.0.0.1:18080/; # frps remotePort; trailing slash strips /api
}
}To narrow the tunnel surface further, the public nginx location can allow only the channel callback path prefix (e.g. /api/unipay/callback/) and return 404 for everything else.
One wildcard certificate, two uses
Issue a single *.daxpay.example.com wildcard certificate via DNS-01 and deploy the same one on the local nginx (Option 1's local.daxpay.example.com) and the public nginx (cb.daxpay.example.com); one renewal covers both.
Callbacks are lost while the tunnel is down
WeChat/Alipay callbacks retry with backoff (several times within a few minutes); if the frp tunnel is down exactly when a payment happens, that callback arrives late or is lost. Fallbacks: the H5 page polls order queries for the result, and the admin panel's "Transaction Management → Channel Callback Records" shows whether callbacks arrived — compensate the order manually if needed. When you only debug the frontend without verifying callbacks, stopping frpc does not affect Option 1 at all.
Configuration Ownership
In combined setups, the one error-prone point is "which domain goes into which config field". The rule in one sentence: everything the phone opens gets the LAN domain; only the channel callback address gets the public domain.
| Config field | Where | Value |
|---|---|---|
| Pay gateway frontend URL (used to generate payment links) | Admin panel "Platform Config → Endpoint Config" | https://local.daxpay.example.com (LAN domain) |
| WeChat authorization domain / JSAPI payment directory | WeChat official account / merchant console | LAN domain (must be ICP-registered) |
| Channel callback / notification URL | Each channel's config in the admin panel | https://cb.daxpay.example.com/api/... (public domain; follow the hints on the channel config page) |
| Web admin / H5 API URL (for production builds) | dax-pay-ui/apps/*/env/.env.production, dax-pay-h5/.env.production | LAN domain, unified /api prefix |
If you only use Option 1 (no tunnel), it is fine to fill in the public domain for the callback address — the callback simply cannot arrive, which is expected; order queries cover the result, and once a tunnel is set up later it takes effect automatically.
When debugging in a local browser at 127.0.0.1:6999 during development, the frontend dev server's local proxy already forwards /api to the local main app — no domain needed; only phone access, third-party callbacks, and in-WeChat payments must go through the debugging domain.
Appendix: Full Tunneling (Remote Testing)
When testers are outside the LAN (e.g. demoing to remote colleagues), tunnel the entire local nginx entry out and dispatch each app by path under a single domain. frp client:
serverAddr = "1.2.3.4" # your public server (frp server)
serverPort = 7000
auth.method = 'token'
auth.token = 'your-token'
[[proxies]]
name = "daxpay-dev"
type = "tcp"
localIP = "127.0.0.1"
localPort = 18080 # local nginx listen port (see below)
remotePort = 18080 # port exposed on the public serverOn the public side, nginx on the server (or an frp http-type proxy) then forwards dev.daxpay.example.com traffic to that port. The local nginx dispatches by path (same forwarding rules as Project Build - Static Deployment; the trailing slash on proxy_pass strips the /api prefix):
server {
listen 18080;
server_name dev.daxpay.example.com;
client_max_body_size 50m;
# Pay gateway API + channel callbacks
location /api/ {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_pass http://127.0.0.1:9999/;
}
# H5 (for phone debugging)
location /h5/ {
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://127.0.0.1:9500/;
}
# Web admin panel
location / {
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://127.0.0.1:6999/;
}
}H5 base path
This project's H5 defaults VITE_PUBLIC_PATH to / (root-path deployment); dispatching it under the /h5/ sub-path as above causes asset 404s. Prefer giving H5 its own subdomain (the Option 1 approach); if you insist on the sub-path approach, adjust the H5 base path accordingly.
ngrok / cpolar work out of the box — one command maps local port 18080 to a temporary domain — but the temporary domain changes every time, so it does not fit into the WeChat/Alipay domain whitelists and is only good for quick demos.
FAQ
Phone cannot open the domain? Check in order: are the phone and computer on the same WiFi; has DNS taken effect (WeChat may cache DNS — toggle airplane mode and retry); does Windows Firewall allow 443; does curl -k https://127.0.0.1 work on the computer. A changed computer IP (DHCP renewal) also invalidates the record — go back to preparation step 1 and bind a static IP.
Callbacks not arriving? First check which setup you are on: with pure LAN direct access, missing callbacks are expected behavior — see "Callback Boundary" above; with callback tunneling, check in order whether the frp tunnel is up (frpc logs), whether the public domain is reachable from outside (test on the phone with WiFi off, cellular on), whether the public nginx /api/ forwarding works, and whether the callback address matches the channel configuration. On the main app side, the admin panel's "Transaction Management → Channel Callback Records" shows whether callbacks arrived.
File uploads return 413? nginx's default client_max_body_size is only 1M; raise it (e.g. to 50m) in the http block or the corresponding server block. Check the nginx on both sides — local and public.
White screen or authorization failure for H5 inside WeChat? Usually caused by an unconfigured or mismatched authorization domain; confirm the domain configured in the official account console matches the actually visited domain exactly (including subdomains); WeChat authorization domains must be ICP-registered — temporary/ngrok domains will not pass the review.
Certificate expired? Let's Encrypt certificates last 90 days; set up auto renewal with acme.sh --install-cert ... --reloadcmd "nginx -s reload" so it does not expire in the middle of a debugging session.