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 beyond a toast |
| 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/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 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 four events mapped |
| FE Product Integration Flows (SDK flow) | §2.3 sequence diagrams | logged_in / switch_user / server_down |
| 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 switch_user, §2.3 | Maps to destroy session → re-auth via existing /sso-callback |
| 3. HA & Security | §3 | FE-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 | §5 | Plus FE-specific cross-domain blockers |
Local Storage msli / Cookies | §2.2 fallback, §5 Q1 | Cross-domain conflict — see §5 Q1 |
| 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 / server_down | Reuses pages/logout.vue + SSO sign_out redirect (no new UI) |
| "User has changed" notification | switch_user (after re-auth) | Reuses notification:show eventBus + Pixel toast (new copy only) |
| Loading during re-auth | switch_user redirect | Reuses CommonSsoCallbackLoading on /sso-callback |
| 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 | current_user value passed to the SDK | useAuthStore().user.sso_id | §2.6-A4 |
| D5 | logged_out / server_down action | Route to existing pages/logout.vue flow | §2.6-A5 |
| D6 | switch_user action | Reset auth → existing SSO re-auth via /sso-callback | §2.6-A6 |
| D7 | Current-company sync mechanism in hub-chat | Re-fetch org + company stores (no new FE endpoint) | §2.6-A7 |
1.C Per-Story Change Map
| Story (from "FE Implementation Scope") | Layer scope | Changes | Acceptance criteria | RFC anchors |
|---|---|---|---|---|
Add @mekari/sdk Session SDK, load with sso_id | FE-only | package.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 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 4 SDK events | FE-only | useCentralizedSession.ts event→action switch + tests | Unit test: each event 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/switch_user | §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 |
msli fallback + server_down sign-out | FE-only | msli localStorage read/write in composable | Unit test: server_down → logout flow; msli written on logged_in | §2.2 fallback · §5 Q1 |
| 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 |
|---|---|---|
| "User has changed" toast | n/a — no new design | Reuses Pixel toast via notification:show; copy only (needs UX copy) |
| 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 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:line | What to learn |
|---|---|
common/store/AuthStore.ts:32 | UserProfile.sso_id — the value the SDK needs as current_user |
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 re-auth entrypoint reused for switch_user |
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:152 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/sdk Session | new-with-justification | Not 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/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/sdk NOT in repo | grep `@mekari/sdk |
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, 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).
| Event | Meaning (parent RFC) | hub-chat action |
|---|---|---|
logged_in | Session exists, same user_sso_id | Write msli = now; ensure current company is fresh (OrganizationStore.getDetail() + CompanyStore.getCompanyDetail()); allow interaction |
logged_out | No session (logout or 2h idle on SSO/any product) | Route to existing forced sign-out: navigateTo("/logout") (clears auth, hits SSO sign_out) |
switch_user | Session exists but different user_sso_id | authStore.resetAuthStore() → redirect to SSO re-auth → on return via /sso-callback, refetch company → toast "user has changed" |
server_down | SDK exhausted backoff retries | Suggested: 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: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 (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 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; 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)
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 app-wide signals (e.g. toast). - Decision: (b). The composable owns the SDK lifecycle and event→action switch;
it emits
notification:showvia the existing bus for the "user changed" toast. - 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 — current_user value
- Decision:
useAuthStore().user.sso_id(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 / server_down action
- Decision:
navigateTo("/logout")(reusepages/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_downhandling. - Reversibility: high.
- Alternative considered: call
authStore.signOut()inline — rejected: would duplicatepages/logout.vue's CRM + Firebase + MQTT cleanup.
§2.6-A6 — switch_user action
- Decision:
authStore.resetAuthStore()→ redirect to SSOauth?return_to=/sso-callback→ existing/sso-callbackmiddleware completes re-auth → refetch company → toast. - Rationale: reuses the verified auth-code re-auth path; SSO autologin works while
_mekari_accountis 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).
- (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/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 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) | 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 5scache — FE must not poll on a tight loop.
3.3 Security Implications (FE-side — infosec approver required)
| Concern | hub-chat action | OWASP |
|---|---|---|
| postMessage origin | The composable MUST verify event.origin === <account.mekari.com env-resolved> before trusting any SDK message | A07 / A08 |
| SDK source integrity | Load SDK from a pinned @mekari/sdk version (bundled) — not a 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 mekari iframe origin; 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 / _mekari_account | 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:152 precedent) | present on toggle-on sessions |
| Each session event | RUM action centralized_session.event { event_type } | switch_user/server_down rate |
| Forced-logout from session | RUM centralized_session.forced_logout | spike alert |
Metric naming mirrors existing RUM usage (
datadogRum.setUserinapp.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_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/sdkSessionat 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).
msliwritten onlogged_in.
- 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 —logged_out→/logout;server_down→/logout(or soft-grace whenmslifresh);switch_user→resetAuthStore+ 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 === 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 the mekari iframe 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 | [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&L | All 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? | Platform | Role 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
| Date | Author | Note |
|---|---|---|
| 2026-06-27 | syafrizal.abdillah@mekari.com | Initial 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_accountfallback semantics undefined; potential structural impossibility for hub-chat. - Q2
[critical]—@mekari/sdkSession 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.