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 pathsm.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
| Field | Value | Notes |
|---|---|---|
| Status | RFC | Open for review; not yet Ready for agent execution (see §7) |
| Type / Sub-type | frontend / enhancement | CRM consumes a new shared SDK; no new product surface beyond a notification |
| Title | Centralized Web Session — CRM FE Integration of mekari-account-web-sdk Session | — |
| Owner | Account & Launchpad (SDK) · CRM Frontend (this integration) | Cross-squad |
| Authors | Qontak CRM Frontend | — |
| Reviewers | A&L (SSO/SM), CRM FE tech lead | Cross-squad review required |
| Approver(s) | CRM FE tech leader, Infosec [REQUIRED] | Infosec gates CSP / SDK origin policy |
| Submitted | 2026-06-27 | ISO-8601 |
| Last updated | 2026-07-02 | ISO-8601 |
| Target release | 2026-Q3 | Pilot is Launchpad first (PRD §4.4); CRM follows in step 5 |
| Related documents | Centralized Web Session RFC (Confluence) | Driver |
| Discussion | [REQUIRED] | Slack thread |
Sections at a Glance
| § | Section | CRM-FE hint |
|---|---|---|
| 1 | Overview | Problem, success criteria, scope, Detail 1.A coverage matrices, 1.B decision index, 1.C per-story change map |
| 2 | Technical Design | Detail 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 |
| 3 | High-Availability & Security | SDK origin/CSP policy, FE observability, fail-open on server_down |
| 4 | Backwards Compatibility & Rollout | Feature toggle centralized_session, Agent Execution Plan, Verification & Rollback Recipe |
| 5 | Concerns, Questions, Known Limitations | Severity-tagged open questions |
| 6 | Comment logs | Review trail |
| 7 | Ready for agent execution | Marker + 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_down — there 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-sdkSessionon all authenticated pages with the current user's SSO id (currentUseroption), gated behind acentralized_sessionfeature 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 singlesession.on("event", (data) => …)subscription that switches ondata.status. - CRM re-validates the SSO session periodically by passing
interval(ms, ≥1000; recommended5 * 60 * 1000per PRD constraint 6.9) to theSessionconstructor — the SDK reloads its hidden iframe on this fixed timer. There is nosession.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. Onserver_down(the SDK's owncheckTimeout+mslifallback 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 forcurrent_companysync — 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_outandserver_downboth reuse CRM's existing sign-out flow; there is no "user has changed" toast (that surface was tied to aswitch_userevent 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.
1.5 Related Documents
| Doc | Kind | Link | Used for |
|---|---|---|---|
| Centralized Web Session RFC | authoritative (driver) | Confluence | SDK contract, event names, flows, rollout |
Detail 1.A — Coverage Matrices
PRD Section Coverage
| PRD section | Covered in RFC | Notes |
|---|---|---|
| 1. Overview / known issues | §1.1 | CRM-relevant stale-session cases mapped |
| Success Criteria | §1.2 | FE-slice subset; SDK/Redis criteria are A&L |
| Out of Scope | §1.3 | Inherited |
| Dependencies (SDK, SM service, Redis) | §2.0 / §5 | SDK consumed; SM+Redis are A&L deps |
| 2. Technical Design — Current/Proposal | §2.1–§2.2 | CRM current auth vs SDK proposal |
| How to use the SDK | §2.4 (Inbound) / §2.2 | Mapped to CRM plugin |
Local Storage (msli) | §2.3 | SDK-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 Q1 | Resolved — 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 Q1 | Full — 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.2 | Reuse userLogout + redirect to sign_out |
| User Switch Account | §2.6 (state) / §5 Q1 | Resolved — 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.3 | Confirmed no DDL |
| 3. HA & Security | §3 | FE: CSP/origin + observability |
| 4. Rollout Plan | §4 | CRM = step 5 (post-Launchpad pilot) |
| 5. Open Questions | §5 | CSP vs referrer/origin + CRM-specific |
| FE Implementation Scope (per product repo) | §1.C / §4 | This RFC's core |
UI / Consumer Surface Coverage
| Surface | Type | Read endpoint | Notes |
|---|---|---|---|
Sign-out redirect (logged_out / server_down) | navigation | n/a | Reuses userLogout + account.mekari.com/sign_out (consumer-driven; URL is unverified external — the SDK has no logout() method) |
| Every authenticated CRM page (SDK host) | runtime | n/a | SDK 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 role | CRM handling | Notes |
|---|---|---|
| Authenticated CRM user (any role) | SDK loads with their SSO id | Session events role-agnostic |
| banned / suspended / freezed / expired | Existing crm-user middleware redirects unchanged | SDK 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)
| # | Decision | Chosen | Pre-baked? | ADR |
|---|---|---|---|---|
| D1 | SDK delivery into CRM | git-installed mekari-account-web-sdk (bundled dependency) + new Nuxt plugin | yes — the SDK ships only as a bundled git dependency, no alternative to evaluate | §2 ADR-1 |
| D2 | Feature gating | Reuse custom_features w/ code centralized_session | yes (PRD names toggle) | §2 ADR-2 |
| D3 | SDK mount point + event wiring | New plugins/mekari-session.js, wired to $auth + store/user, single session.on("event", …) subscription | no | §2 ADR-3 |
| D4 | logged_out / server_down action | Reuse existing userLogout → redirect to account.mekari.com/sign_out (consumer-driven; SDK has no logout()) | no | §2 ADR-4 |
| D5 | switch_user action | The 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) |
| D6 | msli fallback storage | msli (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) |
| D7 | current_company sync | Deferred — BE dependency, CRM uses teams not SSO current_company; the SDK exposes no company data at all | no — see §5 Q4 | §2 ADR-7 |
Detail 1.C — Per-Story Change Map
| Story (from PRD FE Implementation Scope) | Layer scope | Changes | Acceptance criteria | RFC anchors |
|---|---|---|---|---|
S1 Add mekari-account-web-sdk Session, load with the current user's SSO id | FE-only | package.json dep (git install); plugins/mekari-session.js (new); registered in nuxt.config.js:plugins | Unit test: plugin instantiates Session with currentUser from $auth.user; yarn test green | §2.2, §4.C chunk 1–2 |
S2 Gate behind centralized_session toggle | FE-only | Read store.state.user.custom_features for code centralized_session; plugin no-ops when off | Test: SDK not constructed when feature absent/false | §2 ADR-2, §4.C chunk 2 |
| S3 Handle 3 SDK statuses | FE-only (Runtime/behavior) | Single session.on("event", (data) => …) handler in plugin → dispatch user/userLogout, redirect | Test 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 sync | FE + BE | Blocked — no CRM current_company endpoint found; teams ≠ SSO company; the SDK has no company surface either | n/a — covered in BE RFC [REQUIRED link] / deferred | §2 ADR-7, §5 Q4 |
S5 Wire product logout to account.mekari.com/sign_out | FE-only | Extend store/user.js:userLogout success path to redirect to SSO sign_out | Test: after sign_out POST resolves, window.location → account.mekari.com/sign_out | §2.5, §4.C chunk 4 |
| S7 Observability | FE-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 (
mslifallback helper) is removed — the SDK ownsmsliinternally (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.jsmsli-fallback helper node is removed — the SDK ownsmsliinternally (R7 / ADR-6 resolution note); there is nothing for CRM to build there.
Existing Code Anchors
| # | Path | What to learn |
|---|---|---|
| 1 | schemes/crmAuthScheme.js | CRM 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. |
| 2 | utils/helpers/auth.js | refreshToken(), setupAutoTokenRefresh() (10-min pre-expiry timer), clearAutoTokenRefresh(). The SDK must not fight this timer. |
| 3 | store/user.js:120 (userLogout) | Logout action: POST ${USER_URL}/sign_out, then deleteUserLocalData + deleteSsoCookies + $auth.reset(). Reuse for logged_out/server_down. |
| 4 | store/user.js:144 (deleteSsoCookies) | Which cookies CRM clears on logout (crm_sso_*, chat_sso_*, global_sso_*). |
| 5 | store/user.js:67 (getCustomFeature) | Feature toggle source: GET /users/me/feature_enabled → custom_features: [{code, enabled}]. Gate centralized_session here. |
| 6 | middleware/crm-user.js | Auth guard order: runs getUserData then status redirects. SDK mounts after this. |
| 7 | plugins/datadog-rum.js | RUM init pattern + config gate ($config.ddEnabled === 'true', plugins/datadog-rum.js:4). Reuse for session observability. |
| 8 | nuxt.config.js:58 (plugins) | Plugin registration order — ~/plugins/auth runs first; add ~/plugins/mekari-session after auth + auto-token-refresh. |
| 9 | assets/variables/endpoints.js | Endpoint constant pattern (USER_URL, CRM_V28). Add SSO/SM constants here if needed. |
| 10 | adapters/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 / Contract | Evidence (verified) |
|---|---|
| CRM auth = LocalScheme token model | schemes/crmAuthScheme.js:12 export default class CrmAuthScheme extends LocalScheme; login() POST + Cookies.set('qcrm_refresh_token', …) :33 |
| logout flow | schemes/crmAuthScheme.js:234 async logout() → this.$auth.reset() :242; store/user.js:120 userLogout({ dispatch }), POST ${USER_URL}/sign_out :131 |
| sign-out cookie cleanup | store/user.js:144 deleteSsoCookies() removes crm_sso_token/chat_sso_token/global_sso_token :145–155 |
| feature toggle mechanism | store/user.js:67 getCustomFeature(); state custom_features store/user.js:12–13; mutation SET_CUSTOM_FEATURE :349 |
| feature-check shape | middleware/redirect-to-v3.js:48 reads store.state.user?.custom_features; :49 matches via features.some((f) => f.code === … && f.enabled) |
| refresh timer | utils/helpers/auth.js:157 setupAutoTokenRefresh; :13 REFRESH_BEFORE_EXPIRY_MINUTES = 10; :248 clearAutoTokenRefresh |
| plugin pattern + order | nuxt.config.js:58–72 plugins[], '~/plugins/auth' first :59 |
| RUM pattern | plugins/datadog-rum.js:1 import { datadogRum }; :4 if ($config.ddEnabled === 'true') |
| endpoint constants | assets/variables/endpoints.js:1 USER_URL, :5 CRM_V28 |
no mekari-account-web-sdk present | searched 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 domain | schemes/crmAuthScheme.js:8 COOKIE_DOMAIN_ENV = process.env.COOKIE_DOMAIN || '.qontak.com' |
CRM currentUser (SSO id) field | UNVERIFIED — /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 endpoint | UNVERIFIED / 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 code | UNVERIFIED — 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
| Concern | Reference file | Note |
|---|---|---|
| Third-party SDK init via plugin | plugins/datadog-rum.js | Conditional config gate; default-export init fn receiving Nuxt ctx |
Plugin accessing $auth/store | plugins/auth.js | window.$auth = app.$auth pattern; ctx access |
| Feature-flag read | middleware/redirect-to-v3.js:48–49 | store.state.user?.custom_features (:48), matched via features.some((f) => f.code === X && f.enabled) (:49) |
| Sign-out orchestration | store/user.js:120 userLogout | Reuse, don't reinvent |
| External redirect | middleware/redirect-to-v3.js:65 | window.location.href for cross-origin |
The earlier "user notification toast" and "
_mekari_accountcookie access" pattern rows are removed: the toast surface was tied to the fictionalswitch_userevent (ADR-5), and CRM never reads_mekari_accountunder the corrected design (R7) — that cookie is exchanged only between the SDK's iframe and the Session Manager.
Reading Order for the Agent
schemes/crmAuthScheme.js— understand the auth/session lifecycle.utils/helpers/auth.js— refresh timer; do not conflict.store/user.js(userLogout,deleteSsoCookies,getCustomFeature).middleware/crm-user.js— guard order.nuxt.config.js(plugins) — registration point + order.plugins/datadog-rum.js— SDK init + RUM pattern.assets/variables/endpoints.js— endpoint constant style.adapters/http/utils.js— SSO-token header interplay.- PRD (Confluence) — SDK event contract + flows.
utils/helpers/package-features.js— feature code conventions.
Existing API check
| Call | Tag | Justification |
|---|---|---|
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_enabled | reused | store/user.js:67 — read centralized_session toggle |
POST /api/mobile/v2.7/users/sign_out | reused | store/user.js:131 — existing CRM sign-out |
account.mekari.com/sign_out redirect | new (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 sync | new-with-justification / deferred | No 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:
- Reads the current user's SSO id from
$auth.user([REQUIRED field — §5 Q2]). - Constructs
new Session({ currentUser: ssoId, interval: 5 * 60 * 1000 })frommekari-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). TheSessionconstructor is a singleton (R11) — a secondnew Session()call anywhere in the app returns the first instance and silently ignores new options; re-initializing with different options requiressession.destroy()first. - Subscribes with a single
session.on("event", (data) => { switch (data.status) { ... } })and mapsdata.status(logged_in|logged_out|server_down) to CRM actions (§2.4 / §2.6). The callback takes exactly one argument — there is no seconderrorparameter, and per-status event names (e.g.session.on("logged_in", …)) are silent no-ops in the real SDK. - Does not call
session.refresh()— the SDK exposes no such method. Periodic re-validation is driven entirely by theintervaloption (step 2). - Does not read or write the
mslilocalStorage key — the SDK owns it internally. Onserver_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)
| Method | Endpoint | Auth | Owner | Tag | Failure behavior |
|---|---|---|---|---|---|
| GET (via SDK iframe) | https://sm.mekari.com/current | SM-managed session cookie | A&L | new (external) | SDK-internal retry / checkTimeout + msli fallback; exhausted → server_down event |
| POST | /api/mobile/v2.7/users/sign_out | Authorization: $auth.getToken('crm') | CRM BE | reused | .catch → still proceed to client cleanup (existing behavior) |
| GET | /api/mobile/v2.7/users/me/feature_enabled | bearer | CRM BE | reused | toggle read; on error treat feature as off (fail-closed → SDK not loaded) |
| Redirect | account.mekari.com/sign_out | cookie | SSO (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_company | client_credentials / me | SSO (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-argument —
data: { 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.status | Payload | CRM 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_outis consumer-driven and outside the SDK contract — the SDK exposes nologout()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
| Branch | Trigger | Owner | Action |
|---|---|---|---|
| Toggle off | centralized_session false/absent | CRM FE | SDK not loaded; legacy behavior (skip) |
| Toggle read error | feature_enabled fails | CRM FE | Fail-closed: skip SDK (no regression) |
logged_in | normal | CRM FE | No-op (SDK owns msli internally); RUM action only |
server_down | SDK's internal checkTimeout + msli fallback exhausted | CRM FE | Fail-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 (
msli2h is entirely SDK-internal; SM iframe cache ≤5s is SM-owned); Third-party→ADR-1; Consistency→ADR-3; Multi-tenancy→ADR-5 (resolved: thecurrentUsercompare 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, nosdk.jsscript tag, and no npm-registry publication — an earlier draft of this RFC assumed@mekari/sdkfrom an npm registry and a CDN alternative ataccount.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 inplugins/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.comat 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 hascustom_featuresfrom/users/me/feature_enabled. - Options: (a) reuse
custom_features; (b) new env flag; (c) newpackageFeaturesentry. - Decision: (a) reuse
custom_features, check codecentralized_session. - Rationale: server-driven, per-company/app rollout (PRD step 5) matches
feature_enabledsemantics; 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.useravailable, without fightingsetupAutoTokenRefresh. - Options: (a) Nuxt plugin after
~/plugins/auth; (b) per-page mixin; (c) middleware. - Decision: (a) plugin, registered after
auth+auto-token-refreshinnuxt.config.js:plugins. - Rationale: single global init (like RUM), guaranteed
$authready, onesession.on("event", …)subscription. Async/event-driven (no blocking); consistency model is eventual — the SDK re-checks the session on a fixedintervaltimer (constructor option, ms, ≥1000; recommended5*60*1000per PRD constraint 6.9) by reloading its hidden iframe. This is the SDK's only periodic mechanism — there is nosession.refresh()and no activity-driven refresh. - Consequences: SPA route changes don't reload the plugin, but that's not
a gap — the
intervaltimer runs independently of navigation. TheSessionconstructor is a singleton (R11): a secondnew Session()anywhere returns the first instance and ignores new options; re-init requiressession.destroy()first. Teardown must calldestroy(), not justoff(), 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, andserver_down— there is noswitch_user(see ADR-5). PRD: product sign-out flow onlogged_out; fail-open onserver_down(PRD 6.10). - Options: (a) reuse
store/user/userLogout; (b) bespoke teardown. - Decision: (a) reuse
userLogoutonlogged_out, then redirect toaccount.mekari.com/sign_out. Onserver_down, fail-open — no teardown, no redirect. - Rationale:
userLogoutalready 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_outURL is consumer-driven and unverified externally — the SDK has nologout()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_userevent and designed an interim sign-out+toast plus a deferred "seamless SSO-autologin" path (§5 Q1). - Ground truth: the real SDK (
mekari-account-web-sdkv0.3.0,session.ts) has exactly three statuses —logged_in,logged_out,server_down— and noswitch_user. Internally it compares the iframe's ssoId to the configuredcurrentUser; any mismatch (including a genuine account switch on SSO) surfaces as a plainlogged_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-sdkv0.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
localStoragemslitimestamp +_mekari_accountcookie heuristic in a newutils/helpers/mekari-session.jsmodule. - Ground truth:
msli(localStorage key"msli", 2-hour expiry) is owned internally bymekari-account-web-sdk: set onlogged_in, cleared onlogged_out, and consulted automatically on the SDK's owncheckTimeoutto decide between re-emittinglogged_in(msli still fresh) orserver_down(msli stale/absent). Consumers must not read or write themslikey — 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.jshelper and its story (previously S6) are removed; effort recomputed (§4.C, task-breakdown). - Status: RESOLVED by real SDK ground truth (
mekari-account-web-sdkv0.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 nocurrent_companyendpoint or field was found. The realmekari-account-web-sdk(v0.3.0) exposes no company/current-company surface at all — its public API is limited toconstructor,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
| Role | feature_enabled | sign_out | SDK iframe |
|---|---|---|---|
| Any authenticated user | read own toggle | own session | own SSO session (SM-managed, matched via currentUser) |
| banned/suspended/freezed/expired | guarded by crm-user before SDK | same | unchanged |
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).
postMessageorigin validation — UPSTREAM SECURITY FINDING, not a CRM fix (verified againstmekari-account-web-sdkv0.3.0session.ts): the SDK adds its ownwindowmessagelistener and checks onlyevent.data.source === "mekari-account-web-sdk". It does not validateevent.origin. Because the SDK owns this listener internally (public API is limited toconstructor,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 — becauseevent.originis never validated, any frame or window, including a cross-origin one, able to post a message with a spoofedsourcefield 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(notaccount.mekari.com, and notscript-src— the SDK is a bundled dependency, not a runtime script load).frame-ancestorson the SM side applies to thesm.mekari.comorigin and must whitelist CRM's domain before pilot (§4.A). Since CRM is SPA-only with no server headers innuxt.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.eventwith{ status }for everysession.on("event", …)callback invocation. - RUM error on
server_down. Not onpostMessageorigin mismatch — CRM cannot observe this; the SDK owns themessagelistener internally and does not expose mismatches to consumers (§3.1 security finding). - Structured
console-free logging via existing RUM (no newconsole.log).
Metric naming follows existing
service: qontak-crm-frontendRUM 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
| Stage | Audience | Go/No-go evidence |
|---|---|---|
| 0. Build behind toggle | none (toggle off) | Plugin + dep merged; centralized_session off everywhere; no behavior change |
| 1. Internal pilot | CRM internal company | Toggle on for 1 company; RUM mekari_session.event flowing; zero sign-out-loop errors |
| 2. Gradual | % of companies | error rate flat; no spike in server_down sign-outs |
| 3. GA | all | toggle 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 inpackage.jsonscripts (explorer-confirmed).
4.C Agent Execution Plan
| # | Chunk | Files | Commands | Acceptance criteria (assertable) |
|---|---|---|---|---|
| 1 | Add dependency | package.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) |
| 2 | SDK plugin + toggle gate | plugins/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:js | Test: toggle off → no Session; on → Session built w/ currentUser + interval: 5*60*1000; single session.on("event", …) subscription registered |
| 3 | Event handlers | plugins/mekari-session.js; store/user.js (no msli handling — SDK-owned) | yarn test | Per-status tests pass (logged_in/logged_out/server_down) per §2.4; server_down is fail-open (no dispatch, no redirect) |
| 4 | Logout → SSO redirect | store/user.js:userLogout (success path) | yarn test-file tests/store/user.spec.js | Test: after sign_out resolves, window.location.href set to account.mekari.com/sign_out |
| 5 | Observability | plugins/mekari-session.js (RUM calls) | yarn lint:js; yarn build | RUM 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
mslifallback helper chunk is removed — the SDK ownsmsliinternally (ADR-6 resolution note).
4.D Verification & Rollback Recipe
Post-deploy signals:
- Datadog RUM action
mekari_session.eventpresent for piloted company. - No spike in CRM sign-out / login redirects vs baseline (RUM
view.error/session count). server_downrate within expected (mekari_session.event=server_down).
Rollback (numbered, agent-executable):
- Set
centralized_sessiontoggle off for affected company/all (server-sidefeature_enabled) — instant, no deploy. Primary lever. - If code-level revert needed: revert the PR adding
plugins/mekari-session.jsnuxt.config.jsplugin entry; redeploy.
- Confirm CRM auth/logout behaves as pre-RFC (token login works, in-app logout
no longer redirects to
account.mekari.com/sign_out). - Verify RUM session/error counts return to baseline.
5. Concern, Questions, or Known Limitations
| # | Severity | Question / limitation | Blocks |
|---|---|---|---|
| Q1 | The 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 | The "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 | The 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
| Date | Author | Comment |
|---|---|---|
| 2026-06-27 | CRM 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-02 | latest 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 constructSessioncorrectly. - D2 (decision closure):
current_companydeferred without a BE contract (Q4). (The formerswitch_usergap is resolved — ADR-5 / §5 Q1 — the real SDK has no such status.) - C2 (anti-hallucination):
centralized_sessionexact 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).