Skip to main content

RFC: Centralized Web Session — CRM Frontend Integration of mekari-account-web-sdk Session

Document Conventions (do not remove)

Follows the Qontak RFC Template governance shell (metadata table + sections 1–6 + Comment logs) and is agent-execution-ready (§2 Repo Reading Guide, mermaid diagrams, §4 Execution Plan + Verification & Rollback Recipe). YAML frontmatter is the machine-readable index; the metadata table is the human record. Both agree on every shared field.

Scope guard. This RFC covers the CRM frontend repository only (/Users/mekari/Documents/crm). The Mekari Session service (Golang), the dedicated Redis, the Kong/MAG gateway path sm.mekari.com/*, and the SSO session store changes are owned by Account & Launchpad and live in other repos — they are READ-ONLY context here, captured to anchor the CRM integration. No backend code is specified in this RFC.


Metadata

FieldValueNotes
StatusRFCOpen for review; not yet Ready for agent execution (see §7)
Type / Sub-typefrontend / enhancementCRM consumes a new shared SDK; no new product surface beyond a notification
TitleCentralized Web Session — CRM FE Integration of mekari-account-web-sdk Session
OwnerAccount & Launchpad (SDK) · CRM Frontend (this integration)Cross-squad
AuthorsQontak CRM Frontend
ReviewersA&L (SSO/SM), CRM FE tech leadCross-squad review required
Approver(s)CRM FE tech leader, Infosec [REQUIRED]Infosec gates CSP / SDK origin policy
Submitted2026-06-27ISO-8601
Last updated2026-07-02ISO-8601
Target release2026-Q3Pilot is Launchpad first (PRD §4.4); CRM follows in step 5
Related documentsCentralized Web Session RFC (Confluence)Driver
Discussion[REQUIRED]Slack thread

Sections at a Glance

§SectionCRM-FE hint
1OverviewProblem, success criteria, scope, Detail 1.A coverage matrices, 1.B decision index, 1.C per-story change map
2Technical DesignDetail 2.0 Repo Reading Guide (Repo Map, anchors, source verification, patterns, reading order), infra topology, ADR blocks, no-DDL, API matrices, sequence + state + branch diagrams
3High-Availability & SecuritySDK origin/CSP policy, FE observability, fail-open on server_down
4Backwards Compatibility & RolloutFeature toggle centralized_session, Agent Execution Plan, Verification & Rollback Recipe
5Concerns, Questions, Known LimitationsSeverity-tagged open questions
6Comment logsReview trail
7Ready for agent executionMarker + failing gates

1. Overview

1.1 Context & Problem

Mekari products each maintain their own session and do not react when the user logs out, switches account, or goes idle on SSO. CRM is one such product: its session lives in qcrm_access_token / qcrm_refresh_token cookies and an @nuxtjs/auth Vuex strategy (schemes/crmAuthScheme.js), refreshed on a local timer (utils/helpers/auth.js:setupAutoTokenRefresh). There is no signal from SSO into CRM, so the PRD's stale-session bugs apply to CRM:

  • A user who logs out on SSO stays logged in on CRM until the CRM token expires.
  • A user who switches account on SSO still sees the previous company in CRM.

The shared mekari-account-web-sdk Session SDK closes this gap: every CRM page loads the SDK with the current user's SSO id (constructor option currentUser); the SDK opens an iframe to https://sm.mekari.com/current, the Session Manager validates/extends the SSO session in its dedicated Redis, and the SDK dispatches a single "event" callback carrying { status, sessionId? }, where status is exactly one of logged_in, logged_out, or server_downthere is no switch_user. Internally the SDK compares the iframe's ssoId to the configured currentUser; a different SSO account (i.e. a switch) surfaces indistinguishably from a plain logout as logged_out, and the other account's id is never exposed to CRM. CRM must react to each status consistently with its existing auth/session/routing/store patterns.

1.2 Success Criteria (CRM-FE slice)

  • CRM loads mekari-account-web-sdk Session on all authenticated pages with the current user's SSO id (currentUser option), gated behind a centralized_session feature toggle.
  • CRM handles all three SDK statuses (logged_in, logged_out, server_down) consistently with its current sign-out, routing, and store behavior, via a single session.on("event", (data) => …) subscription that switches on data.status.
  • CRM re-validates the SSO session periodically by passing interval (ms, ≥1000; recommended 5 * 60 * 1000 per PRD constraint 6.9) to the Session constructor — the SDK reloads its hidden iframe on this fixed timer. There is no session.refresh() method and no activity-driven refresh.
  • msli (localStorage key "msli", 2-hour expiry) is owned internally by the SDK; CRM must not read or write it. On server_down (the SDK's own checkTimeout + msli fallback exhausted), CRM follows PRD constraint 6.10 = fail-open: it takes no destructive action and does not force logout.

1.3 Non-goals / Out of scope

  • Any Session Manager / SSO / Redis / Kong-MAG backend work (owned by A&L).
  • Auto revoke of access & refresh tokens on inactivity-driven logout (PRD out-of-scope).
  • CRM backend (api.mekari.com / mobile API) changes for current_company sync — dependency, not deliverable here (see §5 Q4). The SDK itself exposes no company data, so this was never an SDK gap.
  • No new product UI surfaces. logged_out and server_down both reuse CRM's existing sign-out flow; there is no "user has changed" toast (that surface was tied to a switch_user event that does not exist in the real SDK — see ADR-5, §5 Q1/Q5).

1.4 Design References

n/a — no Figma. This is a pure integration RFC with no new user-visible surface. An earlier draft proposed a "user has changed" toast on switch_user; the real SDK has no such event, so an SSO account switch reaches CRM as a plain logged_out and runs the normal sign-out path (ADR-5). §5 Q5 (toast component confirmation) is resolved as moot.

DocKindLinkUsed for
Centralized Web Session RFCauthoritative (driver)ConfluenceSDK contract, event names, flows, rollout

Detail 1.A — Coverage Matrices

PRD Section Coverage

PRD sectionCovered in RFCNotes
1. Overview / known issues§1.1CRM-relevant stale-session cases mapped
Success Criteria§1.2FE-slice subset; SDK/Redis criteria are A&L
Out of Scope§1.3Inherited
Dependencies (SDK, SM service, Redis)§2.0 / §5SDK consumed; SM+Redis are A&L deps
2. Technical Design — Current/Proposal§2.1–§2.2CRM current auth vs SDK proposal
How to use the SDK§2.4 (Inbound) / §2.2Mapped to CRM plugin
Local Storage (msli)§2.3SDK-internal; CRM must not touch it
Session Manager SDK contract / events§2.4 (Inbound)Three statuses handled via one session.on("event", …) subscription (no switch_user — real SDK ground truth)
FE Product Integration Flows — SDK Flow§2.5 (seq)Implemented as plugin
FE Flows — Web Session Flow§2.5 / §5 Q1Resolved — CRM's token-based auth needs no SSO-autologin mapping; the SDK delivers a switch as plain logged_out, reusing the existing sign-out path (ADR-4/ADR-5)
FE Flows — OAuth2 Authorization Code Flow§5 Q1Full — CRM does not use authz-code today; not applicable now that account-switch handling is resolved as plain logged_out (Q1 moot)
User Logout From Product§2.5 / §2.2Reuse userLogout + redirect to sign_out
User Switch Account§2.6 (state) / §5 Q1Resolved — SDK delivers a switch as plain logged_out (no switch_user in the real SDK); reuses ADR-4. Company re-sync remains a separate open dependency (Q4)
Database Model (no changes)§2.3Confirmed no DDL
3. HA & Security§3FE: CSP/origin + observability
4. Rollout Plan§4CRM = step 5 (post-Launchpad pilot)
5. Open Questions§5CSP vs referrer/origin + CRM-specific
FE Implementation Scope (per product repo)§1.C / §4This RFC's core

UI / Consumer Surface Coverage

SurfaceTypeRead endpointNotes
Sign-out redirect (logged_out / server_down)navigationn/aReuses userLogout + account.mekari.com/sign_out (consumer-driven; URL is unverified external — the SDK has no logout() method)
Every authenticated CRM page (SDK host)runtimen/aSDK mounted globally via plugin

There is no other consumer UI surface. The earlier switch_user "user has changed" toast row is removed — that event does not exist in the real SDK (see ADR-5).

Role Coverage

PRD roleCRM handlingNotes
Authenticated CRM user (any role)SDK loads with their SSO idSession events role-agnostic
banned / suspended / freezed / expiredExisting crm-user middleware redirects unchangedSDK runs after auth guard; no role-specific session branch in PRD

Per-status authorization is unchanged by this RFC — see §2 Role × Endpoint.


Detail 1.B — Decisions Closed (index → §2 ADR blocks)

#DecisionChosenPre-baked?ADR
D1SDK delivery into CRMgit-installed mekari-account-web-sdk (bundled dependency) + new Nuxt pluginyes — the SDK ships only as a bundled git dependency, no alternative to evaluate§2 ADR-1
D2Feature gatingReuse custom_features w/ code centralized_sessionyes (PRD names toggle)§2 ADR-2
D3SDK mount point + event wiringNew plugins/mekari-session.js, wired to $auth + store/user, single session.on("event", …) subscriptionno§2 ADR-3
D4logged_out / server_down actionReuse existing userLogout → redirect to account.mekari.com/sign_out (consumer-driven; SDK has no logout())no§2 ADR-4
D5switch_user action — RESOLVED, folded into D4The real SDK has no switch_user status; an SSO account switch is delivered as logged_out and runs the same sign-out path as D4. No separate decision is needed.resolved by real SDK ground truth (mekari-account-web-sdk v0.3.0, session.ts)§2 ADR-5 (resolution note)
D6msli fallback storage — RESOLVED, not a CRM decisionmsli (2h) is owned internally by the SDK; CRM must not read/write it. On server_down, CRM fails open (no destructive action) per PRD constraint 6.10.resolved by real SDK ground truth (mekari-account-web-sdk v0.3.0, session.ts)§2 ADR-6 (resolution note)
D7current_company syncDeferred — BE dependency, CRM uses teams not SSO current_company; the SDK exposes no company data at allno — see §5 Q4§2 ADR-7

Detail 1.C — Per-Story Change Map

Story (from PRD FE Implementation Scope)Layer scopeChangesAcceptance criteriaRFC anchors
S1 Add mekari-account-web-sdk Session, load with the current user's SSO idFE-onlypackage.json dep (git install); plugins/mekari-session.js (new); registered in nuxt.config.js:pluginsUnit test: plugin instantiates Session with currentUser from $auth.user; yarn test green§2.2, §4.C chunk 1–2
S2 Gate behind centralized_session toggleFE-onlyRead store.state.user.custom_features for code centralized_session; plugin no-ops when offTest: SDK not constructed when feature absent/false§2 ADR-2, §4.C chunk 2
S3 Handle 3 SDK statusesFE-only (Runtime/behavior)Single session.on("event", (data) => …) handler in plugin → dispatch user/userLogout, redirectTest per status: logged_in→no-op; logged_out→sign-out (incl. account switch); server_down→fail-open (no destructive action)§2.4 Inbound, §2.5, §2.6, §4.C chunk 3
S4 current-company syncFE + BEBlocked — no CRM current_company endpoint found; teams ≠ SSO company; the SDK has no company surface eithern/a — covered in BE RFC [REQUIRED link] / deferred§2 ADR-7, §5 Q4
S5 Wire product logout to account.mekari.com/sign_outFE-onlyExtend store/user.js:userLogout success path to redirect to SSO sign_outTest: after sign_out POST resolves, window.locationaccount.mekari.com/sign_out§2.5, §4.C chunk 4
S7 ObservabilityFE-only (Config)Datadog RUM custom action/error on event + server_down (reuse @datadog/browser-rum)Datadog action mekari_session.event visible; yarn lint:js green§3.2, §4.C chunk 5

S6 (msli fallback helper) is removed — the SDK owns msli internally (ADR-6 resolution note); there is no CRM-owned fallback module to build. Every artifact above re-appears in §2 / §4. Detail 1.C is the index, not the source of truth.


2. Technical Design

2.0 Repo Reading Guide (read before writing any code)

Repo Map (slice this RFC touches)

flowchart LR
subgraph CRM[FE: local/crm — Nuxt 2 SPA]
NCONF["nuxt.config.js<br/>plugins[] (modify)"]
PLUG["plugins/mekari-session.js<br/>(new)"]
AUTH["schemes/crmAuthScheme.js<br/>(read — auth model)"]
AHELP["utils/helpers/auth.js<br/>(read — refresh timer)"]
USTORE["store/user.js<br/>userLogout (modify)"]
UFEAT["store/user.js<br/>custom_features (read)"]
DDR["plugins/datadog-rum.js<br/>(read — RUM pattern)"]
EP["assets/variables/endpoints.js<br/>(read/extend)"]
end
subgraph EXT[READ-ONLY context — owned by A&L]
SDK["mekari-account-web-sdk Session"]
SM["sm.mekari.com/current<br/>(Session Manager + iframe)"]
end
NCONF --> PLUG
PLUG --> SDK
SDK -. iframe .-> SM
PLUG --> USTORE
PLUG --> UFEAT
PLUG --> DDR
USTORE --> EP
AUTH -. informs .-> PLUG
AHELP -. informs .-> PLUG

The earlier draft's utils/helpers/mekari-session.js msli-fallback helper node is removed — the SDK owns msli internally (R7 / ADR-6 resolution note); there is nothing for CRM to build there.

Existing Code Anchors

#PathWhat to learn
1schemes/crmAuthScheme.jsCRM auth model: LocalScheme subclass; tokens in qcrm_access_token/qcrm_refresh_token cookies; logout()requestWith(logout) + $auth.reset(). CRM is token-based, not authz-code.
2utils/helpers/auth.jsrefreshToken(), setupAutoTokenRefresh() (10-min pre-expiry timer), clearAutoTokenRefresh(). The SDK must not fight this timer.
3store/user.js:120 (userLogout)Logout action: POST ${USER_URL}/sign_out, then deleteUserLocalData + deleteSsoCookies + $auth.reset(). Reuse for logged_out/server_down.
4store/user.js:144 (deleteSsoCookies)Which cookies CRM clears on logout (crm_sso_*, chat_sso_*, global_sso_*).
5store/user.js:67 (getCustomFeature)Feature toggle source: GET /users/me/feature_enabledcustom_features: [{code, enabled}]. Gate centralized_session here.
6middleware/crm-user.jsAuth guard order: runs getUserData then status redirects. SDK mounts after this.
7plugins/datadog-rum.jsRUM init pattern + config gate ($config.ddEnabled === 'true', plugins/datadog-rum.js:4). Reuse for session observability.
8nuxt.config.js:58 (plugins)Plugin registration order — ~/plugins/auth runs first; add ~/plugins/mekari-session after auth + auto-token-refresh.
9assets/variables/endpoints.jsEndpoint constant pattern (USER_URL, CRM_V28). Add SSO/SM constants here if needed.
10adapters/http/utils.js (processAuthHeaders)How crm_sso_token becomes X-Auth-Token under ENABLE_SEAMLESS_AUTH. Context for SSO-token interplay.

Source Verification

Anchor / Pattern / ContractEvidence (verified)
CRM auth = LocalScheme token modelschemes/crmAuthScheme.js:12 export default class CrmAuthScheme extends LocalScheme; login() POST + Cookies.set('qcrm_refresh_token', …) :33
logout flowschemes/crmAuthScheme.js:234 async logout()this.$auth.reset() :242; store/user.js:120 userLogout({ dispatch }), POST ${USER_URL}/sign_out :131
sign-out cookie cleanupstore/user.js:144 deleteSsoCookies() removes crm_sso_token/chat_sso_token/global_sso_token :145–155
feature toggle mechanismstore/user.js:67 getCustomFeature(); state custom_features store/user.js:12–13; mutation SET_CUSTOM_FEATURE :349
feature-check shapemiddleware/redirect-to-v3.js:48 reads store.state.user?.custom_features; :49 matches via features.some((f) => f.code === … && f.enabled)
refresh timerutils/helpers/auth.js:157 setupAutoTokenRefresh; :13 REFRESH_BEFORE_EXPIRY_MINUTES = 10; :248 clearAutoTokenRefresh
plugin pattern + ordernuxt.config.js:58–72 plugins[], '~/plugins/auth' first :59
RUM patternplugins/datadog-rum.js:1 import { datadogRum }; :4 if ($config.ddEnabled === 'true')
endpoint constantsassets/variables/endpoints.js:1 USER_URL, :5 CRM_V28
no mekari-account-web-sdk presentsearched package.json deps — only @mekari/pixel found; mekari-account-web-sdk absent (verified by explorer sweep). (An earlier draft named the package @mekari/sdk, which also does not exist — corrected here.)
cookie domainschemes/crmAuthScheme.js:8 COOKIE_DOMAIN_ENV = process.env.COOKIE_DOMAIN || '.qontak.com'
CRM currentUser (SSO id) fieldUNVERIFIED/users/me response not confirmed to expose an SSO id; external_company_id exists (plugins/mixpanel.js:30) but is company, not user SSO id → §5 Q2. Do not conflate this with the unrelated Vuex state store/user.js current_user: [] (~:15) — that is a different, pre-existing CRM concept, not the SDK's currentUser constructor option.
CRM current_company endpointUNVERIFIED / absent — only team endpoints found (store/user.js:244,261,274); no current_company → §5 Q4. The SDK itself has no company API either (constructor/destroy()/on()/off() only).
centralized_session feature codeUNVERIFIED — code format is CP-QONTAKCRM-YYYY-NNNN (utils/helpers/package-features.js), but feature_enabled also uses string codes (e.g. use_central_contact_data). Exact code TBD → §5 Q3

Patterns to Follow

ConcernReference fileNote
Third-party SDK init via pluginplugins/datadog-rum.jsConditional config gate; default-export init fn receiving Nuxt ctx
Plugin accessing $auth/storeplugins/auth.jswindow.$auth = app.$auth pattern; ctx access
Feature-flag readmiddleware/redirect-to-v3.js:48–49store.state.user?.custom_features (:48), matched via features.some((f) => f.code === X && f.enabled) (:49)
Sign-out orchestrationstore/user.js:120 userLogoutReuse, don't reinvent
External redirectmiddleware/redirect-to-v3.js:65window.location.href for cross-origin

The earlier "user notification toast" and "_mekari_account cookie access" pattern rows are removed: the toast surface was tied to the fictional switch_user event (ADR-5), and CRM never reads _mekari_account under the corrected design (R7) — that cookie is exchanged only between the SDK's iframe and the Session Manager.

Reading Order for the Agent

  1. schemes/crmAuthScheme.js — understand the auth/session lifecycle.
  2. utils/helpers/auth.js — refresh timer; do not conflict.
  3. store/user.js (userLogout, deleteSsoCookies, getCustomFeature).
  4. middleware/crm-user.js — guard order.
  5. nuxt.config.js (plugins) — registration point + order.
  6. plugins/datadog-rum.js — SDK init + RUM pattern.
  7. assets/variables/endpoints.js — endpoint constant style.
  8. adapters/http/utils.js — SSO-token header interplay.
  9. PRD (Confluence) — SDK event contract + flows.
  10. utils/helpers/package-features.js — feature code conventions.

Existing API check

CallTagJustification
GET https://sm.mekari.com/current (via SDK iframe)new (external, A&L-owned)Not a CRM endpoint; CRM only loads the SDK which opens the iframe
GET /users/me/feature_enabledreusedstore/user.js:67 — read centralized_session toggle
POST /api/mobile/v2.7/users/sign_outreusedstore/user.js:131 — existing CRM sign-out
account.mekari.com/sign_out redirectnew (external, A&L-owned)New cross-origin redirect target on logout; URL is unverified external — the SDK has no logout() method, this is entirely consumer-driven
current_company syncnew-with-justification / deferredNo CRM endpoint exists; PRD names an SSO endpoint hit by product BE. CRM FE cannot satisfy without BE work → §5 Q4. The SDK itself exposes no company data

2.1 Current state (CRM)

CRM authenticates via POST /authentication/authenticate (crmAuthScheme.login), persists qcrm_access_token (7d) + qcrm_refresh_token (365d) cookies on .qontak.com, and auto-refreshes 10 min before expiry. Under ENABLE_SEAMLESS_AUTH, it also reads crm_sso_token/global_sso_token and injects X-Auth-Token. There is no inbound SSO session signal. Logout (store/user.js:userLogout) POSTs /users/sign_out, clears cookies/localStorage, and resets $auth — it does not redirect to SSO.

2.2 Proposal (CRM)

Add a new Nuxt plugin plugins/mekari-session.js that, when the centralized_session toggle is on:

  1. Reads the current user's SSO id from $auth.user ([REQUIRED field — §5 Q2]).
  2. Constructs new Session({ currentUser: ssoId, interval: 5 * 60 * 1000 }) from mekari-account-web-sdk (git-installed, bundled at build time — no CDN, no npm registry). interval (ms, ≥1000) is the SDK's only periodic re-check mechanism: it reloads the hidden iframe on a fixed timer (PRD constraint 6.9 recommends 5 minutes). The Session constructor is a singleton (R11) — a second new Session() call anywhere in the app returns the first instance and silently ignores new options; re-initializing with different options requires session.destroy() first.
  3. Subscribes with a single session.on("event", (data) => { switch (data.status) { ... } }) and maps data.status (logged_in | logged_out | server_down) to CRM actions (§2.4 / §2.6). The callback takes exactly one argument — there is no second error parameter, and per-status event names (e.g. session.on("logged_in", …)) are silent no-ops in the real SDK.
  4. Does not call session.refresh() — the SDK exposes no such method. Periodic re-validation is driven entirely by the interval option (step 2).
  5. Does not read or write the msli localStorage key — the SDK owns it internally. On server_down, CRM takes no destructive action (fail-open, PRD constraint 6.10); there is no consumer-side fallback heuristic.

The plugin loads after ~/plugins/auth (so $auth.user is available) and must not interfere with setupAutoTokenRefresh. Teardown (e.g. dev hot-reload, or an explicit re-init) must call session.destroy(), not just session.off()off() alone leaves the iframe and the window message listener attached (R11).

2.3 Database Model

n/a — no database changes. CRM FE introduces no new client-side persisted state: the SDK owns its msli localStorage key (2-hour expiry) internally, and CRM must not read or write it. CRM does not read _mekari_account either — that cookie is exchanged only between the SDK's iframe and the Session Manager and is not part of CRM's own logic.

There are no status enums owned by CRM here; the session status lifecycle (§2.6) is a transient client state, not persisted.

2.4 APIs / Contracts

Outbound (CRM → others)

MethodEndpointAuthOwnerTagFailure behavior
GET (via SDK iframe)https://sm.mekari.com/currentSM-managed session cookieA&Lnew (external)SDK-internal retry / checkTimeout + msli fallback; exhausted → server_down event
POST/api/mobile/v2.7/users/sign_outAuthorization: $auth.getToken('crm')CRM BEreused.catch → still proceed to client cleanup (existing behavior)
GET/api/mobile/v2.7/users/me/feature_enabledbearerCRM BEreusedtoggle read; on error treat feature as off (fail-closed → SDK not loaded)
Redirectaccount.mekari.com/sign_outcookieSSO (A&L)new (external)window.location.href after local cleanup; URL is unverified external — the SDK has no logout() method, this redirect is entirely consumer-driven
GET…/current_companyclient_credentials / meSSO (A&L)deferred§5 Q4 — not implemented in this RFC; the SDK itself exposes no company data at all

Inbound (SDK events → CRM)

The SDK adds its own window message listener internally (checking only event.data.source === "mekari-account-web-sdk", not event.origin — see §3.1 security finding) and dispatches to consumers via exactly one event name: session.on("event", (data) => …). The callback is single-argumentdata: { status, sessionId? } (sessionId only present when includeSessionId: true is passed to the constructor). There is no second error parameter, and subscribing to a per-status name (e.g. session.on("logged_in", …)) is a silent no-op.

data.statusPayloadCRM action
logged_in{ status: "logged_in" } (+ sessionId if enabled)no navigation; emit RUM action
logged_out{ status: "logged_out" }run sign-out (ADR-4): store/user/userLogout → redirect account.mekari.com/sign_out. An SSO account switch is also delivered here — the SDK compares the incoming SSO id to the configured currentUser internally and cannot distinguish "switched account" from "logged out"; it never exposes the other account's id (ADR-5).
server_down{ status: "server_down" } (SDK's internal checkTimeout + msli check both failed)fail-open (PRD constraint 6.10): take no destructive action, do not force sign-out

2.5 Sequence Diagrams

Happy path — page load, session valid

sequenceDiagram
autonumber
participant U as User Browser
participant CRM as CRM SPA (plugin)
participant FT as feature_enabled (CRM BE)
participant SDK as mekari-account-web-sdk Session
participant IF as iframe → sm.mekari.com/current
participant SM as Session Manager
participant R as SSO Redis

U->>CRM: load authenticated page
CRM->>FT: GET /users/me/feature_enabled
FT-->>CRM: centralized_session = true
CRM->>SDK: new Session({ currentUser: ssoId, interval: 5*60*1000 })
SDK->>IF: inject iframe
IF->>SM: GET /current (cache ≤5s)
SM->>R: validate + update last_request_at (idle <2h)
R-->>SM: session ok, ssoId
SM-->>IF: render page w/ ssoId
IF-->>SDK: postMessage({ source: "mekari-account-web-sdk", ssoId })
SDK-->>CRM: event { status: "logged_in" }
Note over CRM: no navigation, RUM action mekari_session.event=logged_in

Failure path — Session Manager unreachable (server_down)

sequenceDiagram
autonumber
participant CRM as CRM SPA (plugin)
participant SDK as mekari-account-web-sdk Session
participant IF as iframe → sm.mekari.com/current

CRM->>SDK: new Session({ currentUser: ssoId, interval: 5*60*1000 })
SDK->>IF: inject iframe
IF--xSDK: timeout / no postMessage within checkTimeout
SDK->>SDK: internal msli check (SDK-owned, 2h) also fails
SDK-->>CRM: event { status: "server_down" }
CRM->>CRM: fail-open (PRD 6.10) — no destructive action, no forced sign-out
Note over CRM: RUM error mekari_session.event=server_down

Failure path — logout from CRM (cross-origin)

sequenceDiagram
autonumber
participant U as User
participant CRM as CRM SPA
participant BE as CRM BE (/users/sign_out)
participant SSO as account.mekari.com/sign_out

U->>CRM: click logout
CRM->>BE: POST /users/sign_out (Authorization)
BE-->>CRM: 200 (or error → still proceed)
CRM->>CRM: deleteUserLocalData + deleteSsoCookies + $auth.reset()
CRM->>SSO: window.location.href = account.mekari.com/sign_out
SSO-->>U: destroy SSO session + mapping → redirect to sign in

account.mekari.com/sign_out is consumer-driven and outside the SDK contract — the SDK exposes no logout() method — so this redirect's correctness is CRM's own responsibility and is unverified externally pending Infosec/A&L confirmation.

2.6 Session State Machine (client-side)

stateDiagram-v2
[*] --> Unknown
Unknown --> LoggedIn: status logged_in
Unknown --> LoggedOut: status logged_out
Unknown --> Degraded: status server_down (fail-open, PRD 6.10)
LoggedIn --> LoggedOut: status logged_out (incl. an SSO account switch — see §2.4)
LoggedIn --> Degraded: status server_down (fail-open, no destructive action)
Degraded --> LoggedIn: next iframe re-check (interval) reports logged_in
Degraded --> LoggedOut: next iframe re-check (interval) reports logged_out
LoggedOut --> SignOut
SignOut --> [*]: redirect account.mekari.com/sign_out
LoggedIn --> LoggedIn: interval re-check (fixed timer, not activity-driven)

2.7 Branch & Skip Catalog

BranchTriggerOwnerAction
Toggle offcentralized_session false/absentCRM FESDK not loaded; legacy behavior (skip)
Toggle read errorfeature_enabled failsCRM FEFail-closed: skip SDK (no regression)
logged_innormalCRM FENo-op (SDK owns msli internally); RUM action only
server_downSDK's internal checkTimeout + msli fallback exhaustedCRM FEFail-open (PRD 6.10): keep session, no destructive action

2.8 Technical Decisions (ADR)

Minimum-coverage map: Storage→ADR-6 (resolved: SDK-owned, no CRM storage decision); Sync/async→ADR-3; Caching→ADR-6 (msli 2h is entirely SDK-internal; SM iframe cache ≤5s is SM-owned); Third-party→ADR-1; Consistency→ADR-3; Multi-tenancy→ADR-5 (resolved: the currentUser compare is internal to the SDK, not a CRM decision).

ADR-1 — SDK delivery into CRM

  • Context: The real SDK is mekari-account-web-sdk (v0.3.0), a bundled git dependency: npm install git+https://<user>:<pass>@bitbucket.org/mid-kelola-indonesia/mekari-account-web-sdk#<version>. There is no CDN, no sdk.js script tag, and no npm-registry publication — an earlier draft of this RFC assumed @mekari/sdk from an npm registry and a CDN alternative at account.mekari.com/sm/sdk.js; neither exists. CRM is Nuxt 2 SPA, plugins for third-party SDKs (plugins/datadog-rum.js).
  • Options: none — the SDK ships only as a bundled git dependency; there is no CDN/script-tag alternative to weigh.
  • Decision: git-installed mekari-account-web-sdk, imported in plugins/mekari-session.js.
  • Rationale: it is the only distribution mechanism the real SDK offers; matches the existing plugin pattern and testability (mockable import).
  • Consequences: new pinned git dependency; CI/CD needs credentialed git access to the private Bitbucket repo and a pinned version ref (replaces the earlier "confirm private npm registry" framing — see §5 Q6). The SDK still injects an iframe to sm.mekari.com at runtime, so origin/CSP policy (§3.1) still applies.
  • Reversibility: high — remove plugin + dep; toggle off instantly.

ADR-2 — Feature gating

  • Context: PRD gates behavior behind toggle centralized_session. CRM has custom_features from /users/me/feature_enabled.
  • Options: (a) reuse custom_features; (b) new env flag; (c) new packageFeatures entry.
  • Decision: (a) reuse custom_features, check code centralized_session.
  • Rationale: server-driven, per-company/app rollout (PRD step 5) matches feature_enabled semantics; no new mechanism. Pre-baked.
  • Consequences: exact code string must be confirmed (§5 Q3); fail-closed on read error.
  • Reversibility: high — server toggles off.

ADR-3 — SDK mount point & event wiring

  • Context: SDK must run on all authenticated pages with $auth.user available, without fighting setupAutoTokenRefresh.
  • Options: (a) Nuxt plugin after ~/plugins/auth; (b) per-page mixin; (c) middleware.
  • Decision: (a) plugin, registered after auth+auto-token-refresh in nuxt.config.js:plugins.
  • Rationale: single global init (like RUM), guaranteed $auth ready, one session.on("event", …) subscription. Async/event-driven (no blocking); consistency model is eventual — the SDK re-checks the session on a fixed interval timer (constructor option, ms, ≥1000; recommended 5*60*1000 per PRD constraint 6.9) by reloading its hidden iframe. This is the SDK's only periodic mechanism — there is no session.refresh() and no activity-driven refresh.
  • Consequences: SPA route changes don't reload the plugin, but that's not a gap — the interval timer runs independently of navigation. The Session constructor is a singleton (R11): a second new Session() anywhere returns the first instance and ignores new options; re-init requires session.destroy() first. Teardown must call destroy(), not just off(), or the iframe + window listener stay attached.
  • Reversibility: high.

ADR-4 — logged_out / server_down action

  • Context: the real SDK dispatches only logged_in, logged_out, and server_down — there is no switch_user (see ADR-5). PRD: product sign-out flow on logged_out; fail-open on server_down (PRD 6.10).
  • Options: (a) reuse store/user/userLogout; (b) bespoke teardown.
  • Decision: (a) reuse userLogout on logged_out, then redirect to account.mekari.com/sign_out. On server_down, fail-open — no teardown, no redirect.
  • Rationale: userLogout already clears cookies/localStorage/$auth; only the SSO redirect is new (S5). No duplication.
  • Consequences: adds cross-origin redirect to logout; the account.mekari.com/sign_out URL is consumer-driven and unverified externally — the SDK has no logout() method, so CRM alone owns this redirect's correctness. Also verify it doesn't break existing in-app logout callers (§5 Q7).
  • Reversibility: medium — redirect target behind same toggle.

ADR-5 — Account-switch handling (RESOLVED — folded into ADR-4)

  • Original framing (incorrect): an earlier draft of this RFC assumed the SDK emits a dedicated switch_user event and designed an interim sign-out+toast plus a deferred "seamless SSO-autologin" path (§5 Q1).
  • Ground truth: the real SDK (mekari-account-web-sdk v0.3.0, session.ts) has exactly three statuses — logged_in, logged_out, server_down — and no switch_user. Internally it compares the iframe's ssoId to the configured currentUser; any mismatch (including a genuine account switch on SSO) surfaces as a plain logged_out, and the SDK never exposes the other account's id to the consumer.
  • Decision: no separate handling is needed or possible. An SSO account switch is indistinguishable from a logout to CRM and runs the exact same sign-out path as ADR-4. There is no "user has changed" toast (the SDK gives CRM nothing to detect a switch specifically) and no interim/seamless distinction to make.
  • Consequences: any post-re-login company re-sync remains a separate BE/SSO concern (ADR-7 / §5 Q4), not something the SDK or this status drives.
  • Status: RESOLVED by real SDK ground truth (mekari-account-web-sdk v0.3.0, session.ts). §5 Q1 and Q5 closed as moot.

ADR-6 — msli fallback storage (RESOLVED — not a CRM decision)

  • Original framing (incorrect): an earlier draft designed a CRM-owned localStorage msli timestamp + _mekari_account cookie heuristic in a new utils/helpers/mekari-session.js module.
  • Ground truth: msli (localStorage key "msli", 2-hour expiry) is owned internally by mekari-account-web-sdk: set on logged_in, cleared on logged_out, and consulted automatically on the SDK's own checkTimeout to decide between re-emitting logged_in (msli still fresh) or server_down (msli stale/absent). Consumers must not read or write the msli key — doing so would collide with and corrupt the SDK's own fallback state.
  • Decision: no CRM-owned fallback module. On server_down, CRM fails open (PRD constraint 6.10): no destructive action, no forced sign-out.
  • Consequences: the utils/helpers/mekari-session.js helper and its story (previously S6) are removed; effort recomputed (§4.C, task-breakdown).
  • Status: RESOLVED by real SDK ground truth (mekari-account-web-sdk v0.3.0, session.ts).

ADR-7 — current_company sync

  • Context: PRD products sync current company after session via SSO current_company (BE, client_credentials). CRM uses teams (/users/crm_teams, /crm/teams), and no current_company endpoint or field was found. The real mekari-account-web-sdk (v0.3.0) exposes no company/current-company surface at all — its public API is limited to constructor, destroy(), on(), off() — so this was never an SDK gap, only a genuine cross-team (BE/SSO) dependency.
  • Options: (a) implement once CRM BE exposes current_company; (b) map to CRM team context; (c) defer.
  • Decision: (c) defer — out of FE-deliverable scope; needs BE RFC.
  • Rationale: no in-repo contract exists; inventing one violates anti-hallucination. Whether CRM even has a per-company switch equivalent is unconfirmed.
  • Consequences: after a logged_out-driven re-login (§2.4), company context resolves through CRM's normal login — acceptable for the interim.
  • Reversibility: n/a (deferred).
  • Status: §5 Q4 [critical].

2.9 Role × Endpoint Authorization

Rolefeature_enabledsign_outSDK iframe
Any authenticated userread own toggleown sessionown SSO session (SM-managed, matched via currentUser)
banned/suspended/freezed/expiredguarded by crm-user before SDKsameunchanged

n/a — no new authorization surface; SDK is per-user via currentUser compare (SDK-internal — CRM never sees the comparison result, only the resulting status).


3. High-Availability & Security

3.1 Security — SDK origin policy

The SDK injects an iframe to https://sm.mekari.com/current and exchanges postMessage. CRM (the SDK host) must be hardened. CRM currently has no CSP configured (nuxt.config.js head has meta/link only — verified).

  • postMessage origin validation — UPSTREAM SECURITY FINDING, not a CRM fix (verified against mekari-account-web-sdk v0.3.0 session.ts): the SDK adds its own window message listener and checks only event.data.source === "mekari-account-web-sdk". It does not validate event.origin. Because the SDK owns this listener internally (public API is limited to constructor, destroy(), on(), off()), CRM cannot intercept or override it to add origin validation of its own — there is no hook for a consumer-side check. This is recorded as an open infosec finding against the SDK owner (Account & Launchpad), not something CRM can mitigate (OWASP A08:2021 Software and Data Integrity Failures / A07:2021 Identification and Authentication Failures — because event.origin is never validated, any frame or window, including a cross-origin one, able to post a message with a spoofed source field would satisfy the SDK's check). Track as a standing open security item, not a "confirm" question (§5 Q10).
  • CSP [REQUIRED decision — §5 Q8]: add frame-src/child-src https://sm.mekari.com (not account.mekari.com, and not script-src — the SDK is a bundled dependency, not a runtime script load). frame-ancestors on the SM side applies to the sm.mekari.com origin and must whitelist CRM's domain before pilot (§4.A). Since CRM is SPA-only with no server headers in nuxt.config.js, CSP must be delivered by the CDN/edge (out of repo) or a <meta http-equiv>mechanism unconfirmed.
  • CRM does not read or write any SSO-managed cookie for this integration; the SDK's iframe/cookie exchange with the Session Manager is entirely internal to the SDK.

3.2 Observability (FE)

Reuse @datadog/browser-rum (plugins/datadog-rum.js, RUM init gated by if ($config.ddEnabled === 'true')plugins/datadog-rum.js:4). Emit:

  • RUM custom action mekari_session.event with { status } for every session.on("event", …) callback invocation.
  • RUM error on server_down. Not on postMessage origin mismatch — CRM cannot observe this; the SDK owns the message listener internally and does not expose mismatches to consumers (§3.1 security finding).
  • Structured console-free logging via existing RUM (no new console.log).

Metric naming follows existing service: qontak-crm-frontend RUM convention (plugins/datadog-rum.js:9). Backend p95/RPS alerts (PRD §3) are A&L-owned.

3.3 HA / Performance (FE slice)

CRM adds one async, non-blocking SDK init per page; the toggle plus the SDK's own internal msli fallback and fail-open server_down handling ensure CRM degrades gracefully if sm.mekari.com/current is slow/down (no blocking render). The 6k RPS / 50ms latency targets (PRD §3) are SM-side, not CRM-FE.


4. Backwards Compatibility and Rollout Plan

4.A Rollout

StageAudienceGo/No-go evidence
0. Build behind togglenone (toggle off)Plugin + dep merged; centralized_session off everywhere; no behavior change
1. Internal pilotCRM internal companyToggle on for 1 company; RUM mekari_session.event flowing; zero sign-out-loop errors
2. Gradual% of companieserror rate flat; no spike in server_down sign-outs
3. GAalltoggle default on

CRM rollout is after the PRD's Launchpad pilot (PRD §4 step 5) — CRM's domain must be added to the sm.mekari.com frame-ancestors whitelist by A&L first.

4.B Backwards compatibility & Verification (pre-merge)

Pre-merge commands (in order, sourced from package.json scripts):

yarn lint:js # eslint . --ext .js,.vue --cache
yarn test # jest test -u
yarn test-file tests/plugins/mekari-session.spec.js
yarn build # nuxt build (verify SDK import resolves)

lint:js, test, test-file (jest), build (nuxt build) verified in package.json scripts (explorer-confirmed).

4.C Agent Execution Plan

#ChunkFilesCommandsAcceptance criteria (assertable)
1Add dependencypackage.json (+ yarn.lock)npm install git+https://<user>:<pass>@bitbucket.org/mid-kelola-indonesia/mekari-account-web-sdk#<version> (pin ref)mekari-account-web-sdk in deps at a pinned git ref; yarn install --frozen-lockfile passes — blocked on §5 Q6 (git access / pinned ref confirmation)
2SDK plugin + toggle gateplugins/mekari-session.js (new); nuxt.config.js (plugins[]); tests/plugins/mekari-session.spec.js (new)yarn test-file tests/plugins/mekari-session.spec.js; yarn lint:jsTest: toggle off → no Session; on → Session built w/ currentUser + interval: 5*60*1000; single session.on("event", …) subscription registered
3Event handlersplugins/mekari-session.js; store/user.js (no msli handling — SDK-owned)yarn testPer-status tests pass (logged_in/logged_out/server_down) per §2.4; server_down is fail-open (no dispatch, no redirect)
4Logout → SSO redirectstore/user.js:userLogout (success path)yarn test-file tests/store/user.spec.jsTest: after sign_out resolves, window.location.href set to account.mekari.com/sign_out
5Observabilityplugins/mekari-session.js (RUM calls)yarn lint:js; yarn buildRUM action mekari_session.event emitted in handler unit test (mock datadogRum)

Chunk order respects deps: plugin (2) before handlers (3); store edits (3,4) tested in isolation. The earlier msli fallback helper chunk is removed — the SDK owns msli internally (ADR-6 resolution note).

4.D Verification & Rollback Recipe

Post-deploy signals:

  • Datadog RUM action mekari_session.event present for piloted company.
  • No spike in CRM sign-out / login redirects vs baseline (RUM view.error/session count).
  • server_down rate within expected (mekari_session.event=server_down).

Rollback (numbered, agent-executable):

  1. Set centralized_session toggle off for affected company/all (server-side feature_enabled) — instant, no deploy. Primary lever.
  2. If code-level revert needed: revert the PR adding plugins/mekari-session.js
    • nuxt.config.js plugin entry; redeploy.
  3. Confirm CRM auth/logout behaves as pre-RFC (token login works, in-app logout no longer redirects to account.mekari.com/sign_out).
  4. Verify RUM session/error counts return to baseline.

5. Concern, Questions, or Known Limitations

#SeverityQuestion / limitationBlocks
Q1[critical] RESOLVED — mootThe real SDK has no switch_user status; an SSO account switch reaches CRM as a plain logged_out and reuses ADR-4's sign-out path — no separate autologin contract is needed. Resolved by mekari-account-web-sdk v0.3.0, session.ts.
Q2[critical]Source of the current user's SSO id in CRM: does /users/me (crmAuthScheme.fetchUser) expose it? Only external_company_id (company-level, plugins/mixpanel.js:30) verified. The SDK's currentUser constructor input cannot be filled without this.§7 yes
Q3[critical]Exact centralized_session feature code/string in feature_enabled (string code vs CP-QONTAKCRM-YYYY-NNNN).§7 yes
Q4[critical]current_company sync for CRM: no endpoint/field exists; CRM uses teams. Is there a CRM company-context equivalent, and is a BE RFC needed? (ADR-7 deferred; the SDK itself has no company surface at all — this is a pure BE/SSO dependency, not an SDK gap.)§7 yes
Q5[important] RESOLVED — mootThe "user has changed" toast was tied to the fictional switch_user event; since an account switch surfaces as plain logged_out, there is no dedicated toast surface to build and no component to confirm.
Q6[important]Confirm CI/CD and local-dev git access/credentials to bitbucket.org/mid-kelola-indonesia/mekari-account-web-sdk and pin an exact version ref for npm install git+https://...#<version>. (Previously misframed as "is it published to a private npm registry" — the real SDK has no npm-registry distribution at all.)chunk 1
Q7[important]All existing in-app callers of userLogout — will adding the account.mekari.com/sign_out redirect break any flow expecting to stay in-app (e.g. account-switch, embed layout)?chunk 4
Q8[important]CSP delivery: CRM has no server/header CSP (nuxt.config.js). frame-src/child-src must whitelist sm.mekari.com (not account.mekari.com) — where is the header set (edge/CDN vs meta)? Infosec decision.§3.1
Q9[nice-to-have] RESOLVED — mootThe SDK has no session.refresh() and no activity-driven refresh; periodic re-validation is entirely the interval constructor option (recommended 5*60*1000, PRD constraint 6.9). No throttle to define.
Q10[critical] (infosec finding, not a question)Upstream security finding, not requiring CRM-side investigation: mekari-account-web-sdk v0.3.0 validates incoming postMessages only via event.data.source, never event.origin, and owns the window listener internally so CRM cannot mitigate it (§3.1). Track against the SDK owner (Account & Launchpad); does not block this RFC's own chunks but should gate cross-RFC Infosec sign-off.Infosec sign-off (cross-RFC)

6. Comment logs

DateAuthorComment
2026-06-27CRM Frontend (draft)Initial CRM-FE integration draft grounded against schemes/crmAuthScheme.js, store/user.js, utils/helpers/auth.js, nuxt.config.js. 4 critical unknowns block execution (token model vs authz-code, user_sso_id source, toggle code, current_company).
2026-07-02latest review (reconciliation)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

Failing gates:

  • B2 / F (contracts): SDK input currentUser (the CRM user's SSO id) is unverified in CRM /users/me (Q2). Cannot construct Session correctly.
  • D2 (decision closure): current_company deferred without a BE contract (Q4). (The former switch_user gap is resolved — ADR-5 / §5 Q1 — the real SDK has no such status.)
  • C2 (anti-hallucination): centralized_session exact code not verified (Q3) — left as placeholder rather than invented.

Resolve Q2–Q4 ([critical]) to flip the marker to yes. Chunk 5 (observability) and the shell of chunk 2 (plugin scaffold + toggle gate) are independently executable today; chunks 1, 3, 4 depend on the critical answers or the git dependency landing (Q6).