Skip to main content

RFC: Centralized Web Session — Hub Chat v2 FE Integration

Document Conventions (do not remove)

Governance follows the Qontak RFC Template: metadata table, sections 1–6, and Comment logs are mandatory. This RFC is also agent-execution-ready: §1 Design References, §2 Repo Reading Guide, mermaid diagrams, and the §4 Agent Execution Plan + Verification & Rollback Recipe must be complete before §7 flips to yes.

Scope of THIS RFC: the frontend integration of hub-chat-v2 with the centralized web session SDK. The Session Manager service, SDK package, dedicated Redis, and Kong routing are owned by Account & Launchpad and are documented in the parent cross-product RFC (authoritative reference). They appear here as READ-ONLY upstream context — this RFC does not design or modify them.

Metadata

FieldValueNotes
StatusRFCPending approver + infosec assignment
Type / Sub-typefrontend / enhancementTouches auth/SSO boot path; no new product surface at all
TitleCentralized Web Session — Hub Chat v2 FE Integration
OwnerTribe Qontak PlatformOwns auth/SSO code in hub-chat-v2
Authorssyafrizal.abdillah@mekari.com
ReviewersAccount & Launchpad; Tribe Qontak PlatformA&L owns SDK/service contract
Approverstech-leader TBD; infosec TBDInfosec required — CSP / iframe / postMessage origin
Submitted2026-06-27ISO-8601
Last updated2026-06-27Matches most recent edit
Target releaseTBD — pilot after Launchpad proves the SDKParent RFC names Launchpad as first pilot, then other products
Related docsParent RFC (Confluence) · login spoke · auth-sso spokeParent RFC is the authoritative source for SDK/service contract

Sections at a Glance

§SectionHint (frontend)
1OverviewProblem, scope, PRD coverage, design references, per-story change map
2Technical DesignRepo reading guide, SDK contract, event→action mapping, diagrams
3High-Availability & SecurityCSP/iframe/postMessage origin, perf expectations (FE-side)
4Backwards Compatibility & Rollout PlanFeature toggle gating, agent execution plan, verification & rollback
5Concern, Questions, Known LimitationsOpen questions with severity
6Comment logsReview trail
7Ready for agent executionThe hand-off gate

1. Overview

1.0 Context / Problem

Today every Mekari product, including hub-chat-v2, maintains its own session and does not react when the user's SSO (Mekari Account) session changes. The parent RFC documents the resulting bugs: a user who logs out of SSO stays logged in on the product, and a user who switches account on SSO still sees the previous company on the product.

hub-chat-v2 confirms this gap in code:

  • Authentication is bootstrapped from cookies/localStorage at boot and refreshed on a timer (common/store/AuthStore.ts:498 setupIntervalRefreshToken); there is no check against SSO session state after login.
  • The only cross-state reaction is cross-tab logout via MQTT/BroadcastChannel (app.vue:216 listenForLogout), not cross-product with SSO.

The centralized-session initiative introduces a mekari-account-web-sdk Session SDK (v0.3.0, bundled git dependency — no CDN, no <script> tag) that injects a hidden iframe to https://sm.mekari.com/current, reads the SSO session, and emits a single "event" whose data.status is one of logged_in, logged_out, server_down — there is no switch_user status; an SSO account switch is indistinguishable from a plain logout and surfaces as logged_out (resolved against real SDK mekari-account-web-sdk v0.3.0, session.ts). This RFC defines how hub-chat-v2 consumes that SDK and reconciles the events with its existing auth/session, routing, and store patterns.

1.A PRD / Driver Coverage

The driver is the parent cross-product RFC (treated as authoritative). The "FE Implementation Scope (per product repo)" list is the requirement set for this repo.

PRD Section Coverage

Parent-RFC sectionCovered in this RFCNotes
1. Overview / Success Criteria§1.0, §1.AFE share of: session verified across products, 2h idle, refresh
2. Proposal / SDK usage§2.1, §2.2SDK init + event handling in hub-chat
2. SDK contract (events)§2.2 Event → Action mapAll three real statuses mapped (logged_in/logged_out/server_down)
FE Product Integration Flows (SDK flow)§2.3 sequence diagramslogged_in / server_down (no distinct switch-user flow — see §2.2)
FE Product Integration Flows (Web Session)§2.2, §2.3hub-chat uses auth-code exchange; current-company = org/company refetch
FE Product Integration Flows (OAuth2 code)§2.2hub-chat's /sso-callback IS an auth-code flow; reused
User Logout From Product§2.4 — n/a — already implementedpages/logout.vue:315 + TheSwitchAccount.vue:299 already hit SSO sign_out
User Switch Account§2.2No dedicated event; delivered as logged_out → normal sign-out flow (resolved, see §2.2)
3. HA & Security§3FE-side: CSP, iframe sandbox, event.origin upstream gap (§3.3)
4. Rollout Plan§4 — step 4/5 (this repo is NOT the first pilot)Launchpad pilots first; hub-chat onboards in "The Next Chapter"
5. Open Questions§5Q1/Q2/Q3 resolved/moot against the real SDK; remaining items genuine
Local Storage msli§2.2, §5 Q1 (resolved)msli is SDK-internal; hub-chat must not read/write it — no cross-domain cookie concern
Database Modeln/a — no DB; FE repoParent RFC: "No database changes"
Multiple sessions per accountn/a — server/SSO concernNo FE work in hub-chat
Out of scope: auto token revoke on idlen/a — out of scopeCarried over verbatim

UI / Consumer Surface Coverage

SurfaceTriggerBacking
Session-expired / forced sign-outlogged_out (covers plain logout AND account switch — §2.2)Reuses pages/logout.vue + SSO sign_out redirect (no new UI)
server_downserver_downFail-open (PRD 6.10) — no destructive action, no new UI
No other visible UISDK iframe is invisible; no page/route added

Role Coverage

Role (UserProfile.role)Behavior change
agent / member / supervisor / admin / ownerIdentical: all gated only by centralized_session toggle
super_adminn/a — review — super_admin bypasses billing/MQTT in existing flow (middleware/sso-callback.ts:342); confirm whether SDK applies to super_admin (see §5 Q5)

Per-Status Lifecycle

Session state is client-side only (no persisted enum in this repo). The state surface is the SDK event the app currently holds. See §2.5 state machine.

1.B Decisions Closed (index → §2 ADRs)

#DecisionChosenADR
D1Where to initialize the SDKDedicated client plugin plugins/mekariSession.client.ts§2.6-A1
D2How to gate the behaviorcentralized_session flag in AppConfigStore.appConfig§2.6-A2
D3How event handlers reach the appReuse useEventBus + a thin useCentralizedSession composable§2.6-A3
D4currentUser value passed to the SDKuseAuthStore().user.sso_id§2.6-A4
D5logged_out action; server_down actionlogged_outpages/logout.vue flow; server_down → fail-open (no action, PRD 6.10)§2.6-A5
D6Account-switch handlingNo dedicated event/state — delivered as logged_out, runs normal sign-out flow (resolved, real SDK has no switch_user)§2.6-A6
D7Current-company sync mechanism in hub-chatRe-fetch org + company stores (no new FE endpoint); decoupled from the SDK — BE/SSO concern§2.6-A7

1.C Per-Story Change Map

Story (from "FE Implementation Scope")Layer scopeChangesAcceptance criteriaRFC anchors
Add mekari-account-web-sdk Session SDK, load with sso_idFE-onlypackage.json dep (git install); plugins/mekariSession.client.ts (new); useCentralizedSession.ts (new)Unit test: SDK constructed with currentUser === user.sso_id only when toggle on§2.6-A1/A4 · §4.C ch.2
Gate behind centralized_session toggleFE-onlyAppConfigStore.ts type field (new); guard in pluginUnit test: SDK NOT constructed when appConfig.centralized_session !== true§2.6-A2 · §4.C ch.1
Handle SDK events (logged_in/logged_out/server_down)FE-onlyuseCentralizedSession.ts single session.on("event", (data) => switch(data.status)) + testsUnit test: each status triggers the mapped action (mocked navigateTo/logout)§2.2 · §2.6-A3 · §4.C ch.3
Current-company syncFE + BEFE: re-fetch OrganizationStore.getDetail() + CompanyStore.getCompanyDetail(); BE: SSO current_company (Account & Launchpad / hub-core)Org/company stores re-resolved after logged_in§2.6-A7 · §5 Q4
Wire product logout to SSO sign_outFE-onlyn/a — already implementedExisting test still green (pages/logout.vue, TheSwitchAccount.vue)§2.4
server_down handling — fail-openFE-onlyNo consumer msli; SDK owns fallback internally; take no destructive actionUnit test: server_down does NOT force logout (fail-open, PRD 6.10)§2.2 · §5 Q1 (resolved)
Performance/HA + observabilityRuntime / ConfigDatadog RUM event on SDK init + each event; CSP at nginx (deploy)RUM shows centralized_session.event with event_type; CSP header present on prod§3

1.D Design References

SurfaceFigmaNotes
Forced sign-out / re-authn/a — reuses existingExisting logout + CommonSsoCallbackLoading

Frontend RFCs normally require Figma per surface. This integration adds no new visual surface at all — every reaction (forced sign-out, fail-open) reuses existing auth/redirect screens. There is no "user has changed" toast (removed — account switching has no dedicated SDK event; see §2.2).


2. Technical Design

2.0 Repo Reading Guide (read before writing)

Repo Map (slice this RFC touches)

flowchart LR
subgraph hubchat["FE: hub-chat-v2 (THIS repo — write here)"]
plugin["plugins/mekariSession.client.ts (new)"]
composable["common/composables/useCentralizedSession.ts (new)"]
appcfg["common/store/AppConfigStore.ts (modified: +centralized_session)"]
auth["common/store/AuthStore.ts (read: sso_id, resetAuthStore)"]
eventbus["plugins/eventBus.ts + useEventBus (read/extend)"]
logout["pages/logout.vue (read: forced sign-out target)"]
appvue["app.vue (read: boot wiring reference)"]
org["OrganizationStore + CompanyStore (read: current-company refetch)"]
plugin --> composable
composable --> appcfg
composable --> auth
composable --> eventbus
composable --> org
composable -.redirect.-> logout
end

subgraph al["Account & Launchpad (READ-ONLY upstream — not modified here)"]
sdk["mekari-account-web-sdk Session (bundled git dep, v0.3.0)"]
iframe["sm.mekari.com/current (iframe)"]
svc["Session Manager (Go)"]
redis["dedicated Redis"]
iframe --> svc --> redis
end

plugin -->|new Session currentUser, interval| sdk
sdk -->|injects| iframe
iframe -->|postMessage events| sdk

Existing Code Anchors

File:lineWhat to learn
common/store/AuthStore.ts:42UserProfile.sso_id — the value the SDK needs as currentUser
common/store/AuthStore.ts:545Store shape (setup syntax); resetAuthStore, isAuthenticated, user
common/store/AppConfigStore.ts:2AppConfig interface — where to add centralized_session?: boolean
common/store/AppConfigStore.ts:76getAppConfig() idempotent fetch from /client_configs/config
app.vue:203onMounted boot wiring — pattern for when/where to start session SDK
app.vue:216listenForLogout cross-tab cleanup — the existing forced-logout precedent
plugins/eventBus.ts:6AppEventMap typed event bus — extend with session events
common/composables/useEventBus.tsDefineEventMap / useEventBus API
pages/logout.vue:315Forced sign-out: clears auth, redirects to ${SSO.url}/sign_out
middleware/sso-callback.ts:541runSsoCallback — the normal auth-code re-auth entrypoint (n/a for account-switch: switches surface as logged_out, not a dedicated re-auth round-trip — §2.2)
common/store/CompanyStore.ts:28getCompanyDetail() — current-company refetch
common/store/OrganizationStore.tsgetDetail() — org refetch

Patterns to Follow

ConcernReference fileNote
Client-only bootapp.vue:203 onMounted; *.client.ts pluginsSDK touches window/iframe → must be client-only
Feature flag gateAppConfigStore.appConfig.value?.<flag>e.g. seamless_auth_first usage in middleware/authenticated.global.ts:107
Cross-feature eventsplugins/eventBus.ts:6 + useEventBusAdd session:* keys to AppEventMap
Forced logoutpages/logout.vue (navigate to /logout)Reuse rather than re-implement cookie clearing
Store (setup syntax)common/store/CompanyStore.tsNew composable, not a store, but mirror error handling
Datadog loggingapp.vue:153 datadogRum.setUser; @datadog/browser-rumUse RUM, never console.log (lint error in prod)

Reading Order for the Agent

  1. docs/architecture/flows/login/README.md
  2. docs/architecture/flows/cross-cutting/auth-sso/README.md
  3. common/store/AuthStore.ts (focus :32, :498, :545)
  4. common/store/AppConfigStore.ts
  5. middleware/sso-callback.ts
  6. app.vue (focus :203:239)
  7. plugins/eventBus.ts + common/composables/useEventBus.ts
  8. pages/logout.vue
  9. layouts/components/TheNavbar/TheSwitchAccount/TheSwitchAccount.vue
  10. common/store/CompanyStore.ts

Existing API / Contract Check

ContractTagEvidence
Forced sign-out → ${SSO.url}/sign_outreusedpages/logout.vue:315, TheSwitchAccount.vue:299
Re-auth via /sso-callback?code=/users/sign_inreusedmiddleware/sso-callback.ts:271 performSsoSignIn
App config flag source /client_configs/configextended (add field)AppConfigStore.ts:86 — new boolean centralized_session (BE-owned)
Current company in hub-chatreusedCompanyStore.ts:28 /launchpad/v1/companies; OrganizationStore.getDetail()
mekari-account-web-sdk Sessionnew-with-justificationNot yet added to repo (grep mekari-account-web-sdk → no match; expected — new dep to add in Chunk 2). External dep, owned by A&L; cannot be satisfied by existing code. Availability RESOLVED — v0.3.0, git dependency, no CDN (§5 Q2)
sm.mekari.com/currentnew-with-justificationUpstream service (A&L). hub-chat only loads it via the SDK iframe

Source Verification

ClaimEvidence (verified)
sso_id exists on user profilecommon/store/AuthStore.ts:42 sso_id: string; and :93 initial state
App config is the FE feature-flag sourceAppConfigStore.ts:76-98 fetch; bool fields :6-65; used authenticated.global.ts:107
Typed event bus existsplugins/eventBus.ts:6 AppEventMap = DefineEventMap<{...}>
Forced logout already redirects to SSO sign_outpages/logout.vue:301-321 signOut(); TheSwitchAccount.vue:288-300
Re-auth entrypoint is /sso-callback middlewaremiddleware/sso-callback.ts:541 runSsoCallback, :564 performSsoSignIn
Cross-tab forced logout precedentapp.vue:216-223 listenForLogout
Test runner = vitestpackage.json:15 "test": "vitest --dom --pool=forks", :23 test:related
Lint / type-check commandspackage.json:13 "lint", :22 "type-check": "vue-tsc --noEmit"
SPA is static (no runtime server → CSP at nginx)memory sso-cross-domain-cookies; ssr:false / nitro static preset
hub-chat (qontak) ≠ SSO (mekari) registrable domainmemory sso-cross-domain-cookies; COOKIE_DOMAIN=.qontak.com
mekari-account-web-sdk not yet added to repogrep mekari-account-web-sdk → no match (expected, new dep; not a blocker — RESOLVED, §5 Q2)
No current_company endpoint in repogrep current_company → no match (§5 Q4)

2.1 SDK Initialization (hub-chat side)

A new client-only plugin constructs the SDK after auth is known:

// plugins/mekariSession.client.ts (new) — shape, not final code
export default defineNuxtPlugin(() => {
const appConfig = useAppConfigStore();
const { user, isAuthenticated } = storeToRefs(useAuthStore());
const { start } = useCentralizedSession();

// Gate: only when BE flag is on AND we have an sso_id.
watch(
[() => appConfig.appConfig?.centralized_session, () => user.value.sso_id, isAuthenticated],
([enabled, ssoId, authed]) => {
if (enabled && authed && ssoId) start(ssoId);
},
{ immediate: true },
);
});

useCentralizedSession() owns the SDK instance (new Session({ currentUser, interval })), the single "event" subscription and its event→action switch, and RUM logging. Periodic re-validation is achieved by passing interval (ms, ≥1000; recommended 5 * 60 * 1000 per PRD constraint 6.9) to the constructor — there is no session.refresh() to call or throttle (resolved/moot, was §5 Q3; real SDK mekari-account-web-sdk v0.3.0 exposes only constructor, destroy(), on(), off()). It is the single unit under test.

2.2 SDK Contract & Event → Action Map

Input to the SDK: currentUser = useAuthStore().user.sso_id (D4 / §2.6-A4). Subscription is the SDK's single valid event name: session.on("event", (data) => { switch (data.status) { ... } }). The callback is single-arg — there is no second error argument — and any per-status subscription such as .on("logged_in", ...) is a silent no-op on the real SDK; only the literal event name "event" fires. data.status is exactly one of logged_in, logged_out, server_down (resolved against real SDK mekari-account-web-sdk v0.3.0, session.ts — there is no switch_user status, and the payload never exposes the incoming/other user's ssoId).

data.statusMeaning (real SDK)hub-chat action
logged_inInternal data.ssoId === currentUserEnsure current company is fresh (OrganizationStore.getDetail() + CompanyStore.getCompanyDetail()); allow interaction
logged_outNo session, 2h idle, plain sign-out, or an SSO account switch (the SDK cannot distinguish a switch from a logout and never exposes the new ssoId)Route to existing forced sign-out: navigateTo("/logout") (clears auth, hits SSO sign_out)
server_downSDK's internal checkTimeout/msli fallback exhausted (already stale by the time hub-chat sees this)Fail-open (PRD constraint 6.10): take NO destructive action; do not force logout. Log/alert only.

Account switching (resolved). An SSO account switch is delivered by the SDK as logged_out — the SDK cannot distinguish a switch from a logout and does not expose the new ssoId — so hub-chat runs its normal sign-out flow; there is no dedicated switch_user handler, state, toast, or re-auth round-trip. Any post-re-login company re-sync is a separate BE/SSO concern (§2.6-A7), not SDK-driven. (Previously modeled as a distinct switch_user event; corrected against real SDK mekari-account-web-sdk v0.3.0, type Status = "logged_in" | "logged_out" | "server_down".)

msli is SDK-internal (resolved — was §5 Q1). The SDK owns the msli localStorage key (2h expiry) internally: it sets msli on logged_in and clears it on logged_out, consulting it only in its own checkTimeout fallback. hub-chat MUST NOT read or write the msli key — a consumer touching it would corrupt the SDK's own fallback logic. There is no cross-domain _mekari_account cookie check for hub-chat to perform; that concern does not apply to the real SDK contract, which owns session/cookie evaluation entirely inside the iframe.

2.3 Sequence Diagrams

Happy path — logged_in (with infra stack via SDK iframe)

sequenceDiagram
autonumber
participant U as User
participant HC as hub-chat SPA (qontak)
participant SDK as mekari-account-web-sdk
participant IF as iframe sm/current (mekari)
participant KONG as Kong Gateway
participant SM as Session Manager (Go)
participant R as Redis
U->>HC: open page (toggle centralized_session = on)
HC->>SDK: new Session({ currentUser: sso_id, interval })
SDK->>IF: inject hidden iframe (reloaded every `interval` ms)
IF->>KONG: GET sm.mekari.com/current
KONG->>SM: route /current
SM->>R: validate session, update last_request_at (idle < 2h)
R-->>SM: session { ssoId }
SM-->>IF: render page w/ ssoId (cache max 5s)
IF-->>SDK: postMessage({ source: "mekari-account-web-sdk", ssoId })
SDK-->>HC: session.on("event", { status: "logged_in" })
HC->>HC: refetch org + company
HC-->>U: allow interaction

Account switching has no dedicated diagram: a switch surfaces as logged_out (see §2.2) and follows the ordinary forced sign-out shown for logged_out above — there is no reset→re-auth→toast round-trip.

Failure path — server_down

sequenceDiagram
autonumber
participant SDK as mekari-account-web-sdk
participant HC as hub-chat SPA
SDK-->>HC: session.on("event", { status: "server_down" }) (SDK's internal msli fallback exhausted)
HC->>HC: fail-open — take no destructive action (PRD constraint 6.10), log/alert only

2.4 User Logout From Product — n/a — already implemented

The parent RFC requires product logout to also hit account.mekari.com/sign_out. hub-chat already does this:

  • pages/logout.vue:315window.location.href = ${ssoUrl}/sign_out?client_id=...
  • TheSwitchAccount.vue:299 → same redirect after $auth.signOut()

No change required. The execution plan must keep these green, not rewrite them.

2.5 Session State Machine (client-side)

stateDiagram-v2
[*] --> Unknown: app boot, toggle on
Unknown --> Active: logged_in (ssoId match)
Active --> Active: logged_in (periodic `interval` re-check)
Active --> SignedOut: logged_out (plain logout OR account switch — §2.2)
Active --> Degraded: server_down
Degraded --> Active: logged_in (recovered)
Degraded --> Degraded: server_down (fail-open — no destructive action, PRD 6.10)
SignedOut --> [*]: redirect to SSO sign_out

2.6 Technical Decisions (ADR format)

Minimum coverage: storage n/a — client-only, no DB; sync/async (SDK init); caching (msli); third-party (SDK integration mode); consistency (eventual via postMessage); multi-tenancy (n/a — single org per session); reuse vs new.

§2.6-A1 — Where to initialize the SDK

  • Context: SDK touches window + injects an iframe → client-only; must start only after sso_id and the toggle are known.
  • Options: (a) inline in app.vue onMounted; (b) dedicated *.client.ts plugin.
    • (a) pros: co-located with other boot wiring (app.vue:203). cons: app.vue is already 350 lines and high-blast-radius; adds session logic to a crowded file.
    • (b) pros: isolated, independently testable, client-only by filename convention (plugins/datadog.client.ts precedent). cons: one more plugin file.
  • Decision: (b) plugins/mekariSession.client.ts, delegating to a composable.
  • Rationale: keeps app.vue blast radius down; isolates external-SDK risk.
  • Consequences: new plugin in boot order. The real SDK is a singleton — a 2nd new Session() call returns the 1st instance and ignores new options — so "double-init" is naturally a no-op, but teardown MUST call session.destroy() (which also resets the singleton); off() alone leaves the iframe + window listener attached (R11, mekari-account-web-sdk v0.3.0).
  • Reversibility: high — delete plugin + composable, remove dep.

§2.6-A2 — Gating mechanism

  • Context: Behavior must ship dark and roll out per the parent RFC's "Next Chapter" step.
  • Options: (a) AppConfigStore boolean flag; (b) organizationSettings flag; (c) env var.
    • (a) pros: established FE flag source (AppConfigStore.ts), per-deploy/account controllable by BE. (b) pros: org-level; cons: 140+ flags, heavier, org-scoped only. (c) cons: static, no gradual rollout.
  • Decision: (a) add centralized_session?: boolean to AppConfig (AppConfigStore.ts:2), set by /client_configs/config (BE-owned).
  • Rationale: matches existing seamless_auth_first gating precedent.
  • Consequences: BE must expose the flag; FE default = off.
  • Reversibility: high — flag off disables entirely.

§2.6-A3 — Event delivery

  • Context: SDK events must drive auth/routing without coupling features.
  • Options: (a) handle inside the plugin directly; (b) thin composable useCentralizedSession + reuse useEventBus for any app-wide signals needed.
  • Decision: (b). The composable owns the SDK lifecycle and event→action switch; no app-wide toast is emitted (there is no dedicated "user changed" surface — §2.2), but the bus remains available if a future cross-feature signal is needed.
  • Rationale: testable unit; respects cross-feature comms rule (AGENTS.md).
  • Consequences: add session:* keys to AppEventMap only if cross-feature listeners are needed; otherwise direct actions.
  • Reversibility: high.

§2.6-A4 — currentUser value

  • Decision: useAuthStore().user.sso_id, passed as the SDK's currentUser option (camelCase — AuthStore.ts:42).
  • Rationale: the parent RFC's user_sso_id is exactly this field; populated by fetchUser() from /users/me.
  • Consequences: SDK must start only after fetchUser resolves (sso_id non-empty).
  • Reversibility: n/a.
  • Alternative: no alternative considered — sso_id is the only SSO identity on the profile.

§2.6-A5 — logged_out action; server_down action

  • Decision: logged_outnavigateTo("/logout") (reuse pages/logout.vue). server_downfail-open: take no destructive action (PRD constraint 6.10).
  • Rationale: pages/logout.vue already clears all auth cookies/localStorage, notifies BE, and redirects to SSO sign_out — exactly the desired forced-logout behavior for logged_out. server_down is no longer mapped to forced sign-out: by the time hub-chat sees server_down the SDK's own msli fallback is already stale, and PRD constraint 6.10 requires fail-open, not a forced logout.
  • Consequences: server_down sessions may briefly show no reaction; log/alert only.
  • Reversibility: high.
  • Alternative considered: call authStore.signOut() inline for logged_out — rejected: would duplicate pages/logout.vue's CRM + Firebase + MQTT cleanup.

§2.6-A6 — Account-switch handling (no dedicated event)

  • Decision: there is no switch_user event on the real SDK. An account switch is delivered as logged_out (§2.2); hub-chat runs its normal forced sign-out flow via pages/logout.vue — no reset→re-auth→toast round-trip is implemented.
  • Rationale: resolved against real SDK mekari-account-web-sdk v0.3.0 (type Status = "logged_in" | "logged_out" | "server_down"); the SDK cannot distinguish a switch from a logout and never exposes the incoming ssoId, so any "reset → silently re-auth → toast" design was unimplementable.
  • Consequences: a switched-account user sees the normal sign-out/sign-in experience rather than a silent hand-off; no toast copy is needed (§5 Q6 dropped).
  • Reversibility: n/a — this is a correction, not a choice.

§2.6-A7 — Current-company sync

  • Context: Parent RFC: after session created, sync current company. It names api.mekari.com/v1.1/users/{sso_id}/current_company (SSO/BE).
  • Options: (a) FE calls the SSO endpoint directly; (b) FE re-fetches existing org/company stores and lets hub-core resolve current company server-side.
    • (a) cons: cross-domain, new client contract, auth headers for SSO API unclear in hub-chat. (b) pros: hub-chat already derives "current company" from OrganizationStore.getDetail() + CompanyStore.getCompanyDetail() (CompanyStore.ts:28).
  • Decision: (b) — re-fetch existing stores; the SSO current_company call is a BE concern (hub-core / Account & Launchpad), not new hub-chat FE code.
  • Rationale: no current_company endpoint or client exists in this repo (grep confirmed); reuse is correct.
  • Consequences: depends on BE setting current company before hub-chat refetch. See §5 Q4.
  • Reversibility: high.

3. High-Availability & Security

3.1 Infrastructure Topology (FE view — upstream is READ-ONLY)

flowchart TB
browser["User Browser"]
subgraph qontak["qontak domain (hub-chat — THIS repo)"]
spa["hub-chat-v2 SPA (static, nginx)"]
sdkjs["mekari-account-web-sdk Session (bundled git dep, v0.3.0 — no CDN)"]
end
subgraph mekari["mekari domain (Account & Launchpad — read-only)"]
lb["Kong Gateway (sm.mekari.com/*)"]
sm["Session Manager pods (Go)"]
redis["dedicated Redis (cache.t3.small, RDB)"]
end
browser --> spa --> sdkjs
sdkjs -->|invisible iframe, reloaded every `interval` ms| lb
lb --> sm --> redis
sm -.postMessage via iframe.-> sdkjs

There is no CDN node: the SDK is a bundled build-time dependency (mekari-account-web-sdk v0.3.0, installed via git), not a <script> load from sm.mekari.com or any other host (R2).

Service touched by this RFCFE use caseInternal calls (owner)External / third-party
hub-chat-v2 SPALoad SDK, react to session events/client_configs/config (hub-core), /users/me (hub-core)sm.mekari.com/current via SDK iframe (A&L)

3.2 Performance (FE-side expectations)

The parent RFC sets sm/current targets (6k RPS, ~50ms avg, p95 < 100ms) — owned by A&L. FE obligations:

  • SDK init must not block first paint; toggle-gated and async.
  • Periodic re-validation is achieved by passing interval (ms, ≥1000; recommended 5 * 60 * 1000 per PRD constraint 6.9) to the constructor — there is no session.refresh() to call or throttle. (Resolved/moot — was §5 Q3; the real SDK mekari-account-web-sdk v0.3.0 exposes only constructor, destroy(), on(), off().)
  • The SDK iframe is invisible and max 5s cache — the interval value already bounds check frequency; FE does not need additional polling logic.

3.3 Security Implications (FE-side — infosec approver required)

Concernhub-chat actionOWASP
postMessage originUpstream SDK gap, not consumer-fixable (resolved finding, was "MUST verify"). The real SDK adds its own window message listener and validates only event.data.source === "mekari-account-web-sdk" — it does NOT validate event.origin, and hub-chat cannot intercept or override the SDK's own listener. Recorded as an open infosec finding for the SDK owners (Account & Launchpad). See §5 Q6.A08 / A07
SDK source integrityLoad SDK from a pinned mekari-account-web-sdk version (bundled git dependency) — no CDN, no live <script> from a third partyA08
CSP frame-src / frame-ancestorsAdd CSP at nginx ingress (SPA has no runtime server — memory sso-cross-domain-cookies) allowing the sm.mekari.com iframe origin via frame-src/child-src (NOT script-src — the SDK is bundled, not script-loaded); the SDK-side frame-ancestors whitelist of chat.qontak.com is A&L's config repoA05
No token in logsRUM events log event_type + page URL only — never sso_id raw tokenA09
iframe sandboxConfirm with A&L whether the iframe should carry sandbox attrs (SDK-controlled)A05

3.4 Observability

SignalName / sourceThreshold
SDK initRUM action centralized_session.init (Datadog, app.vue:153 precedent)present on toggle-on sessions
Each session eventRUM action centralized_session.event { event_type }logged_out/server_down rate
Forced-logout from sessionRUM centralized_session.forced_logoutspike alert

Metric naming mirrors existing RUM usage (datadogRum.setUser in app.vue:153). No existing custom RUM action name was found to copy verbatim — proposed names follow the <domain>.<action> shape; confirm naming convention with the team (§5 Q7).


4. Backwards Compatibility and Rollout Plan

4.A Compatibility

  • Default off (centralized_session flag absent/false) → zero behavior change; existing login/refresh/logout untouched.
  • hub-chat is not the first pilot — Launchpad is (parent RFC step 4). hub-chat onboards in step 5 ("The Next Chapter") after Launchpad proves the SDK and after chat.qontak.com is added to the SDK CSP / referrer-origin whitelist (A&L).

4.B Pre-merge Verification Commands (from package.json)

pnpm lint # package.json:13 — eslint + prettier, must exit 0
pnpm type-check # package.json:22 — vue-tsc --noEmit
pnpm test common/composables/__tests__/useCentralizedSession.spec.ts # package.json:15 vitest
pnpm test:related # package.json:23 — specs related to staged files

4.C Agent Execution Plan (ordered chunks)

Chunk 1 — Add the feature flag (gate first, ship dark).

  • Files: common/store/AppConfigStore.ts (add centralized_session?: boolean to AppConfig), common/store/__tests__/AppConfigStore.spec.ts (extend).
  • Commands: pnpm type-check && pnpm test common/store/__tests__/AppConfigStore.spec.ts
  • Acceptance: type-check passes; test asserts the field is readable and defaults undefined → treated as off.

Chunk 2 — useCentralizedSession composable + SDK lifecycle (TDD).

  • Files (new): common/composables/useCentralizedSession.ts, common/composables/__tests__/useCentralizedSession.spec.ts.
  • Mock mekari-account-web-sdk Session at module level (vi.mock).
  • Commands: pnpm test common/composables/__tests__/useCentralizedSession.spec.ts
  • Acceptance (assertable):
    • SDK constructed with currentUser === user.sso_id and an interval option set.
    • Subscribed via the single session.on("event", (data) => { ... }) call (single-arg callback); per-status subscriptions are asserted NOT used.
    • session.destroy() is called on teardown (singleton reset, §2.6-A1).

Chunk 3 — Event → action mapping (TDD).

  • Files: extend useCentralizedSession.ts + spec.
  • Commands: pnpm test common/composables/__tests__/useCentralizedSession.spec.ts
  • Acceptance: with navigateTo / store actions mocked — data.status === "logged_out"/logout (covers plain logout AND account switch, since the SDK cannot distinguish them, §2.2); data.status === "server_down" → fail-open, no destructive action (PRD 6.10); data.status === "logged_in" → org+company refetch.

Chunk 4 — Plugin wiring (gated, client-only).

  • Files (new): plugins/mekariSession.client.ts, plugins/__tests__/mekariSession.client.spec.ts.
  • Commands: pnpm test plugins/__tests__/mekariSession.client.spec.ts
  • Acceptance: SDK started only when centralized_session === true AND isAuthenticated AND sso_id non-empty; never started otherwise.

Chunk 5 — Observability + RUM events.

  • Files: useCentralizedSession.ts (RUM calls), spec.
  • Acceptance: RUM action emitted per event with event_type; no token/sso_id in payload.

Chunk 6 — Docs.

  • Files: docs/architecture/flows/login/README.md and/or docs/architecture/flows/cross-cutting/auth-sso/README.md — add the SDK session reconciliation flow; set status: ready.
  • Acceptance: spoke updated; diagram added.

Dependency order: Chunk 1 (flag) → 2/3 (logic) → 4 (wiring) → 5 (obs) → 6 (docs).

4.D Verification & Rollback Recipe

Post-deploy verification signals:

  1. With flag off: RUM shows no centralized_session.init → confirms dark.
  2. Enable flag for one internal company: RUM centralized_session.init present; centralized_session.event shows logged_in; manual SSO logout in another tab produces logged_out → hub-chat lands on SSO sign_out.
  3. CSP header present on chat.qontak.com responses (curl -I, check content-security-policy allows the sm.mekari.com iframe origin).

Rollback (numbered, agent-executable):

  1. Set centralized_session = false in /client_configs/config (BE flag) → SDK stops initializing immediately on next load. This is the primary lever.
  2. If code-level revert needed: revert the chunk-4 plugin PR (kills wiring) — logic composable is inert without the plugin.
  3. Confirm RUM centralized_session.init drops to zero.
  4. No migration / data rollback — client-only change.

5. Concern, Questions, or Known Limitations

#SeverityQuestion / limitationOwnerBlocks
Q1[resolved]Cross-domain fallback — RESOLVED/MOOT. The real SDK owns msli/session evaluation entirely inside the iframe; hub-chat never needs to read _mekari_account or any cross-domain cookie — it only consumes session.on("event", ...). Resolved by real SDK mekari-account-web-sdk v0.3.0, session.ts.A&L + Platformnone (resolved) — see §2.2
Q2[resolved]SDK availability — RESOLVED. mekari-account-web-sdk v0.3.0, installed via npm install git+https://<user>:<pass>@bitbucket.org/mid-kelola-indonesia/mekari-account-web-sdk#<version>, ESM (type: module, main: dist/index.js). Public API: constructor({ currentUser, sessionUrl?, interval?, checkTimeout?, includeSessionId? }), destroy(), on(), off().A&Lnone (resolved) — see §2.1/§2.2
Q3[moot]session.refresh() throttle — MOOT. There is no refresh() on the real SDK; periodic re-validation is the constructor's interval option. Resolved by real SDK mekari-account-web-sdk v0.3.0 (public API is exactly constructor/destroy()/on()/off()).A&L + Platformnone (moot) — see §2.1/§3.2
Q4[important]Current-company sync. Does hub-core set current company server-side after re-auth (so a plain org/company refetch suffices), or must hub-chat FE call SSO current_company directly? No such endpoint exists in repo. This remains a genuine BE/SSO dependency, independent of the SDK (§2.6-A7, R10).hub-core + A&L§2.6-A7, Chunk 3
Q5[important]super_admin scope. super_admin bypasses billing/MQTT today (middleware/sso-callback.ts:342). Does centralized session apply to super_admin sessions, or are they exempt?PlatformRole coverage, Chunk 4
Q6[important]event.origin not validated by the SDK (upstream infosec finding). The real SDK checks only event.data.source === "mekari-account-web-sdk" on its own window message listener and does not validate event.origin; hub-chat cannot intercept or override that listener, so this cannot be fixed in the composable. Open finding for the SDK owners (OWASP A08/A07).Account & Launchpad§3.3, §7 sign-off
Q7[nice-to-have]RUM action naming — confirm the <domain>.<action> convention for custom Datadog actions (no existing custom action name found to mirror).Platform§3.4, Chunk 5
Q8[important]Approvers unassigned, incl. mandatory infosec approver (iframe/postMessage/CSP, now including the Q6 origin finding). Metadata has placeholders.Platform lead§7 sign-off

6. Comment logs

DateAuthorNote
2026-06-27syafrizal.abdillah@mekari.comInitial FE-integration draft for hub-chat-v2 generated via rfc-starter
2026-07-02latest review (agent)Reconciled against real SDK mekari-account-web-sdk v0.3.0 (latest review): corrected package/import, currentUser, single-arg on("event"), removed switch_user/session.refresh()/consumer-msli, fixed iframe host to sm.mekari.com, recorded the event.origin security finding, reclassified resolved OQs.

7. Ready for agent execution

Ready for agent execution: no

Blocking gates (must resolve before flipping to yes):

  • Q8 [important] — mandatory infosec approver still unassigned (metadata has placeholders); now additionally covers the event.origin upstream SDK gap recorded at Q6 (§3.3).

Secondary (do not block compile but block correct behavior): Q4 (current-company sync ownership), Q5 (super_admin scope).

Q1 (cross-domain fallback) and Q2 (SDK availability) are RESOLVED — the real SDK mekari-account-web-sdk v0.3.0 is confirmed available (git dependency, no CDN) and owns msli/session evaluation entirely inside the iframe; see §2.2, §5 Q1/Q2. Q3 (refresh throttle) is moot — there is no refresh(); use interval.

The flag-gating, composable scaffold, and event-mapping chunks are well-specified and anchored to real files; with Q1 + Q2 now resolved, the remaining gate is approver assignment (Q8) plus the secondary items above.