A browser session cookie belongs to the host that set it. If your application loads at https://portal.example but sends sign-in to https://api.example, the browser stores the cookie for api.example. Requests rendered by portal.example cannot send it. The sign-in response can be successful while the next page load sees no session and returns to the login page.
Keep the Platform API behind your application's own origin. The browser calls /api on the app host, and your server forwards that path to the private Platform API URL. The SDK supplies the proxy so every app applies the same cookie, header, origin, stream, and failure rules.
Mount the proxy
Install the SDK and export a handler from a server route. This TanStack Start example accepts the methods a Platform API route can receive:
import { createFileRoute } from "@tanstack/react-router";
import { createDalpProxy } from "@settlemint/dalp-sdk/server-proxy";
const proxy = createDalpProxy({
upstream: process.env.DALP_API_INTERNAL_URL!,
origin: process.env.APP_PUBLIC_ORIGIN,
trustedProxyProfile: (() => {
const profile = process.env.TRUSTED_PROXY_PROFILE?.trim();
return profile === "envoy" || profile === "nginx" || profile === "openshift" ? profile : "none";
})(),
});
export const Route = createFileRoute("/api/$")({
server: {
handlers: {
GET: ({ request }) => proxy(request),
HEAD: ({ request }) => proxy(request),
POST: ({ request }) => proxy(request),
PUT: ({ request }) => proxy(request),
PATCH: ({ request }) => proxy(request),
DELETE: ({ request }) => proxy(request),
OPTIONS: ({ request }) => proxy(request),
},
},
});The returned function is a standard (request: Request) => Promise<Response> handler. In a server route, pass the incoming Fetch Request directly to it and return its Response. Mount /dalp through the same handler if your app serves Platform API assets such as organisation logos. DALP_API_INTERNAL_URL, APP_PUBLIC_ORIGIN and TRUSTED_PROXY_PROFILE are your app's own variable names: upstream is the private Platform API address and origin is your app's public URL, for example https://portal.example.
In the browser, construct the SDK client against the current origin:
import { createDalpClient } from "@settlemint/dalp-sdk";
const dalp = createDalpClient({
baseUrl: window.location.origin,
runtime: "browser",
});Do not give the browser a public Platform API URL. Keep DALP_API_INTERNAL_URL server-only, because it is the address your app server uses to reach the Platform API inside your network.
To check the setup, sign in through the auth client on the same origin and read the signed-in user:
import { createDalpAuthClient } from "@settlemint/dalp-sdk/auth";
const auth = await createDalpAuthClient({ url: window.location.origin });
const { error } = await auth.signIn.email({ email, password });
const me = await dalp.api.party.users.me();With no error from the sign-in, a me response that carries your user's data.id shows that the session cookie lands on your app host and travels with each call.
What the handler forwards
- It forwards the request method, path, query, body, cookies, auth-surface header, and
Set-Cookieresponse headers. Cookies therefore land on the app host. - It streams
text/event-streamandapplication/x-ndjsonresponses and buffers all others. It preserves redirects and no-content statuses. - It asks the platform for an uncompressed response. With
accept, it passes the browser'sEncoding: "forward" Accept-Encodingon instead and returns the compressed bytes unchanged. Set it only when your server'sfetchhonoursdecompress: false. Otherwise the browser receives a decoded body that is still labelled as compressed. - It removes hop-by-hop headers,
host,content-length, and every client-forwarding header before the upstream request. - It passes on one client address, chosen by
trustedProxyProfile. Forenvoyit takes a validx-envoy-external-address, or else the lastx-forwarded-forentry. Fornginxandopenshiftit takes the lastx-forwarded-forentry. It passes nothing when that entry is not a valid IP address, and never reads an earlier entry. Underenvoyit sends the address upstream asx-envoy-external-address.nonepasses no address. - When the incoming request has no
Origin, it stamps the configured public origin, so the platform treats a request that reaches the proxy without anOriginas one from your app. An incoming origin is preserved for the Platform API's auth-surface checks. - A failed upstream request returns
502 UPSTREAM_, also when the platform answered but the handler could not read the body. For a write, a 502 therefore does not show that the platform did not receive it: read the operation's status before you send it again.UNAVAILABLE - A GET or HEAD request retries once after a connection reset. A request with a body is never replayed.
- When the browser has already abandoned the request, the handler answers
499with no body.
onUpstreamFailure receives the method, path, upstream origin, and error details of each failure, so you can log it. upstreamFailureMessage replaces the message in the 502 body, and fetch replaces the upstream fetch, for example in tests.
Set TRUSTED_PROXY_PROFILE to match the edge that overwrites forwarding headers. An unknown or missing value is treated as none. Choose a profile other than none only when your ingress overwrites the header that profile reads, or a browser can supply the client address itself. Under envoy, the edge must set or remove x-envoy-external-address on every request. Envoy Gateway in xForwardedFor mode does neither by itself, so remove the header in its ClientTrafficPolicy as Count readers separately behind Cloudflare shows.
Deployment note
When several apps run on separate hosts, each host serves its own /api and /dalp paths and forwards them to the same private Platform API address. The browser never needs a public Platform API URL. Same-origin app deployment shows how an operator configures the Console and the Investor portal this way, with Helm and OpenShift examples.
Auth redirects stay on your deployment's origins
Every URL you pass to the auth client for the user's next page must be on an origin your deployment lists. The platform checks each URL before it sends an email or redirects the browser, so a reset or verification link never hands its token to an unlisted origin.
The check covers these fields:
redirectToon a password-reset request.callbackURLon sign-up, sign-in, sign-out, verification, email-change, account-deletion and account-linking requests.errorCallbackURLandnewUserCallbackURLon a single sign-on request.
The listed origins are the Console URL, global., and each URL in global. and global.. An origin includes its scheme, so on a deployment whose URLs use https://, an http:// URL on any listed host is refused. A path such as /reset-password resolves on the Console URL, not on your app, so an app on its own origin passes its full URL, such as https://.
const { error } = await auth.requestPasswordReset({
email,
redirectTo: new URL("/reset-password", window.location.origin).href,
});The platform answers HTTP 403 when a URL is on any other origin, or is a path that a browser reads as another host, such as //example.com. The code names the field: INVALID_REDIRECT_URL for redirectTo, INVALID_CALLBACK_URL for callbackURL, INVALID_ for errorCallbackURL and INVALID_ for newUserCallbackURL. A refused password-reset or verification request sends no email. The refusal is a caller error, and a retry with the same URL gets the same answer: pass a listed URL, or ask your operator to list your app's origin. When someone opens a reset or verification link whose callbackURL names an unlisted origin, the platform also answers HTTP 403 and does not pass the token on.
A listed origin also receives a session. The Console session, administrators' included, goes to every origin in consoleUrls, and the Investor portal session to every origin in portalUrls. Both may call the Platform API from the browser. List an app that serves investors under portalUrls, and list only apps your team runs.
The chart's default lists read DAPP_E2E_URL into consoleUrls, and INVESTOR_PORTAL_URL and CLIENT_PORTAL_URL into portalUrls. Leave each one you do not use unset. A list you set in your values replaces the chart's list, so repeat the entries you keep, such as the Investor portal URL, beside your app's URL. Same-origin app deployment shows how to set the Console and Investor portal URLs.
Troubleshooting: sign-in succeeds but returns to the login page
Inspect the sign-in response and the next session request. If the response sets a cookie for the Platform API host while the page is on your application host, the browser is doing the correct host-only cookie isolation. Mount createDalpProxy at /api, set the browser SDK baseUrl to window.location.origin, and remove any public Platform API URL from the app. Then sign in again so the cookie is issued by the application host.