RFC: Centralized Web Session — Hub FE Integration with mekari-session SDK
Document Conventions (do not remove)
Governance follows the Qontak RFC Template (metadata table + sections 1–6 + Comment logs). This RFC is agent-execution-ready: §1 references, §2 Repo Reading Guide, mermaid diagrams, and §4 Agent Execution Plan + Verification & Rollback Recipe must be complete before §7 flips to yes.
Scope of THIS RFC = Hub frontend integration only (the
local/hubrepo). The cross-product SDK (@mekari/sdk), the Golang Session Manager service, the dedicated Redis, and the SSO/Kong changes are owned by Account & Launchpad and are treated here as upstream dependencies — not in scope to build.
Metadata
| Field | Value | Notes |
|---|---|---|
| Status | RFC | One of IDEA / RFC / ABANDON / AGREED |
| Type / Sub-type | frontend / enhancement | Hub consumes a new shared SDK |
| Title | Centralized Web Session — Hub FE Integration | — |
| Owner | Hub (Qontak) | Consuming squad |
| Authors | Syafrizal Muhammad | — |
| Reviewers | Hub Tech Lead · Account & Launchpad | A&L owns SDK + Session Manager |
| Approver(s) | Hub EM · Infosec [REQUIRED] | Infosec mandatory (iframe / postMessage / CSP) |
| Submitted | 2026-06-27 | ISO-8601 |
| Last updated | 2026-06-27 | ISO-8601 |
| Target release | 2026-Q3 | After Launchpad pilot (rollout step 4) |
| Related docs | Source RFC (Confluence) | See frontmatter |
| Discussion | [REQUIRED] | Slack thread |
Sections at a Glance
| § | Section | Hub-FE hints |
|---|---|---|
| 1 | Overview | Problem, scope, PRD coverage, design references, decision index |
| 2 | Technical Design | Repo Reading Guide, SDK wiring, event handling, sequences, ER/state |
| 3 | High-Availability & Security | iframe/postMessage/CSP/referrer, observability |
| 4 | Backwards Compatibility & Rollout | feature toggle, agent execution plan, verify/rollback |
| 5 | Concerns / Questions / Limitations | Open questions with severity |
| 6 | Comment logs | Review trail |
| 7 | Ready for agent execution | Gate marker |
1. Overview
1.1 Problem
Hub maintains its own session (bearer token in qontak._token.hub cookie, see
schemes/hubAuthScheme.js:152) independently of the SSO session. When a user
logs out on SSO, or switches account on SSO, or is idle on SSO for >2h, Hub does
not notice — it stays logged in to the stale account/company. The source RFC
documents two concrete bugs (stale session after SSO logout; wrong company shown
after account switch). Hub already redirects to SSO for login
(middleware/login.js:22) and to ${SSO_ACCOUNT_URL}/sign_out on logout
(components/layouts/main/SwitchAccount.vue:409), but there is no continuous
session-status check while the user sits inside Hub.
This RFC integrates the new mekari-session SDK into Hub so Hub reacts to SSO
session state (logged_in / logged_out / switch_user / server_down) on every page.
1.2 Success Criteria (Hub-scoped slice of source RFC)
- Hub loads the
mekari-sessionSDK on authenticated pages, passing the currentuser_sso_id(available asthis.$auth.user.sso_id, seecomponents/layouts/main/InitComponent.vue:1138). - Hub signs out when the SDK reports
logged_out(no SSO session, or >2h idle). - Hub re-authenticates / adjusts company when the SDK reports
switch_user. - Behaviour is gated by a
centralized_sessiontoggle; OFF = today's behaviour, byte-for-byte. server_downfalls back to themslilocal-storage heuristic, then to sign-out, to keep session behaviour consistent across Mekari.
1.3 Out of Scope
- Building the
@mekari/sdkpackage, Session Manager (Golang), dedicated Redis, SSO Kong pathaccount.mekari.com/sm/*— owned by Account & Launchpad. - Auto-revoke of access/refresh tokens on inactivity (explicitly out of scope in source RFC).
- Multiple-sessions-per-account UX (server-side; rolls out in parallel, no Hub FE change required for the base integration).
- Hub backend changes for
current_companysync — see §5 OQ-1 (cross-squad, unverified; blocks the company-sync half of this RFC).
1.4 Detail 1.A — PRD Section Coverage
| Source RFC section | Covered in | Note |
|---|---|---|
| 1. Overview (web + FE-app bugs) | §1.1 | Hub is a web-session product |
| Success Criteria | §1.2 | Hub-scoped subset |
| Out of Scope | §1.3 | inherited |
| Dependencies (SDK, Session Mgr, Redis) | §1.6, §2.0 | upstream, READ-ONLY for Hub |
| 2. Technical Design — Proposal | §2 | Hub uses SDK on all authed pages |
| SDK usage / contract / events | §2.4, §2.6 | four events handled |
Local Storage msli | §2.5, §2.6 | fallback handler |
Cookies _mekari_account | §2.0, §3 | set by SSO/Rails; Hub only reads via iframe |
| FE Product Integration Flows | §2.6 sequences | Hub = OAuth2 auth-code flow |
| Web Session Flow (client_credentials) | n/a — Hub uses auth-code flow | see ADR-3 |
| OAuth2 Authorization Code Flow | §2.6 | matches existing sso-callback |
| User Logout / Switch Account | §2.6, §2.7 | maps to doSignOut / re-auth |
| Database Model (no change) | §2.8 | no DB; FE-only |
| 3. HA & Security | §3 | Hub-side CSP/referrer + observability |
| 4. Rollout Plan | §4 | Hub = "Next Chapter" consumer (step 5) |
| 5. Open Questions | §5 | extended with Hub-specific OQs |
| FE Implementation Scope (per repo) | §4.C | Hub execution plan |
1.5 Detail 1.A — UI / Consumer Surface Coverage
| Surface | Role(s) | Backing | Note |
|---|---|---|---|
Every authenticated Hub page (under layouts/hub.vue) | all | SDK loaded once via boot component | SDK injects iframe; no visible UI |
| "User has changed" notification (on switch_user) | all | Pixel toast / mp-broadcast | source RFC step: "continue with user-has-changed notification" |
| Sign-out redirect | all | existing doSignOut() | reused |
| No new page / route | — | — | FE behavioural change only |
1.5b Detail 1.A — Role Coverage
| Role | Behaviour | Note |
|---|---|---|
| super_admin | same event handling | no role-specific branch in SDK flow |
| admin | same | — |
| supervisor | same; existing online-status side-effects on sign-out (SwitchAccount.vue:383) preserved | — |
| agent | same; same online-status side-effects | — |
Authorization is not endpoint-gated by this RFC (no new authz surface) — see
§3 Role × Endpoint Authorization (n/a).
1.6 Detail 1.B — Decisions Closed (index → §2 ADR blocks)
| # | Decision | Chosen | ADR |
|---|---|---|---|
| 1 | Where to load the SDK | Boot component (InitComponent.vue) under layouts/hub.vue | ADR-1 |
| 2 | How to load the external SDK script | Nuxt plugin pattern (cf. plugins/hotjar.js) + dynamic import | ADR-2 |
| 3 | Which session-sync flow Hub follows | OAuth2 Authorization Code flow | ADR-3 |
| 4 | Feature-flag source for centralized_session | store/organization.js feature_flag map | ADR-4 |
| 5 | Map SDK events onto existing auth primitives | Reuse $auth.logout / doSignOut / middleware/login redirect | ADR-5 |
| 6 | server_down behaviour | msli heuristic → then sign-out | ADR-6 |
1.7 Detail 1.C — Per-Story Change Map
| Story | Layer scope | Changes | Acceptance criteria | RFC anchors |
|---|---|---|---|---|
Load SDK with user_sso_id on authed pages | FE-only | plugins/mekari-session.js (new), components/inbox/.../MekariSession.vue or mixin (new), wire in InitComponent.vue | Unit test: SDK constructed with sso_id from $auth.user; toggle OFF → not constructed | §2.4, §4.C-2 |
Gate behind centralized_session toggle | FE-only | read organization.feature_flag.centralized_session | Test: OFF → zero SDK calls (no iframe) | §2.3 ADR-4, §4.C-1 |
Handle logged_out | Runtime / behavior | call existing doSignOut() | Test: event → doSignOut invoked once | §2.6, §4.C-3 |
Handle switch_user | Runtime / behavior | revoke tokens → auth-code re-login → notify | Test: event → $auth.logout + redirect to SSO auth | §2.6, §4.C-4 |
Handle server_down | Runtime / behavior | msli heuristic, then doSignOut() | Test: msli>2h → sign-out; msli<2h+valid → stay | §2.6, §4.C-5 |
Handle logged_in + company sync | FE + BE | call Hub BE current-company sync endpoint (new, OQ-1) | Test (FE): on logged_in, sync action dispatched | §2.4, §5 OQ-1 |
| Wire logout to also hit SSO sign_out | FE-only | already done (SwitchAccount.vue:409) | reuse — no change | n/a — already implemented |
msli write on confirmed login | FE-only | write msli timestamp on logged_in | Test: localStorage msli set to now | §2.5, §4.C-5 |
2. Technical Design
2.0 Repo Reading Guide (read before writing)
Repo Map (slice this RFC touches)
flowchart LR
subgraph hub["local/hub (FE — write here)"]
layout["layouts/hub.vue\n(authed shell, read)"]
init["components/layouts/main/InitComponent.vue\n(boot, modify)"]
sess["plugins/mekari-session.js\n(new)"]
mixin["assets/mixins/session/centralizedSession.js\n(new)"]
auth["schemes/hubAuthScheme.js\n(read — logout/refresh)"]
sw["components/layouts/main/SwitchAccount.vue\n(read — doSignOut, EventBus)"]
mwlogin["middleware/login.js\n(read — auth-code redirect)"]
org["store/organization.js\n(read — feature_flag)"]
ep["common/constants/endpoint.js\n(modify — current_company key)"]
end
subgraph al["account.mekari.com (Account & Launchpad — READ-ONLY)"]
sdk["@mekari/sdk Session\n(/sm/sdk.js)"]
sm["Session Manager /sm/current\n(iframe + postMessage)"]
end
layout --> init
init --> mixin
mixin --> sess
sess --> sdk
sdk -. iframe + postMessage .-> sm
mixin --> auth
mixin --> sw
mixin --> mwlogin
mixin --> org
mixin --> ep
Existing Code Anchors
| File | What to learn |
|---|---|
layouts/hub.vue:1 | Authenticated shell; renders InitComponent; gated by $auth.loggedIn |
components/layouts/main/InitComponent.vue | Boot component; has this.$auth.user.sso_id (:1138); cross-tab logout relay (:602-611) emits EventBus user-sign-out |
components/layouts/main/SwitchAccount.vue:376 | doSignOut() — full sign-out (MQTT, FCM, moengage, $auth.logout, redirect to SSO sign_out) |
components/layouts/main/SwitchAccount.vue:299 | EventBus.$on('user-sign-out', this.doSignOut) + $off at :302 — the relay hook to reuse |
schemes/hubAuthScheme.js:36 | logout() — revokes via endpoint + clears all token cookies (removeChatTokenCookie etc.) |
middleware/login.js:22 | SSO authorization-code redirect (/auth/?client_id=...&redirect_uri=.../sso-callback) — the re-auth pattern |
middleware/sso-callback.js:130 | doSSOValidation() — auth-code callback handler; already uses window.opener.postMessage (:23) |
store/organization.js:30 | feature_flag: {} state + getter (:44); flags come from org payload |
store/preferences.js:91 | alt flag source: getFeatureFlag action (server settings) returning res.data[param] |
plugins/hotjar.js:1 | Pattern for a third-party SDK boot plugin registered in nuxt.config.js:96 |
utils/general.js:104 | storeUserLoggedInLocalStorage — example local-storage helper pattern for msli |
common/constants/endpoint.js | Canonical API URL registry (per AGENTS.md) — register current_company key here |
Patterns to Follow
| Concern | Reference file | Note |
|---|---|---|
| Third-party SDK boot | plugins/hotjar.js:1-3 | init in a Nuxt plugin; register in nuxt.config.js:84 plugins array |
| Cross-tab / decoupled events | components/layouts/main/InitComponent.vue:610, SwitchAccount.vue:299-302 | EventBus.$on/$off with beforeDestroy cleanup (AGENTS.md leak rule) |
| Feature flag (org payload) | store/organization.js:30,44 | v-if / computed getter; never v-show |
| Sign-out | SwitchAccount.vue:376-414 | reuse doSignOut(); do not re-implement |
| Re-auth (auth-code) | middleware/login.js:19-24 | redirect to SSO /auth with redirect_uri=.../sso-callback |
| Token revoke / reset | schemes/hubAuthScheme.js:36-48, :24-34 | $auth.logout(payload) then reset() |
| Mixin scoping | AGENTS.md "Mixins" row | assets/mixins/<domain>/; remove listeners in beforeDestroy |
Detail 2.0 — Source Verification (anti-hallucination)
| Claim | Evidence |
|---|---|
user_sso_id available as $auth.user.sso_id | components/layouts/main/InitComponent.vue:1138 const ssoId = this.$auth.user.sso_id; also middleware/utils.js:10 $auth.user.sso_id |
| Hub uses OAuth2 authorization-code flow with SSO | middleware/sso-callback.js:14 builds data.auth = route.query.code; middleware/login.js:22 redirect with response_type=code |
| Sign-out already redirects to SSO sign_out | components/layouts/main/SwitchAccount.vue:407-410 window.location.replace(\${SSO_ACCOUNT_URL}/sign_out...`)` |
| EventBus relay exists for sign-out | SwitchAccount.vue:299 $on('user-sign-out', this.doSignOut); InitComponent.vue:610 $emit('user-sign-out') |
| Custom auth scheme owns logout/refresh | schemes/hubAuthScheme.js:23 class HubAuthScheme extends LocalScheme; logout :36, refresh :210 |
| Org feature_flag map exists | store/organization.js:30 feature_flag: {}, :1665 state.feature_flag = payload |
| postMessage already used in callback | middleware/sso-callback.js:23 window.opener.postMessage({ type: 'SSO_LOGIN_SUCCESS' }, '*') |
| Third-party SDK boot pattern | plugins/hotjar.js:1-3; registered nuxt.config.js:96 |
| Test/build commands | package.json:21 "test": "jest --coverage", :9 "build": "nuxt build", :19 "lint" |
@mekari/sdk not yet a dependency | package.json:34-60 deps list contains @mekari/pixel only, no @mekari/sdk → SDK add is new |
No existing current_company usage in Hub FE | repo grep `current_company |
| SSO env vars exist | nuxt.config.js:250 env block; SSO_ACCOUNT_URL/SSO_URL/SSO_UNIFIED_CLIENT_ID used across middleware/*, SwitchAccount.vue |
2.1 Current State
Each page under layouts/hub.vue boots InitComponent.vue (MQTT, FCM, identity).
Session is purely local: bearer token cookie + $auth state. SSO is contacted
only at login (middleware/login.js) and logout (SwitchAccount.vue). No
continuous SSO session check exists.
2.2 Proposal
Add a thin, toggle-gated centralized-session mixin booted from
InitComponent.vue. When organization.feature_flag.centralized_session is on,
it instantiates the mekari-session SDK with current_user = $auth.user.sso_id,
subscribes to the four SDK events, and maps each event onto Hub's existing
auth primitives (sign-out, auth-code re-login, company sync). The SDK itself
injects the iframe to account.mekari.com/sm/current and relays state via
postMessage; Hub only consumes the resulting events. Hub uses the OAuth2
Authorization Code variant (ADR-3) because that is already how Hub authenticates.
2.3 Technical Decisions (ADR format)
ADR-1 — Where to load the SDK
- Context: SDK must run on "all pages" of the authenticated app; Hub already has a single boot component per authed shell.
- Options: (a)
InitComponent.vueboot component underlayouts/hub.vue; (b) a Nuxt global middleware; (c) per-page. - Decision: (a). The SDK lifecycle lives in a mixin used by
InitComponent.vue. - Rationale:
InitComponentalready centralises boot side-effects (MQTT/FCM/ identity) and already reads$auth.user.sso_id(:1138). One mount point = one iframe, matching SDK "caching disabled, 5s" expectation without per-route churn. - Consequences: SDK runs only inside authed shell (correct — unauthed pages
have no
sso_id). Login/SSO-callback pages keep current behaviour. - Reversibility: High — remove the mixin import + toggle.
ADR-2 — How to load the external script
- Context: SDK ships as CJS+ESM and is also served from
https://account.mekari.com/sm/sdk.js. - Options: (a) npm
@mekari/sdkimport; (b) runtime<script src>injection likeplugins/hotjar.js; (c) both (npm with CDN fallback). - Decision: (a) npm
@mekari/sdkimport behind a dynamicimport()inside the toggle guard, mirroring lazy-load discipline. - Rationale: Versioned, tree-shakeable, testable (mockable in Jest). CDN URL
kept as
[REQUIRED]env for CSP allow-listing, not as the load path. - Consequences: Adds
@mekari/sdktopackage.json; needs A&L to publish the package + grant registry access (OQ-3). - Reversibility: High.
ADR-3 — Which session-sync flow Hub follows
- Context: Source RFC defines a Web-Session (client_credentials, BE→SSO) flow and an OAuth2 Authorization-Code flow.
- Options: (a) Web-session/client_credentials; (b) OAuth2 auth-code.
- Decision: (b) OAuth2 Authorization Code.
- Rationale: Hub already authenticates via auth-code (
middleware/sso-callback.js:14consumesroute.query.code;middleware/login.js:22requestsresponse_type=code). Reusing it meansswitch_userre-auth andcurrent_companyfetch follow theusers/me/current_company(auth-code) contract, notusers/{id}/current_company. - Consequences:
logged_out/server_downmust revoke tokens then sign out (auth-code semantics), which$auth.logoutalready does. - Reversibility: Medium — switching to client_credentials would require a Hub BE proxy endpoint instead.
ADR-4 — Feature-flag source
- Context: Two flag mechanisms exist: org payload
feature_flag(store/organization.js:30) and server-settingspreferences/getFeatureFlag(store/preferences.js:91). - Options: (a) org payload map; (b) preferences server-setting.
- Decision: (a)
organization.feature_flag.centralized_session. - Rationale: AGENTS.md mandates org-payload flags as the standard; read synchronously via getter; supports per-company rollout (matches source RFC "enable per company/application").
- Consequences: Backend must add
centralized_sessionto the org payload (OQ-2). Until then the toggle readsundefined→ treated as OFF (safe default). - Reversibility: High.
ADR-5 — Reuse existing auth primitives for event handling
- Context: Each SDK event maps onto an action Hub already performs.
- Options: (a) reuse
doSignOut()/$auth.logout/middleware/loginredirect; (b) build new session-control code. - Decision: (a) reuse.
- Rationale:
doSignOut()already tears down MQTT/FCM/moengage and redirects to SSO sign_out; duplicating it risks leaks.EventBus 'user-sign-out'is the existing decoupled trigger (SwitchAccount.vue:299). - Consequences: Mixin emits
EventBus.$emit('user-sign-out')for logout paths rather than calling sign-out directly, matching the cross-tab relay pattern. - Reversibility: High.
ADR-6 — server_down behaviour
- Context: SDK emits
server_downafter backoff exhaustion; source RFC defines anmsli(Mekari Session Logged-In) local-storage fallback. - Options: (a) immediate sign-out; (b)
msliheuristic then sign-out; (c) ignore. - Decision: (b). If
msliexists andnow - msli < 2hAND the local Hub token is still valid → treat aslogged_in(stay); else sign out. - Rationale: Matches source RFC "Retry-backoff fallback" and the stated goal "keep consistent session behavior across Mekari" without hard-logging-out users on a transient Session-Manager outage.
- Consequences: Hub must write
mslion every confirmedlogged_in. - Reversibility: High.
Minimum coverage checklist: Storage → n/a — FE-only, no DB (§2.8); local
storage msli + cookies (read) covered. Sync vs async → SDK events are async,
handled via listeners (ADR-5). Caching → SDK iframe caching is A&L-owned
("max 5s"); Hub caches nothing. Third-party → npm import + CSP allow-list
(ADR-2, §3). Consistency → eventual (poll-on-load via SDK). Multi-tenancy →
sso_id comparison is the isolation key (event switch_user). Reuse vs new →
ADR-5 (reuse auth primitives); only one new endpoint key (OQ-1).
2.4 APIs
Outbound (Hub → others)
| Method | URL (key) | Status | Owner | Note |
|---|---|---|---|---|
| iframe GET | account.mekari.com/sm/current | new (A&L) | Account & Launchpad | invoked by SDK, not Hub code directly |
| GET | @mekari/sdk Session.refresh() (throttled) | new (A&L) | A&L | extends SSO session on user activity |
| GET | Hub BE current-company sync → endpoint.*.user.currentCompany | new-with-justification | Hub BE (OQ-1) | no existing key (repo grep: 0 hits); needed to set company on logged_in/after switch_user |
| GET (existing) | SSO /auth/?response_type=code...&redirect_uri=.../sso-callback | reused | SSO | re-auth on switch_user (middleware/login.js:22) |
| GET (existing) | ${SSO_ACCOUNT_URL}/sign_out | reused | SSO | logout (SwitchAccount.vue:409) |
new-with-justification (current-company): reuse is impossible because Hub has
no current-company endpoint today (verified by repo grep). The exact BE contract
(/me/current_company per ADR-3, auth-code variant) must be confirmed by Hub BE
— OQ-1.
Inbound (others → Hub)
| Channel | From | Payload | Note |
|---|---|---|---|
postMessage (window) | Session Manager iframe (/sm/current) | session state incl. user_sso_id | consumed inside the SDK, surfaced to Hub as the 4 events below; Hub does not parse postMessage directly |
| SDK event | @mekari/sdk Session | { status, current_user, ... } | see §2.6 |
SDK contract consumed by Hub
// assets/mixins/session/centralizedSession.js (new — shape per source RFC)
import { Session } from '@mekari/sdk'
const session = new Session({ current_user: this.$auth.user.sso_id })
session.on('event', (data, error) => {
// data.status ∈ { logged_in, logged_out, switch_user, server_down }
// NOTE: source RFC sample uses data.status === 'logout' while the events
// section lists 'logged_out' — reconcile with A&L (OQ-4) before coding.
})
session.refresh() // throttled per X (TBD by A&L)
2.5 Local Storage & Cookies (Hub-side)
| Key | Type | Owner | Hub use |
|---|---|---|---|
msli | localStorage (timestamp) | Hub writes | written = Date.now() on confirmed logged_in; read in server_down fallback (ADR-6) |
_mekari_account | cookie | SSO/Rails | Hub never reads directly; used by SDK iframe |
qontak._token.hub | cookie | Hub (hubAuthScheme.js:152) | existing token validity check in server_down fallback |
2.6 Sequence Diagrams
Happy path — page load, logged_in + company sync
sequenceDiagram
autonumber
participant U as User
participant Hub as Hub FE (InitComponent + mixin)
participant SDK as @mekari/sdk
participant IF as SM iframe (/sm/current)
participant SM as Session Manager (A&L)
participant R as Redis (session)
participant BE as Hub BE
participant SSO as SSO API
U->>Hub: open authed page
Hub->>Hub: toggle centralized_session ON?
Hub->>SDK: new Session({current_user: sso_id})
SDK->>IF: inject iframe (_mekari_account cookie)
IF->>SM: GET /sm/current
SM->>R: validate + update last_request_at (<2h)
R-->>SM: session ok (user_sso_id)
SM-->>IF: render page w/ user_sso_id
IF-->>SDK: postMessage(user_sso_id)
SDK-->>Hub: event {status: logged_in}
Hub->>Hub: localStorage msli = now
Hub->>BE: GET current_company (OQ-1)
BE->>SSO: GET /v1.1/users/me/current_company
SSO-->>BE: company
BE-->>Hub: company → set → allow interaction
Failure path — server_down (Session Manager unreachable)
sequenceDiagram
autonumber
participant Hub as Hub FE (mixin)
participant SDK as @mekari/sdk
participant SM as Session Manager
SDK->>SM: GET /sm/current (timeout / 5xx)
Note over SDK,SM: retry with backoff (A&L-owned); ~exhaust ≤ a few s
SDK-->>Hub: event {status: server_down}
alt msli exists AND now - msli < 2h AND qontak._token.hub valid
Hub->>Hub: treat as logged_in (stay) — no sign-out
else stale or no token
Hub->>Hub: EventBus.$emit('user-sign-out') → doSignOut()
end
switch_user — re-auth via authorization code
sequenceDiagram
autonumber
participant Hub as Hub FE (mixin)
participant SDK as @mekari/sdk
participant Auth as $auth (hubAuthScheme)
participant SSO as SSO
SDK-->>Hub: event {status: switch_user, current_user: other_sso_id}
Hub->>Hub: remove msli
Hub->>Auth: $auth.logout({token}) (revoke + clear cookies)
Hub->>SSO: redirect /auth?response_type=code&redirect_uri=.../sso-callback
SSO-->>Hub: /sso-callback?code=... (autologin via valid _mekari_account)
Hub->>Hub: doSSOValidation() → new session → company sync
Hub->>Hub: toast "Your account has changed"
logged_out
sequenceDiagram
autonumber
participant Hub as Hub FE (mixin)
participant SDK as @mekari/sdk
SDK-->>Hub: event {status: logged_out}
Hub->>Hub: remove msli
Hub->>Hub: EventBus.$emit('user-sign-out') → doSignOut() → SSO /sign_out
2.7 State Surface Contract & State Machine
stateDiagram-v2
[*] --> Unknown
Unknown --> LoggedIn: event logged_in (sso_id match)
Unknown --> LoggedOut: event logged_out
LoggedIn --> SwitchUser: event switch_user (sso_id mismatch)
LoggedIn --> LoggedOut: event logged_out
LoggedIn --> Degraded: event server_down
Degraded --> LoggedIn: msli<2h & token valid
Degraded --> LoggedOut: msli stale / invalid token
SwitchUser --> LoggedIn: re-auth complete (new sso_id)
LoggedOut --> [*]: doSignOut → SSO /sign_out
| State | Visibility | Transitions | Note |
|---|---|---|---|
| LoggedIn | normal app | →SwitchUser/LoggedOut/Degraded | writes msli |
| SwitchUser | brief redirect | →LoggedIn | revoke + auth-code |
| Degraded | app stays (fallback) | →LoggedIn/LoggedOut | server_down |
| LoggedOut | redirect to SSO | terminal | doSignOut() |
2.8 Database Model
n/a — FE-only, no schema change (matches source RFC "Database Model: No
database changes"). No erDiagram.
2.9 Branch & Skip Catalog
| Branch | Owner | Behaviour |
|---|---|---|
| toggle OFF | Hub FE | SDK never constructed; today's behaviour verbatim |
sso_id absent ($auth not ready) | Hub FE | skip SDK init until $auth.user.sso_id present |
| server_down + msli fresh | Hub FE | stay logged in (no sign-out) |
| multiple-sessions-per-account | A&L (server) | n/a — no Hub FE branch |
2.10 Responsibility Boundary (cross-squad)
| Step | Owner |
|---|---|
| SDK package, iframe, /sm/current, Redis, Kong path | Account & Launchpad |
current_company SSO contract + Hub BE proxy | Hub BE (OQ-1) |
org payload centralized_session flag | Hub BE (OQ-2) |
| SDK wiring, event handling, toggle, msli, observability | Hub FE (this RFC) |
3. High-Availability & Security
3.1 Performance (Hub-side)
Hub adds one SDK init + one iframe load per authed shell mount. The 6k RPS /
50ms latency targets in the source RFC apply to A&L's /sm/current, not Hub.
Hub requirement: SDK init must not block first paint — load lazily after
$auth.loggedIn (ADR-1) and never gate route rendering on it (toggle-guarded).
3.2 Security Implications (Hub-side)
| Check | Rule / action |
|---|---|
| iframe origin (A05) | SDK iframe must target account.mekari.com only; add CSP frame-src/child-src allow-list entry for account.mekari.com |
| script source (A05) | CSP script-src allow-list account.mekari.com for /sm/sdk.js if CDN-loaded; npm import avoids this (ADR-2) |
| postMessage (A03/A07) | SDK validates the iframe origin internally; Hub must NOT add its own wildcard postMessage listener for session data (Hub already uses postMessage('*') at sso-callback.js:23 for a different purpose — keep separate) |
| token handling (A02) | no token logged; $auth.logout clears cookies (hubAuthScheme.js:43-46) |
| referrer/origin | source RFC option 2 (referrer/origin whitelist) is A&L-side; Hub's prod domain must be added to A&L's whitelist before rollout (OQ-5) |
CSP/whitelist update mechanism is A&L-owned (source RFC §3); Hub's contribution is supplying its production domain(s) for the allow-list.
3.3 Observability (Hub-side)
- Datadog RUM already initialised (
plugins/datadog-rum.ts,nuxt.config.js:92). Emit a RUM custom action per SDK event (centralized_session.<event>). - Mixpanel v2 (
assets/mixins/metric/mixpanelMixin.js, AGENTS.md) — logswitch_userand forced sign-outs for funnel analysis. - Alert: spike in
centralized_session.server_down(proxy for A&L outage).
4. Backwards Compatibility & Rollout Plan
4.A Compatibility
Toggle OFF ⇒ no SDK, no iframe, no behavioural change — fully backwards
compatible. Hub is rollout step 5 ("The Next Chapter") in the source RFC,
after the Launchpad pilot (step 4) proves the SDK. Hub's prod domain must be added
to the SDK CSP frame-ancestors / referrer-origin whitelist before enabling.
4.B Pre-merge Verification (from package.json)
npm run lint # package.json:19
npm run test # package.json:21 (jest --coverage)
npm run build # package.json:9 (nuxt build) — must exit 0
4.C Agent Execution Plan (ordered chunks)
Chunk 1 — Toggle plumbing
- Files:
store/organization.js(confirmfeature_flaggetter exposescentralized_session; no code change if generic), new computed in mixin. - Acceptance: unit test — getter returns
falsewhen flag absent.
Chunk 2 — SDK boot plugin + mixin (TDD first)
- Files (new):
plugins/mekari-session.js,assets/mixins/session/centralizedSession.js,assets/mixins/session/__tests__/centralizedSession.spec.js. - Files (modify):
package.json(add@mekari/sdk),nuxt.config.js:84(register plugin),nuxt.config.js:250(addMEKARI_SESSION_SDK_URLenv for CSP). - Commands:
npm run test -- --testPathPattern="centralizedSession". - Acceptance: toggle ON +
sso_idpresent ⇒Sessionconstructed withcurrent_user = sso_id; toggle OFF ⇒Sessionnever constructed (mock asserts 0 calls).
Chunk 3 — Wire into boot + logged_out/logged_in
- Files (modify):
components/layouts/main/InitComponent.vue(use mixin after$auth.loggedIn). - Acceptance:
logged_out⇒EventBus.$emit('user-sign-out')called once;logged_in⇒localStorage.msliset to a numeric timestamp.
Chunk 4 — switch_user re-auth
- Files: mixin handler; reuse
middleware/login.jsredirect URL builder. - Acceptance:
switch_user⇒$auth.logoutinvoked, thenwindow.locationredirect to SSO/auth?response_type=code...redirect_uri=.../sso-callback;msliremoved; toast emitted.
Chunk 5 — server_down fallback + msli
- Files: mixin handler + small
utilshelper (cf.utils/general.js:104). - Acceptance:
now-msli<2h& valid token ⇒ no sign-out; elseuser-sign-outemitted. Test both branches.
Chunk 6 — company sync (BLOCKED on OQ-1)
- Files:
common/constants/endpoint.js(register key),store/organization.jsorstore/usersaction for current_company. - Acceptance:
logged_in⇒ sync action dispatched; sets company. Do not start until OQ-1 resolved.
Chunk 7 — observability
- Files: mixin (RUM/Mixpanel calls).
- Acceptance: each event emits a RUM action; verifiable via mocked tracker.
Order rationale: toggle (1) → SDK/mixin (2) → boot+core events (3) → switch (4) → fallback (5) → company sync (6, gated) → telemetry (7).
4.D Verification & Rollback Recipe
Post-deploy signals
- Datadog RUM action
centralized_session.logged_inappears for piloted company. centralized_session.server_downrate ≈ 0 under normal A&L health.- No regression in Hub login funnel (Mixpanel v2).
Rollback (numbered, agent-executable)
- Set org
feature_flag.centralized_session = falsefor affected company (instant, no deploy) → SDK stops constructing. - If code-level: revert the InitComponent wiring PR (Chunk 3) — toggle already makes this safe; revert removes the mixin import entirely.
- Confirm
centralized_session.*RUM actions drop to zero. - Ask A&L to remove Hub's domain from the SDK whitelist if fully reverting.
Rollback respects order: disable flag (1) before reverting code (2); no other Hub layer depends on this mixin.
5. Concerns, Questions, or Known Limitations
| # | Severity | Question / limitation | Owner |
|---|---|---|---|
| OQ-1 | [critical] | Hub BE has no current-company endpoint (repo grep: 0 hits). Need confirmed BE contract for users/me/current_company proxy (auth-code variant per ADR-3). Blocks Chunk 6. | Hub BE |
| OQ-2 | [critical] | centralized_session flag is not yet in Hub's org payload (store/organization.js). Without it the toggle is permanently OFF. | Hub BE |
| OQ-3 | [critical] | @mekari/sdk is not in package.json and not published/accessible to Hub's registry yet. Cannot import until A&L publishes + grants access. | A&L |
| OQ-4 | [important] | Source RFC contradicts itself: sample uses data.status === 'logout'; events list says logged_out. Need canonical event names + payload shape before coding handlers. | A&L |
| OQ-5 | [important] | Hub production domain(s) must be added to SDK CSP frame-ancestors / referrer-origin whitelist before enable. | A&L + Hub |
| OQ-6 | [important] | CSP frame-src/script-src for account.mekari.com — confirm Hub's current CSP source (header vs meta) and where to edit. | Hub FE / Infra |
| OQ-7 | [nice-to-have] | Session.refresh() throttle interval is TBD in source RFC; Hub needs the value to wire activity-based refresh. | A&L |
| OQ-8 | [nice-to-have] | Infosec approver name + Slack discussion thread unfilled in metadata. | Hub |
Known limitation: until OQ-1/OQ-2/OQ-3 close, only Chunks 1–5 (session detect + sign-out/switch/fallback, toggle-gated, no company sync) are agent-executable.
6. Comment logs
| Date | Author | Note |
|---|---|---|
| 2026-06-27 | Syafrizal Muhammad | Initial Hub-FE integration draft derived from A&L source RFC |
7. Ready for agent execution
Ready for agent execution: no
Blocking gates (must resolve before flipping to yes):
- OQ-1
[critical]— Hub BE current-company endpoint contract unverified (blocks company sync / Chunk 6). - OQ-2
[critical]—centralized_sessionorg-payload flag does not exist yet (toggle inert). - OQ-3
[critical]—@mekari/sdknot published / not inpackage.json; cannot import.
Non-blocking but needed for full completion: OQ-4 (event-name reconciliation), OQ-5/OQ-6 (CSP/whitelist), OQ-7 (refresh throttle).
Chunks 1–5 are executable today against the live repo behind the (currently OFF) toggle; Chunk 6 is gated on OQ-1/OQ-2.