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 beyond a toast
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/sdk Session SDK that injects an iframe to account.mekari.com/sm/current, reads the SSO session, and emits events (logged_in, logged_out, switch_user, server_down). 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 four events mapped
FE Product Integration Flows (SDK flow)§2.3 sequence diagramslogged_in / switch_user / server_down
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.2 switch_user, §2.3Maps to destroy session → re-auth via existing /sso-callback
3. HA & Security§3FE-side: CSP, iframe sandbox, postMessage origin
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§5Plus FE-specific cross-domain blockers
Local Storage msli / Cookies§2.2 fallback, §5 Q1Cross-domain conflict — see §5 Q1
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 / server_downReuses pages/logout.vue + SSO sign_out redirect (no new UI)
"User has changed" notificationswitch_user (after re-auth)Reuses notification:show eventBus + Pixel toast (new copy only)
Loading during re-authswitch_user redirectReuses CommonSsoCallbackLoading on /sso-callback
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
D4current_user value passed to the SDKuseAuthStore().user.sso_id§2.6-A4
D5logged_out / server_down actionRoute to existing pages/logout.vue flow§2.6-A5
D6switch_user actionReset auth → existing SSO re-auth via /sso-callback§2.6-A6
D7Current-company sync mechanism in hub-chatRe-fetch org + company stores (no new FE endpoint)§2.6-A7

1.C Per-Story Change Map

Story (from "FE Implementation Scope")Layer scopeChangesAcceptance criteriaRFC anchors
Add @mekari/sdk Session SDK, load with sso_idFE-onlypackage.json dep; plugins/mekariSession.client.ts (new); useCentralizedSession.ts (new)Unit test: SDK constructed with current_user === 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 4 SDK eventsFE-onlyuseCentralizedSession.ts event→action switch + testsUnit test: each event 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/switch_user§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
msli fallback + server_down sign-outFE-onlymsli localStorage read/write in composableUnit test: server_down → logout flow; msli written on logged_in§2.2 fallback · §5 Q1
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
"User has changed" toastn/a — no new designReuses Pixel toast via notification:show; copy only (needs UX copy)
Forced sign-out / re-authn/a — reuses existingExisting logout + CommonSsoCallbackLoading

Frontend RFCs normally require Figma per surface. This integration adds no new visual surface beyond a toast string; all other UX reuses existing auth/redirect screens. The only design input outstanding is UX copy for the "user has changed" toast (see §5 Q6).


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/sdk Session (npm pkg)"]
iframe["account.mekari.com/sm/current (iframe)"]
svc["Session Manager (Go)"]
redis["dedicated Redis"]
iframe --> svc --> redis
end

plugin -->|new Session current_user| sdk
sdk -->|injects| iframe
iframe -->|postMessage events| sdk

Existing Code Anchors

File:lineWhat to learn
common/store/AuthStore.ts:32UserProfile.sso_id — the value the SDK needs as current_user
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 re-auth entrypoint reused for switch_user
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:152 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/sdk Sessionnew-with-justificationNot present in repo (grep @mekari/sdk → no match). External dep, owned by A&L; cannot be satisfied by existing code. Availability unverified — §5 Q2
account.mekari.com/sm/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/sdk NOT in repogrep `@mekari/sdk
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, the event→action map, the msli fallback, throttled session.refresh(), and RUM logging. It is the single unit under test.

2.2 SDK Contract & Event → Action Map

Input to the SDK: current_user = useAuthStore().user.sso_id (D4 / §2.6-A4).

EventMeaning (parent RFC)hub-chat action
logged_inSession exists, same user_sso_idWrite msli = now; ensure current company is fresh (OrganizationStore.getDetail() + CompanyStore.getCompanyDetail()); allow interaction
logged_outNo session (logout or 2h idle on SSO/any product)Route to existing forced sign-out: navigateTo("/logout") (clears auth, hits SSO sign_out)
switch_userSession exists but different user_sso_idauthStore.resetAuthStore() → redirect to SSO re-auth → on return via /sso-callback, refetch company → toast "user has changed"
server_downSDK exhausted backoff retriesSuggested: same as logged_out (forced sign-out) to keep session behavior consistent across Mekari (parent RFC)

msli fallback (FE-owned portion only). msli is a localStorage timestamp on the qontak origin written on each logged_in. If the SDK reports server_down, hub-chat MAY treat a fresh msli (now − msli < 2h) as a soft "still logged in" grace before forcing sign-out. The _mekari_account cookie portion of the parent RFC's fallback is NOT readable by hub-chat (cross-domain) — it can only be evaluated inside the SDK/iframe (mekari origin). This is a contract boundary, not a hub-chat task. See §5 Q1.

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/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({ current_user: sso_id })
SDK->>IF: inject iframe (sends _mekari_account cookie)
IF->>KONG: GET account.mekari.com/sm/current
KONG->>SM: route /sm/*
SM->>R: validate session, update last_request_at (idle < 2h)
R-->>SM: session { user_sso_id }
SM-->>IF: render page w/ user_sso_id (cache max 5s)
IF-->>SDK: postMessage(user_sso_id)
SDK-->>HC: event "logged_in"
HC->>HC: msli = now; refetch org + company
HC-->>U: allow interaction

switch_user — re-auth via existing /sso-callback

sequenceDiagram
autonumber
participant U as User
participant HC as hub-chat SPA
participant SDK as @mekari/sdk
participant SSO as account.mekari.com
participant CB as /sso-callback (hub-chat)
SDK-->>HC: event "switch_user" (different user_sso_id)
HC->>HC: authStore.resetAuthStore() (clear product session)
HC->>SSO: redirect account.mekari.com/auth?return_to=/sso-callback
SSO-->>CB: redirect /sso-callback?code=AUTH_CODE (autologin via valid _mekari_account)
CB->>CB: runSsoCallback → performSsoSignIn → storeLoginTokens
CB->>HC: navigate home; refetch org + company
HC-->>U: toast "Your account has changed"

Failure path — server_down

sequenceDiagram
autonumber
participant SDK as @mekari/sdk
participant HC as hub-chat SPA
participant LO as /logout
participant SSO as account.mekari.com
SDK-->>HC: event "server_down" (backoff exhausted)
alt msli fresh (< 2h) AND grace enabled
HC->>HC: soft-allow; schedule re-check
else no msli / stale
HC->>LO: navigateTo("/logout")
LO->>SSO: redirect /sign_out?client_id=...
end

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 (sso_id match)
Active --> Active: logged_in (refresh / activity)
Active --> Reauth: switch_user
Reauth --> Active: /sso-callback success
Active --> SignedOut: logged_out
Active --> Degraded: server_down
Degraded --> Active: logged_in (recovered)
Degraded --> SignedOut: msli stale / grace expired
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; must guard against double-init.
  • 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 app-wide signals (e.g. toast).
  • Decision: (b). The composable owns the SDK lifecycle and event→action switch; it emits notification:show via the existing bus for the "user changed" toast.
  • 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 — current_user value

  • Decision: useAuthStore().user.sso_id (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 / server_down action

  • Decision: navigateTo("/logout") (reuse pages/logout.vue).
  • Rationale: that page already clears all auth cookies/localStorage, notifies BE, and redirects to SSO sign_out — exactly the desired forced-logout behavior.
  • Consequences: consistent with parent RFC's "suggested" server_down handling.
  • Reversibility: high.
  • Alternative considered: call authStore.signOut() inline — rejected: would duplicate pages/logout.vue's CRM + Firebase + MQTT cleanup.

§2.6-A6 — switch_user action

  • Decision: authStore.resetAuthStore() → redirect to SSO auth?return_to=/sso-callback → existing /sso-callback middleware completes re-auth → refetch company → toast.
  • Rationale: reuses the verified auth-code re-auth path; SSO autologin works while _mekari_account is valid (parent RFC).
  • Consequences: brief redirect; user sees CommonSsoCallbackLoading.
  • Reversibility: high.

§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/sdk Session (bundled)"]
end
subgraph mekari["mekari domain (Account & Launchpad — read-only)"]
cdn["account.mekari.com/sm/sdk.js"]
lb["Kong Gateway (account.mekari.com/sm/*)"]
sm["Session Manager pods (Go)"]
redis["dedicated Redis (cache.t3.small, RDB)"]
end
browser --> spa --> sdkjs
sdkjs -->|invisible iframe + _mekari_account cookie| lb
lb --> sm --> redis
sm -.postMessage via iframe.-> sdkjs
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)account.mekari.com/sm/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.
  • session.refresh() MUST be throttled (interval TBD — coordinate with A&L; the parent RFC marks the throttle "TBD"). See §5 Q3.
  • The SDK iframe is invisible and max 5s cache — FE must not poll on a tight loop.

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

Concernhub-chat actionOWASP
postMessage originThe composable MUST verify event.origin === <account.mekari.com env-resolved> before trusting any SDK messageA07 / A08
SDK source integrityLoad SDK from a pinned @mekari/sdk version (bundled) — not a 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 mekari iframe origin; 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 token / _mekari_accountA09
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:152 precedent)present on toggle-on sessions
Each session eventRUM action centralized_session.event { event_type }switch_user/server_down rate
Forced-logout from sessionRUM centralized_session.forced_logoutspike alert

Metric naming mirrors existing RUM usage (datadogRum.setUser in app.vue:152). 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/sdk Session at module level (vi.mock).
  • Commands: pnpm test common/composables/__tests__/useCentralizedSession.spec.ts
  • Acceptance (assertable):
    • SDK constructed with current_user === user.sso_id.
    • postMessage with mismatched origin is ignored (no action fires).
    • msli written on logged_in.

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 — logged_out/logout; server_down/logout (or soft-grace when msli fresh); switch_userresetAuthStore + SSO redirect; 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 mekari 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[critical]Cross-domain fallback. Parent RFC's fallback checks "_mekari_account valid", but hub-chat is on qontak and cannot read the mekari cookie (memory sso-cross-domain-cookies). Confirm the entire _mekari_account evaluation happens inside the SDK/iframe and hub-chat only consumes events + its own msli. If hub-chat is expected to read _mekari_account, the design is structurally impossible.A&L + Platform§2.2 fallback, Chunk 3
Q2[critical]SDK availability. @mekari/sdk Session is not in this repo or resolvable (grep empty). Need package name, registry/access, version, and CJS/ESM entry confirmed before any chunk can run.A&LAll chunks (esp. 2, 4)
Q3[important]session.refresh() throttle interval is "TBD" in the parent RFC. hub-chat's existing token refresh runs every 1s tick (AuthStore.ts:509); need the agreed refresh cadence to avoid hammering sm/current.A&L + Platform§3.2, Chunk 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.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[nice-to-have]UX copy for the "your account has changed" toast (no Figma; copy only).Product / UX§1.D, Chunk 3
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). 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

7. Ready for agent execution

Ready for agent execution: no

Blocking gates (must resolve before flipping to yes):

  • Q1 [critical] — cross-domain _mekari_account fallback semantics undefined; potential structural impossibility for hub-chat.
  • Q2 [critical]@mekari/sdk Session package not available/verified; no chunk can compile or run without it.
  • Secondary (do not block compile but block correct behavior): Q3 (refresh throttle), Q4 (current-company sync ownership), Q5 (super_admin scope), Q8 (approvers/infosec).

The flag-gating, composable scaffold, and event-mapping chunks are well-specified and anchored to real files; once Q1 + Q2 are answered, this RFC is executable end-to-end.