Skip to content

feat(localnet): role-scoped nginx vhosts for wallet and ledger APIs - #280

Merged
zheli merged 2 commits into
mainfrom
instance-vhosts
Jul 20, 2026
Merged

feat(localnet): role-scoped nginx vhosts for wallet and ledger APIs#280
zheli merged 2 commits into
mainfrom
instance-vhosts

Conversation

@zheli

@zheli zheli commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Builds on #279. Serves each nginx-fronted Splice UI/API behind an instance-scoped virtual host so URLs stay unambiguous when multiple localnets run concurrently, dropping the flat *.localhost names entirely.

Vhosts come in two shapes:

  • Role-scoped (<service>.<role>.<instance>.localhost) for services that exist per role — wallet, ans, json-ledger-api, grpc-ledger-api — e.g. wallet.app-user.localnet-2.localhost, json-ledger-api.app-provider.localnet-2.localhost. This disambiguates the app-user / app-provider / sv instances of each service, which all listen on distinct UI ports.
  • Single-per-instance (<service>.<instance>.localhost) for the SV node's own UIs — scan, sv.

Changes

  • assets/nginx/{app-provider,app-user,sv}.conf: server_name is now a role-suffixed ${VHOST_*_<ROLE>} placeholder for role-scoped services (e.g. ${VHOST_WALLET_APP_USER}), and a plain ${VHOST_SCAN} / ${VHOST_SV} for the single ones; flat Splice names removed. The deprecated canton.localhost block is left flat.
  • assets/nginx/00-devkit-tuning.conf (new): http-context snippet mounted straight into conf.d (ahead of the role blocks via the 00- sort prefix). The longer role-scoped names overflow nginx's default 64-byte server-name hash bucket, so this bumps server_names_hash_bucket_size to 128 — without it nginx aborts on boot with could not build server_names_hash.
  • overlay.go: instanceVHost() (single) + new instanceVHostRole() (role-scoped) + VHostService* consts; WriteNginxVhostOverlay injects the per-instance/per-role VHOST_* values as nginx container env (envsubst expands them at boot) and materializes + mounts the tuning snippet (always overwritten — it's a boot invariant, not edit-preserving).
  • up.go / status.go: welcome screen + status endpoints advertise each wallet UI at its role-scoped wallet.<role>.<instance>.localhost via a shared walletUIRoleByKey map / walletVHostForKey helper.
  • env.go: single-scopes CANTON_SCAN_UI_URL; per-role CANTON_<ROLE>_{JSON,GRPC}_LEDGER_API_URL and unqualified CANTON_{JSON,GRPC}_LEDGER_API_URL aliases (default app-provider) now carry the role-scoped ledger-api vhost.
  • ui_reachability.go: dials loopback (multi-label *.localhost doesn't resolve via the OS/Go resolver on macOS) but carries the role-scoped wallet vhost as the Host header so the probe validates the real route.

Resolution caveat (documented throughout)

*.localhost resolves to 127.0.0.1 in browsers, curl, and Go, but not in JVM/Node/Python resolvers (and some Rust HTTP clients). Those clients must send an explicit Host: header (HTTP) / :authority: pseudo-header (gRPC) or add an /etc/hosts entry. No auto /etc/hosts management is performed.

Verification

  • go build ./..., go vet, golangci-lint clean; full go test ./... green (1538 passed).
  • Re-upped localnet-2 (Splice 0.6.12) and curl-verified against the live nginx:
    • nginx boots clean with the bucket-size fix (previously crash-looped on the longer role-scoped names).
    • Host: wallet.app-user.localnet-2.localhost → wallet bundle (index-V6UCL5Qo.js) on :52342; app-provider :52343; sv :52344.
    • Host: ans.app-user.localnet-2.localhost → ANS bundle (index-BFLvwVCQ.js) — confirms wallet and ANS route to distinct upstreams on the same port (the bug this fixes: the wallet URL was previously falling through to the ANS server block).
    • Host: json-ledger-api.app-provider.localnet-2.localhost /v2/version → 200.
    • Host: scan.localnet-2.localhost → 200.
    • localnet status shows role-scoped wallet URLs with reachability: ok; localnet env emits the role-scoped ledger-api URL vars.

Note: because the materialized nginx/*.conf files are edit-preserving, upgrading an instance created by an older DevKit keeps the old confs until they are deleted (the documented "delete to restore default" behavior). The 00-devkit-tuning.conf snippet is always overwritten so the bucket-size fix can't be lost on upgrade.

Serve each Splice UI/API behind an instance-scoped virtual host of the
form <service>.<instance>.localhost (e.g. wallet.localnet-2.localhost)
instead of the flat *.localhost names, so URLs stay unambiguous across
concurrently running localnets.

- assets/nginx/{app-provider,app-user,sv}.conf: server_name is now a
  ${VHOST_*} placeholder (wallet, ans, scan, sv, json-ledger-api,
  grpc-ledger-api); the flat Splice names are dropped. The deprecated
  canton.localhost block is left flat.
- overlay.go: add instanceVHost() + VHostService* consts; thread the
  instance name into WriteNginxVhostOverlay and inject the per-instance
  VHOST_* values as nginx container env (envsubst expands them at boot).
- up.go / status.go: advertise wallet UIs at wallet.<instance>.localhost
  in the welcome screen and status endpoints.
- env.go: instance-scope CANTON_SCAN_UI_URL and emit per-role
  CANTON_<ROLE>_{JSON,GRPC}_LEDGER_API_URL plus unqualified
  CANTON_{JSON,GRPC}_LEDGER_API_URL aliases (app-provider), each behind
  the matching ledger-api vhost.
- token/registry_url.go + registry/client.go + registry/doc.go +
  canton integration test: thread the instance-scoped scan Host header
  through DevKit's own scan-registry client.
- ui_reachability.go: dial loopback (multi-label *.localhost does not
  resolve via the OS/Go resolver on macOS) but carry the wallet vhost as
  the Host header so the probe validates the real route.

Note the resolution caveat documented throughout: *.localhost resolves
to 127.0.0.1 in browsers, curl, and Go, but not in JVM/Node/Python
resolvers, which must send an explicit Host header (HTTP) / :authority
pseudo-header (gRPC) or add an /etc/hosts entry.
@cursor

cursor Bot commented Jul 15, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

Serve role-specific Splice services at role-scoped instance vhosts of the
form <service>.<role>.<instance>.localhost (wallet, json-ledger-api,
grpc-ledger-api, ans) so per-role UIs/APIs are unambiguous. Single-per-
instance services (scan, sv) keep the shorter <service>.<instance> shape.

The longer role-scoped names overflow nginx's default 64-byte
server-name hash bucket, so add a DevKit-owned http-context tuning
snippet (00-devkit-tuning.conf) that bumps server_names_hash_bucket_size
to 128 and mount it straight into conf.d; without it nginx aborts on
boot with "could not build server_names_hash".
@zheli zheli changed the title feat(localnet): instance-scoped nginx vhosts for every Splice UI/API feat(localnet): role-scoped nginx vhosts for wallet and ledger APIs Jul 15, 2026
@zheli

zheli commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Note: this is an intermediate migration step.

Right now each localnet instance still exposes its services across several distinct UI ports (e.g. app-user :52342, app-provider :52343, sv :52344), and we disambiguate per role/service with hostnames layered on top of those ports.

The end goal is to serve each localnet instance behind a single port, using multiple hostnames to route to the individual services/roles — i.e. one listen port per instance, with nginx server_name selecting the upstream. This PR moves us toward that by making every service addressable by a stable, unambiguous hostname; collapsing to the single shared port is the follow-up.

@zheli
zheli merged commit 6a82c37 into main Jul 20, 2026
25 checks passed
@zheli
zheli deleted the instance-vhosts branch July 20, 2026 20:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant