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-v2with 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
| Field | Value | Notes |
|---|---|---|
| Status | RFC | Pending approver + infosec assignment |
| Type / Sub-type | frontend / enhancement | Touches auth/SSO boot path; no new product surface at all |
| Title | Centralized Web Session — Hub Chat v2 FE Integration | |
| Owner | Tribe Qontak Platform | Owns auth/SSO code in hub-chat-v2 |
| Authors | syafrizal.abdillah@mekari.com | |
| Reviewers | Account & Launchpad; Tribe Qontak Platform | A&L owns SDK/service contract |
| Approvers | tech-leader TBD; infosec TBD | Infosec required — CSP / iframe / postMessage origin |
| Submitted | 2026-06-27 | ISO-8601 |
| Last updated | 2026-06-27 | Matches most recent edit |
| Target release | TBD — pilot after Launchpad proves the SDK | Parent RFC names Launchpad as first pilot, then other products |
| Related docs | Parent RFC (Confluence) · login spoke · auth-sso spoke | Parent RFC is the authoritative source for SDK/service contract |
Sections at a Glance
| § | Section | Hint (frontend) |
|---|---|---|
| 1 | Overview | Problem, scope, PRD coverage, design references, per-story change map |
| 2 | Technical Design | Repo reading guide, SDK contract, event→action mapping, diagrams |
| 3 | High-Availability & Security | CSP/iframe/postMessage origin, perf expectations (FE-side) |
| 4 | Backwards Compatibility & Rollout Plan | Feature toggle gating, agent execution plan, verification & rollback |
| 5 | Concern, Questions, Known Limitations | Open questions with severity |
| 6 | Comment logs | Review trail |
| 7 | Ready for agent execution | The 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:498setupIntervalRefreshToken); there is no check against SSO session state after login. - The only cross-state reaction is cross-tab logout via MQTT/BroadcastChannel
(
app.vue:216listenForLogout), 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 section | Covered in this RFC | Notes |
|---|---|---|
| 1. Overview / Success Criteria | §1.0, §1.A | FE share of: session verified across products, 2h idle, refresh |
| 2. Proposal / SDK usage | §2.1, §2.2 | SDK init + event handling in hub-chat |
| 2. SDK contract (events) | §2.2 Event → Action map | All three real statuses mapped (logged_in/logged_out/server_down) |
| FE Product Integration Flows (SDK flow) | §2.3 sequence diagrams | logged_in / server_down (no distinct switch-user flow — see §2.2) |
| FE Product Integration Flows (Web Session) | §2.2, §2.3 | hub-chat uses auth-code exchange; current-company = org/company refetch |
| FE Product Integration Flows (OAuth2 code) | §2.2 | hub-chat's /sso-callback IS an auth-code flow; reused |
| User Logout From Product | §2.4 — n/a — already implemented | pages/logout.vue:315 + TheSwitchAccount.vue:299 already hit SSO sign_out |
| User Switch Account | §2.2 | No dedicated event; delivered as logged_out → normal sign-out flow (resolved, see §2.2) |
| 3. HA & Security | §3 | FE-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 | §5 | Q1/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 Model | n/a — no DB; FE repo | Parent RFC: "No database changes" |
| Multiple sessions per account | n/a — server/SSO concern | No FE work in hub-chat |
| Out of scope: auto token revoke on idle | n/a — out of scope | Carried over verbatim |
UI / Consumer Surface Coverage
| Surface | Trigger | Backing |
|---|---|---|
| Session-expired / forced sign-out | logged_out (covers plain logout AND account switch — §2.2) | Reuses pages/logout.vue + SSO sign_out redirect (no new UI) |
server_down | server_down | Fail-open (PRD 6.10) — no destructive action, no new UI |
| No other visible UI | — | SDK iframe is invisible; no page/route added |
Role Coverage
Role (UserProfile.role) | Behavior change |
|---|---|
| agent / member / supervisor / admin / owner | Identical: all gated only by centralized_session toggle |
| super_admin | n/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)
| # | Decision | Chosen | ADR |
|---|---|---|---|
| D1 | Where to initialize the SDK | Dedicated client plugin plugins/mekariSession.client.ts | §2.6-A1 |
| D2 | How to gate the behavior | centralized_session flag in AppConfigStore.appConfig | §2.6-A2 |
| D3 | How event handlers reach the app | Reuse useEventBus + a thin useCentralizedSession composable | §2.6-A3 |
| D4 | currentUser value passed to the SDK | useAuthStore().user.sso_id | §2.6-A4 |
| D5 | logged_out action; server_down action | logged_out → pages/logout.vue flow; server_down → fail-open (no action, PRD 6.10) | §2.6-A5 |
| D6 | Account-switch handling | No dedicated event/state — delivered as logged_out, runs normal sign-out flow (resolved, real SDK has no switch_user) | §2.6-A6 |
| D7 | Current-company sync mechanism in hub-chat | Re-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 scope | Changes | Acceptance criteria | RFC anchors |
|---|---|---|---|---|
Add mekari-account-web-sdk Session SDK, load with sso_id | FE-only | package.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 toggle | FE-only | AppConfigStore.ts type field (new); guard in plugin | Unit 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-only | useCentralizedSession.ts single session.on("event", (data) => switch(data.status)) + tests | Unit test: each status triggers the mapped action (mocked navigateTo/logout) | §2.2 · §2.6-A3 · §4.C ch.3 |
| Current-company sync | FE + BE | FE: 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_out | FE-only | n/a — already implemented | Existing test still green (pages/logout.vue, TheSwitchAccount.vue) | §2.4 |
server_down handling — fail-open | FE-only | No consumer msli; SDK owns fallback internally; take no destructive action | Unit test: server_down does NOT force logout (fail-open, PRD 6.10) | §2.2 · §5 Q1 (resolved) |
| Performance/HA + observability | Runtime / Config | Datadog 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
| Surface | Figma | Notes |
|---|---|---|
| Forced sign-out / re-auth | n/a — reuses existing | Existing 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:line | What to learn |
|---|---|
common/store/AuthStore.ts:42 | UserProfile.sso_id — the value the SDK needs as currentUser |
common/store/AuthStore.ts:545 | Store shape (setup syntax); resetAuthStore, isAuthenticated, user |
common/store/AppConfigStore.ts:2 | AppConfig interface — where to add centralized_session?: boolean |
common/store/AppConfigStore.ts:76 | getAppConfig() idempotent fetch from /client_configs/config |
app.vue:203 | onMounted boot wiring — pattern for when/where to start session SDK |
app.vue:216 | listenForLogout cross-tab cleanup — the existing forced-logout precedent |
plugins/eventBus.ts:6 | AppEventMap typed event bus — extend with session events |
common/composables/useEventBus.ts | DefineEventMap / useEventBus API |
pages/logout.vue:315 | Forced sign-out: clears auth, redirects to ${SSO.url}/sign_out |
middleware/sso-callback.ts:541 | runSsoCallback — 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:28 | getCompanyDetail() — current-company refetch |
common/store/OrganizationStore.ts | getDetail() — org refetch |
Patterns to Follow
| Concern | Reference file | Note |
|---|---|---|
| Client-only boot | app.vue:203 onMounted; *.client.ts plugins | SDK touches window/iframe → must be client-only |
| Feature flag gate | AppConfigStore.appConfig.value?.<flag> | e.g. seamless_auth_first usage in middleware/authenticated.global.ts:107 |
| Cross-feature events | plugins/eventBus.ts:6 + useEventBus | Add session:* keys to AppEventMap |
| Forced logout | pages/logout.vue (navigate to /logout) | Reuse rather than re-implement cookie clearing |
| Store (setup syntax) | common/store/CompanyStore.ts | New composable, not a store, but mirror error handling |
| Datadog logging | app.vue:153 datadogRum.setUser; @datadog/browser-rum | Use RUM, never console.log (lint error in prod) |
Reading Order for the Agent
docs/architecture/flows/login/README.mddocs/architecture/flows/cross-cutting/auth-sso/README.mdcommon/store/AuthStore.ts(focus:32,:498,:545)common/store/AppConfigStore.tsmiddleware/sso-callback.tsapp.vue(focus:203–:239)plugins/eventBus.ts+common/composables/useEventBus.tspages/logout.vuelayouts/components/TheNavbar/TheSwitchAccount/TheSwitchAccount.vuecommon/store/CompanyStore.ts
Existing API / Contract Check
| Contract | Tag | Evidence |
|---|---|---|
Forced sign-out → ${SSO.url}/sign_out | reused | pages/logout.vue:315, TheSwitchAccount.vue:299 |
Re-auth via /sso-callback?code= → /users/sign_in | reused | middleware/sso-callback.ts:271 performSsoSignIn |
App config flag source /client_configs/config | extended (add field) | AppConfigStore.ts:86 — new boolean centralized_session (BE-owned) |
| Current company in hub-chat | reused | CompanyStore.ts:28 /launchpad/v1/companies; OrganizationStore.getDetail() |
mekari-account-web-sdk Session | new-with-justification | Not 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/current | new-with-justification | Upstream service (A&L). hub-chat only loads it via the SDK iframe |
Source Verification
| Claim | Evidence (verified) |
|---|---|
sso_id exists on user profile | common/store/AuthStore.ts:42 sso_id: string; and :93 initial state |
| App config is the FE feature-flag source | AppConfigStore.ts:76-98 fetch; bool fields :6-65; used authenticated.global.ts:107 |
| Typed event bus exists | plugins/eventBus.ts:6 AppEventMap = DefineEventMap<{...}> |
| Forced logout already redirects to SSO sign_out | pages/logout.vue:301-321 signOut(); TheSwitchAccount.vue:288-300 |
Re-auth entrypoint is /sso-callback middleware | middleware/sso-callback.ts:541 runSsoCallback, :564 performSsoSignIn |
| Cross-tab forced logout precedent | app.vue:216-223 listenForLogout |
| Test runner = vitest | package.json:15 "test": "vitest --dom --pool=forks", :23 test:related |
| Lint / type-check commands | package.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 domain | memory sso-cross-domain-cookies; COOKIE_DOMAIN=.qontak.com |
mekari-account-web-sdk not yet added to repo | grep mekari-account-web-sdk → no match (expected, new dep; not a blocker — RESOLVED, §5 Q2) |
No current_company endpoint in repo | grep 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.status | Meaning (real SDK) | hub-chat action |
|---|---|---|
logged_in | Internal data.ssoId === currentUser | Ensure current company is fresh (OrganizationStore.getDetail() + CompanyStore.getCompanyDetail()); allow interaction |
logged_out | No 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_down | SDK'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 forlogged_outabove — 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:315→window.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 aftersso_idand the toggle are known. - Options: (a) inline in
app.vue onMounted; (b) dedicated*.client.tsplugin.- (a) pros: co-located with other boot wiring (
app.vue:203). cons:app.vueis 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.tsprecedent). cons: one more plugin file.
- (a) pros: co-located with other boot wiring (
- Decision: (b)
plugins/mekariSession.client.ts, delegating to a composable. - Rationale: keeps
app.vueblast 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 callsession.destroy()(which also resets the singleton);off()alone leaves the iframe + window listener attached (R11,mekari-account-web-sdkv0.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)
AppConfigStoreboolean flag; (b)organizationSettingsflag; (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.
- (a) pros: established FE flag source (
- Decision: (a) add
centralized_session?: booleantoAppConfig(AppConfigStore.ts:2), set by/client_configs/config(BE-owned). - Rationale: matches existing
seamless_auth_firstgating 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+ reuseuseEventBusfor 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 toAppEventMaponly 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'scurrentUseroption (camelCase —AuthStore.ts:42). - Rationale: the parent RFC's
user_sso_idis exactly this field; populated byfetchUser()from/users/me. - Consequences: SDK must start only after
fetchUserresolves (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_out→navigateTo("/logout")(reusepages/logout.vue).server_down→ fail-open: take no destructive action (PRD constraint 6.10). - Rationale:
pages/logout.vuealready clears all auth cookies/localStorage, notifies BE, and redirects to SSOsign_out— exactly the desired forced-logout behavior forlogged_out.server_downis no longer mapped to forced sign-out: by the time hub-chat seesserver_downthe SDK's ownmslifallback is already stale, and PRD constraint 6.10 requires fail-open, not a forced logout. - Consequences:
server_downsessions may briefly show no reaction; log/alert only. - Reversibility: high.
- Alternative considered: call
authStore.signOut()inline forlogged_out— rejected: would duplicatepages/logout.vue's CRM + Firebase + MQTT cleanup.
§2.6-A6 — Account-switch handling (no dedicated event)
- Decision: there is no
switch_userevent on the real SDK. An account switch is delivered aslogged_out(§2.2); hub-chat runs its normal forced sign-out flow viapages/logout.vue— no reset→re-auth→toast round-trip is implemented. - Rationale: resolved against real SDK
mekari-account-web-sdkv0.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).
- (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
- Decision: (b) — re-fetch existing stores; the SSO
current_companycall is a BE concern (hub-core / Account & Launchpad), not new hub-chat FE code. - Rationale: no
current_companyendpoint 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-sdkv0.3.0, installed via git), not a<script>load fromsm.mekari.comor any other host (R2).
| Service touched by this RFC | FE use case | Internal calls (owner) | External / third-party |
|---|---|---|---|
| hub-chat-v2 SPA | Load 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; recommended5 * 60 * 1000per PRD constraint 6.9) to the constructor — there is nosession.refresh()to call or throttle. (Resolved/moot — was §5 Q3; the real SDKmekari-account-web-sdkv0.3.0 exposes onlyconstructor,destroy(),on(),off().) - The SDK iframe is invisible and
max 5scache — theintervalvalue already bounds check frequency; FE does not need additional polling logic.
3.3 Security Implications (FE-side — infosec approver required)
| Concern | hub-chat action | OWASP |
|---|---|---|
| postMessage origin | Upstream 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 integrity | Load SDK from a pinned mekari-account-web-sdk version (bundled git dependency) — no CDN, no live <script> from a third party | A08 |
CSP frame-src / frame-ancestors | Add 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 repo | A05 |
| No token in logs | RUM events log event_type + page URL only — never sso_id raw token | A09 |
| iframe sandbox | Confirm with A&L whether the iframe should carry sandbox attrs (SDK-controlled) | A05 |
3.4 Observability
| Signal | Name / source | Threshold |
|---|---|---|
| SDK init | RUM action centralized_session.init (Datadog, app.vue:153 precedent) | present on toggle-on sessions |
| Each session event | RUM action centralized_session.event { event_type } | logged_out/server_down rate |
| Forced-logout from session | RUM centralized_session.forced_logout | spike alert |
Metric naming mirrors existing RUM usage (
datadogRum.setUserinapp.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_sessionflag 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.comis 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(addcentralized_session?: booleantoAppConfig),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-sdkSessionat module level (vi.mock). - Commands:
pnpm test common/composables/__tests__/useCentralizedSession.spec.ts - Acceptance (assertable):
- SDK constructed with
currentUser === user.sso_idand anintervaloption 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).
- SDK constructed with
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 === trueANDisAuthenticatedANDsso_idnon-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.mdand/ordocs/architecture/flows/cross-cutting/auth-sso/README.md— add the SDK session reconciliation flow; setstatus: 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:
- With flag off: RUM shows no
centralized_session.init→ confirms dark. - Enable flag for one internal company: RUM
centralized_session.initpresent;centralized_session.eventshowslogged_in; manual SSO logout in another tab produceslogged_out→ hub-chat lands on SSO sign_out. - CSP header present on
chat.qontak.comresponses (curl-I, checkcontent-security-policyallows thesm.mekari.comiframe origin).
Rollback (numbered, agent-executable):
- Set
centralized_session = falsein/client_configs/config(BE flag) → SDK stops initializing immediately on next load. This is the primary lever. - If code-level revert needed: revert the chunk-4 plugin PR (kills wiring) — logic composable is inert without the plugin.
- Confirm RUM
centralized_session.initdrops to zero. - No migration / data rollback — client-only change.
5. Concern, Questions, or Known Limitations
| # | Severity | Question / limitation | Owner | Blocks |
|---|---|---|---|---|
| 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 + Platform | none (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&L | none (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 + Platform | none (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? | Platform | Role 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
| Date | Author | Note |
|---|---|---|
| 2026-06-27 | syafrizal.abdillah@mekari.com | Initial FE-integration draft for hub-chat-v2 generated via rfc-starter |
| 2026-07-02 | latest 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 theevent.originupstream 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.