Skip to main content

Session heartbeat

@camunda/session-heartbeat tracks genuine browser activity and calls the Camunda security library (CSL) heartbeat endpoint, so that a webapp session expires from real user inactivity rather than from the absence of backend traffic.

Design decision: CSL ADR-0042 — Configurable session idle timeout driven by client activity.

Package source: webapp/client/packages/session-heartbeat. Its README is the API reference; this page covers how it fits into the monorepo.

The backend contract

CSL owns two properties:

PropertyDefaultMeaning
camunda.security.session.max-inactive-interval30mIdle timeout. Accepts 30m, 1800s, or PT30M.
camunda.security.session.heartbeat.enabledfalseOff: any request keeps the session alive. On: only the heartbeat call does.

POST {basePath}/session/heartbeat is installed on every webapp security chain — the primary surface and every physical-tenant scope, on both OIDC and Basic auth — and derives from basePath exactly like /login and /logout. It requires an authenticated session and answers 204 No Content.

Two consequences shape the frontend:

  • The endpoint exists regardless of the flag. Sending heartbeats is harmless when heartbeat.enabled=false, which is what makes adopting the package independent of any host's rollout decision.
  • CSRF applies. CSL exempts only /login and /logout from CSRF protection, so a heartbeat from a session-bearing browser must carry X-CSRF-TOKEN. A missing or stale token is rejected with 401 (the webapp chain routes the CSRF denial through its auth-failure handler, with the reason in the response detail), so it reaches the frontend the same way an expired session does.

Usage in the orchestration cluster webapp

The hook is mounted once in the authenticated route, src/routes/_auth/route.tsx, next to SessionWatcher:

useSessionHeartbeat({
url: endpoints.sessionHeartbeatUrl(),
csrfToken: getCsrfTokenFromStorage,
onUnauthorized: () => {
authenticationStore.disableSession();
reactQueryClient.clear();
},
});
  • endpoints.sessionHeartbeatUrl() resolves the URL through getFullURL, so the configured context path is applied like it is for every other call.
  • csrfToken reuses the token the app's request layer already caches in sessionStorage.
  • onUnauthorized mirrors what request() does on a 401, so a session that expired anyway ends up in the same state as one detected by any other call.

The dev server proxies /session/heartbeat to localhost:8080 alongside /login and /logout, so heartbeats reach a locally running backend.

Because the heartbeat lives in the _auth route, it starts after login and stops on logout or unmount. Every open tab heartbeats on its own; they share one session, so the cost is one extra request per tab per interval.

Adopting it in another frontend

  1. Add @camunda/session-heartbeat to the app's dependencies.
  2. Call useSessionHeartbeat once inside the authenticated part of the app — not per page.
  3. Pass the scope's own {basePath}/session/heartbeat, including the context path.
  4. Pass the CSRF token from wherever that app keeps it.
  5. Keep intervalMs well under the host's max-inactive-interval (a quarter or less). The 60s default suits CSL's 30m default.

A host may only set camunda.security.session.heartbeat.enabled=true once every frontend served by that deployment sends heartbeats. Enabling it earlier logs users out mid-session.

Publishing a new version

Same flow as Camunda API Zod schemas:

  1. Increment the version in packages/session-heartbeat/package.json, update the dependency version in consuming workspace packages, run npm i, and merge to main.
  2. Run the Publish Session Heartbeat to npm GitHub Action. Dry-run is enabled by default — uncheck it to publish.
  3. Bump the dependency in any consumer outside this workspace.

Behavior changes reach consumers only when they bump the dependency, so a fix to the throttle interval or the event list is not automatically picked up by apps that pinned an older version.