Skip to content

feat(backend-js): add a Node.js implementation of the Quickstart backend#1

Open
fernandomg wants to merge 8 commits intomainfrom
js-parity
Open

feat(backend-js): add a Node.js implementation of the Quickstart backend#1
fernandomg wants to merge 8 commits intomainfrom
js-parity

Conversation

@fernandomg
Copy link
Copy Markdown
Member

Close digital-asset#156

Summary

  • Adds a Node.js / TypeScript / Fastify backend that mirrors the Java backend's HTTP API surface, selectable at build time via BACKEND=js (persisted in .env.local by make setup).
  • Both backends share the same OpenAPI contract, env files, onboarding volume, listening port, and cookie/CSRF auth surface (OAuth2 and shared-secret). The JS backend talks to Canton via the JSON Ledger API on port 3975 instead of gRPC, reads PQS Postgres directly, and uses the dpm codegen-js Daml bindings for typed command construction. The Compose stack swaps in the JS service via an override file when BACKEND=js.

Test plan

  • Ran make clean-all && make setup and answered js to the new interactive Backend prompt — alongside the existing prompts for AUTH_MODE, OBSERVABILITY_ENABLED, TEST_MODE, and PARTY_HINT. The selection is written to .env.local as BACKEND=js, so every subsequent make start / make restart-backend / make stop automatically targets the JS service without needing to repeat the flag. Re-ran make setup and chose java to confirm the prompt round-trips and that the stack swaps back to the Java backend cleanly. Verified under both AUTH_MODE=oauth2 and AUTH_MODE=shared-secret.
  • Walked through the entire flow described in the Explore the Demo guide (https://docs.digitalasset.com/build/3.5/quickstart/operate/explore-the-demo.html): App User and App Provider login, tenant pre-registration via register-app-user-tenant, AppInstallRequest accept/reject, License issuance, the renewal flow with wallet allocation, and license expiry. Each step produced behaviour identical to the Java backend.

Deferred to follow-up

The items below were surfaced during review and consciously left out of this PR. Each one is either parity-equal with the Java backend (so a JS-only fix would create drift) or a production-hardening concern that needs a cross-cutting design touching both backends. They should be tracked in a follow-up issue against both implementations.

# Issue Why deferred
1 SESSION_SECRET falls back to random bytes per restart in backend-js/src/auth/session.ts, invalidating signed session cookies. Java's in-memory HttpSession produces the same observable behaviour: every restart logs all users out (signature mismatch in JS, lost server-side state in Java). Fix needs a stable signing secret and a persistent session store applied to both backends.
2 Raw Canton ledger error text is forwarded to HTTP clients (party IDs, template IDs, choice arguments). Java's application.yml explicitly enables include-message: always, include-stacktrace: always, include-exception: true for dev — leaks at least as much, with stacktraces. A sanitising error mapper needs to land on both backends together.
3 Malformed duration strings throw → HTTP 500 where 400 would be the correct status. Same behaviour in Java: Duration.parse throws DateTimeParseException and there is no @ControllerAdvice mapping it to 400. Fix on both sides in one PR.
4 DELETE /admin/tenant-registrations/:tenantId removes the OAuth2 registry entry before the tenant repo entry; if step 2 fails the registry is gone but the tenant remains. Java's deleteTenantRegistration uses the same ordering and has the same risk window. Proper transactional/rollback handling needs to land on both backends.
5 The 201 response body for AppInstallRequest_Accept does not include the new AppInstall's contractId. Same omission in Java's AppInstallRequestsApiImpl#acceptAppInstallRequest. The OpenAPI response shape and the choice-result extraction need to be updated together on both sides.

fernandomg and others added 8 commits April 29, 2026 12:25
…mbing

Adds BACKEND=java|js as a make-level selector, persisted in .env.local
via make setup, with a compose override, Dockerfile, and start.sh for
the JS service. The :daml gradle build emits both Java and TS codegen
and skips the irrelevant one per active BACKEND. Root .gitignore gains
an npm-prevention block to keep package.json/lock from leaking up to
the repo root.

Co-Authored-By: Gabito Esmiapodo <4015436+gabitoesmiapodo@users.noreply.github.com>
Project skeleton: package + lockfile, NodeNext tsconfig, runtime config
loader, server entrypoint, app builder with ErrorResponse-shaped error
handler and an empty-body JSON parser to accept the frontend's logout
post.

Co-Authored-By: Gabito Esmiapodo <4015436+gabitoesmiapodo@users.noreply.github.com>
Wires session, CSRF, and cookie middleware; OAuth2 (with plain-HTTP
issuer opt-in for local Keycloak, JWKS cache per issuer, partyId
resolved from the tenant repository); shared-secret form login that
resolves the username against the tenant repository and seeds the
AppProvider tenant with admin users; CSRF bypass for Bearer-
authenticated service traffic; TEST_MODE party_id override applied at
session population; admin-gate distinguishing 401/403 by Bearer or
session presence; and the /admin, /user, and /login-links routes that
sit on top of all of it.

Co-Authored-By: Gabito Esmiapodo <4015436+gabitoesmiapodo@users.noreply.github.com>
Adds the read-side data access used by the licensing routes: a pg
connection pool, a typed contract row helper that decodes the JSONB
payload, and a licensing repository that finds active contracts by id
and lists them by predicate.

Co-Authored-By: Gabito Esmiapodo <4015436+gabitoesmiapodo@users.noreply.github.com>
…nd bindings

Adds the write-side ledger access: a client-credentials token provider
with a 30s expiry buffer, a JSON Ledger API client wrapping submit-and-
wait-for-transaction, and command builders typed via the dpm codegen-js
output (damlTypes.Template/Choice). Template ids carry the
'#<package-name>:' prefix the JSON Ledger API requires, every submit is
wrapped in the JsCommands envelope, and userId is matched to the JWT
subject claim. A token-standard client adds the registry admin id and
allocation transfer context lookup.

Co-Authored-By: Gabito Esmiapodo <4015436+gabitoesmiapodo@users.noreply.github.com>
Adds the business surface exposed over the OpenAPI contract: a
licensing service that orchestrates PQS reads and ledger writes,
mappers that mirror the Java backend's DTO shapes, and the routes for
feature-flags, app-install-requests, app-installs, licenses, and
license-renewal-requests. Daml-choice routes register one wildcard per
resource to dodge find-my-way's colon-route limitation while preserving
the URL contract. Microsecond-precision timestamps are emitted on
License_Renew, the new License contract id is read from the
CreatedEvent walk (since submit-and-wait returns ACS_DELTA events
without exerciseResult), and admin-only choice endpoints are gated by
checkAdmin.

Co-Authored-By: Gabito Esmiapodo <4015436+gabitoesmiapodo@users.noreply.github.com>
Adds a Backend implementations section that contrasts the Java and JS
backends, documents the BACKEND selector and switching workflow, and
notes the Java-only debug port and the per-backend codegen behaviour.

Co-Authored-By: Gabito Esmiapodo <4015436+gabitoesmiapodo@users.noreply.github.com>
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.

Add a Node.js backend example to the QS

1 participant