Skip to content

gateway integration

Kadyapam edited this page May 29, 2026 · 3 revisions

Gateway integration

How the SPA talks to the NoETL gateway. Three modules, one SSE channel, three frame families, and a small set of REST endpoints.

Source-of-truth files:

The wire protocol

Browser ───────►  POST /api/auth/login          ──►  gateway issues session_token
Browser ───────►  POST /api/auth/validate       ──►  gateway confirms session_token
Browser ───────►  GET  /events?session_token=…  ──►  SSE channel opens
                                                       Server pushes:
                                                         - message (init, client_id)
                                                         - ping (heartbeat)
                                                         - playbook/result
                                                         - playbook/state
                                                           (incl. calendar.event.touched)
Browser ───────►  POST /graphql                 ──►  executePlaybook mutation
                                                       Response: { requestId, executionId }
Browser ◄───────  playbook/result frame         ◄──  on completion
                  (or playbook/state frames)         (or lifecycle frames)

Every request from the browser carries the session token. Authorization is checked at the gateway. The SPA never holds a database credential or a third-party API token.

Session lifecycle

gatewaySession.ts covers three flows:

Login

The SPA logs the user into Auth0 (@auth0/auth0-react), receives an ID token, and exchanges it with the gateway:

const response = await fetch(`${gateway}/api/auth/login`, {
  method: 'POST',
  body: JSON.stringify({ id_token })
});
// response: { session_token, expires_at, user_id, ... }

The gateway verifies the Auth0 token against the Auth0 JWKS, upserts the user, creates a session_token, and returns it. The SPA stores the session_token in localStorage.

Validate

On app load, the SPA calls POST /api/auth/validate with the stored session_token. If the gateway returns OK, the user is signed in. If not, the SPA clears storage and routes to the Auth0 login page.

Logout

A standard Auth0 SDK call, plus clearing the session_token from storage. The gateway-side session is allowed to expire naturally; there is no explicit "invalidate session" call today.

See Auth and session for the deeper flow and how to swap Auth0 for another identity provider.

Executing a playbook

noetlClient.ts exposes:

executePlaybook(path, workload, options): Promise<...>

Internally:

  1. The SPA opens (or reuses) the SSE channel via connectSSE(session_token).
  2. The SPA waits for the SSE channel to confirm client_id.
  3. The SPA POSTs to /graphql with the executePlaybook mutation, including the client_id (so the gateway knows which SSE client to deliver the callback to).
  4. The gateway dispatches the playbook to NoETL via /api/execute.
  5. NoETL runs the playbook; on completion, the gateway forwards a playbook/result SSE frame to the client.
  6. The SPA's pendingCallbacks map resolves the corresponding promise.

Fallback path

If the SSE callback does not arrive within CALLBACK_GRACE_MS (currently 30s), the SPA falls back to waitForExecution which polls /api/executions/{id} every 1.5s, up to 200 attempts (5 min). This path will be removed once playbook/state SSE coverage proves out in production.

SSE frame families

Frames the gateway emits on /events:

Frame type / event Payload Sent when
message (init) { result: { clientId } } On connection, once.
ping { heartbeat: true } Periodic keep-alive.
playbook/result { requestId, executionId, status, data, error? } A playbook execution completed and the gateway has its final result (delivered via /api/internal/callback).
playbook/state { execution_id, event_type: "step.exit | playbook.completed | playbook.failed | calendar.event.touched", step_name, status, at } A NoETL execution lifecycle event matching the gateway's FORWARDED_EVENT_TYPES allowlist was observed on NATS.

Each frame is filtered client-side by listening on the matching addEventListener name.

Calendar live updates: playbook transport (Round 3, final)

The calendar widget reads and refreshes via executePlaybook against the travel/playbooks/catalog/calendar/list read playbook. This is the final shape as of Round 3 of the Firestore-removal work (noetl/ai-meta#23).

src/api/calendarSubscription.ts implements the transport:

subscribeToCalendarEvents(trip_id, events_path, onItems, options?)

The module:

  1. On mount, calls executePlaybook( "travel/playbooks/catalog/calendar/list", { trip_id, thread_path, user_uid }). Waits for the playbook/result frame and feeds display_events to onItems.
  2. Adds a playbook/state SSE listener. Triggers a re-read when:
    • event_type === "calendar.event.touched" — the specific signal the itinerary-planner emits after writing a calendar event, forwarded by the gateway as of v2.12.0 / noetl/ai-meta#25.
    • event_type === "playbook.completed" — fallback for turns that finish without writing calendar events (e.g. clears loading state).
  3. Returns an unsubscribe function that removes the SSE listener and aborts any in-flight executePlaybook call.

events_path is now optional in CalendarViewPayload and is no longer emitted by the orchestrator. calendarSubscription.ts handles absent events_path gracefully (falls back to deriving user_uid / thread_path from context, or emits a console.warn if neither is available).

This transport satisfies the Ephemeral Blueprints gatekeeper rule: the gateway stays stateless, data access happens inside a playbook under that playbook's policy block, and the SPA holds no database credential.

SPA never directly does these

For pushback discipline:

  • The SPA never calls Auth0 SPA "authorization endpoint" to fetch a token for an API directly. The gateway is the API; the SPA holds only a gateway session_token.
  • The SPA never holds a database client. The firebase dependency was removed in #50; the bundle no longer ships firebase/firestore.
  • The SPA never reads or writes a third-party API token. Even VITE_GOOGLE_MAPS_KEY is a restricted widget-only key for the Maps embed, not a server-side auth.
  • The SPA never polls an arbitrary NoETL endpoint. The only polling that exists is the cold-start fallback in waitForExecution, gated to error cases.

Tests

The gateway-integration modules have unit coverage with mocked SSE servers:

Run with npm test.

Note: src/api/gatewaySubscriptions.test.ts was removed in Round 3 (noetl/ai-meta#23) along with gatewaySubscriptions.ts itself.

Related

Clone this wiki locally