docs: server-side hash recipe and Novu divergence guide#81
Conversation
The divergence page claimed multi-tenancy maps to environments and that payloads are stored as plain text. Environments isolate data, not tenants (admin roles are instance-wide); payloads are stored as jsonb and pass through verbatim. Also states the createEventSource escape hatch instead of 'nothing to configure beyond backoff'.
The recipe comments addressed the reader. Rephrased impersonally per the repo comment style.
|
| Filename | Overview |
|---|---|
| docs/content/docs/auth.mdx | Adds a "Never sign a client-supplied id" error callout and a complete secure hash endpoint recipe. The example correctly derives the subscriber id from the server-side session, not request input. No issues found. |
| docs/content/docs/coming-from-novu.mdx | New migration guide page. Divergence table and the first three "in practice" bullets are accurate against the codebase invariants. The JSX-like syntax in the frontmatter description will show raw angle brackets in meta tags, and two of the five table rows have no corresponding "in practice" entry. |
| docs/content/docs/meta.json | Registers coming-from-novu in the docs nav. Straightforward addition, no issues. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Browser as Browser / Inbox
participant Backend as Your Backend (requireLogin)
participant Chimely as Chimely Server
Browser->>Backend: GET /api/inbox-auth (authenticated session)
Note over Backend: Resolve subscriberId from req.session.userId (never from request input)
Backend->>Backend: HMAC-SHA256(secret, subscriberId)
Backend-->>Browser: "{ subscriberId, subscriberHash }"
Browser->>Chimely: SSE / REST X-Chimely-Subscriber + X-Chimely-Subscriber-Hash
Chimely->>Chimely: Verify hash against environment secret
Chimely-->>Browser: Inbox data for subscriberId
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Browser as Browser / Inbox
participant Backend as Your Backend (requireLogin)
participant Chimely as Chimely Server
Browser->>Backend: GET /api/inbox-auth (authenticated session)
Note over Backend: Resolve subscriberId from req.session.userId (never from request input)
Backend->>Backend: HMAC-SHA256(secret, subscriberId)
Backend-->>Browser: "{ subscriberId, subscriberHash }"
Browser->>Chimely: SSE / REST X-Chimely-Subscriber + X-Chimely-Subscriber-Hash
Chimely->>Chimely: Verify hash against environment secret
Chimely-->>Browser: Inbox data for subscriberId
Reviews (1): Last reviewed commit: "docs(auth): reword endpoint example comm..." | Re-trigger Greptile
| ## What this means in practice | ||
|
|
||
| - **Tenant scoping.** There is no `context` / `contextHash` equivalent | ||
| inside one instance. Admin users and roles span every environment, so an | ||
| environment is not a tenant boundary: serving multiple tenants means | ||
| running one instance per tenant. Environments within an instance separate | ||
| stages (development, staging, production), each with its own HMAC secret. | ||
| See [Auth & HMAC](/docs/auth). | ||
| - **Realtime.** There is no `socketUrl` equivalent. The client opens one SSE | ||
| stream per client instance and treats events as hints to refetch over REST, | ||
| so a dropped or missed event is harmless. `<Inbox />` exposes `backoff`; | ||
| outside the browser, the headless client also takes a `createEventSource` | ||
| factory (polyfills, React Native, testing). | ||
| - **Rich content.** Payload JSON is stored and passed through verbatim; the | ||
| default renderer treats the `title` and `body` strings as plain text. To | ||
| render markup you trust, override `renderItem` (or `renderSubject` / | ||
| `renderBody`) and sanitize in your own code. |
There was a problem hiding this comment.
"What this means in practice" covers only 3 of 5 table rows
The divergence table lists 5 entries but the "What this means in practice" section has bullets for only 3 (tenant scoping, realtime, rich content). Schedule/quiet hours and keyless mode/plan-gated limits are absent. A reader who hits that section expecting to understand every divergence will find no actionable guidance for those two rows, and may assume the omission is an error rather than a conscious choice.
There was a problem hiding this comment.
Resolved as of abb278c (docs(novu): practice bullets for all five rows), which landed before this thread was triaged: the "What this means in practice" section now has a bullet per table row — Tenant scoping, Realtime, Rich content, Quiet hours, and Keyless mode. The Quiet-hours bullet explains why there is no schedule to recreate (in-app is the only channel) and the Keyless bullet covers the missing cloud signup plus the operational-not-commercial limits.
| @@ -0,0 +1,41 @@ | |||
| --- | |||
| title: Coming from Novu | |||
| description: What Chimely's <Inbox /> covers, and where it deliberately diverges. | |||
There was a problem hiding this comment.
The
<Inbox /> JSX-like syntax in the YAML frontmatter description will appear verbatim in <meta name="description"> and Open Graph tags — search engines and social-preview crawlers will display the raw angle-bracket text. Plain prose avoids this.
| description: What Chimely's <Inbox /> covers, and where it deliberately diverges. | |
| description: What Chimely's Inbox component covers, and where it deliberately diverges from Novu. |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Good catch — frontmatter is plain YAML, so the JSX-looking text would have shipped verbatim into the meta description and OG tags. Applied your suggested wording in f5a9f2a; the body prose keeps the literal <Inbox /> where MDX renders it as code.
|
Self-review note: one imprecision survived our final pass — the Keyless bullet says notifications accept "at most 100 recipients", but the enforced contract is a 1-100 range (the minimum is validated too). Happy to push the one-word fix if preferred, or it can be folded into any maintainer edit. |
This PR closes the two remaining documentation gaps from the #34 parity audit. The HMAC auth docs showed how to compute the subscriber hash but never warned against signing a client-supplied id, so a naive endpoint turns the scheme into a confused deputy: the secret never leaks, yet any authenticated user can ask the backend to mint a valid hash for any inbox. Separately, the deliberate Novu divergences from Part 3 of the audit were undocumented, leaving migrators to mistake design decisions for roadmap gaps.
What changed:
docs/content/docs/auth.mdx: a "Never sign a client-supplied id" callout plus a secure hash endpoint recipe. The example authenticates the caller, resolves the subscriber id from the server-side session, and signs that; the id never comes from query, body, or headers.docs/content/docs/coming-from-novu.mdx(new page, registered inmeta.json): the Part-3 divergence table (context/contextHash multi-tenancy, WebSocket transport, HTML rendering, schedule/quiet hours, keyless/plan gating) with the reasoning for each, plus a section on what each divergence means for a migration. Every claim is backed by code on main.Ordering notes for two open PRs that touch neighboring docs:
auth.mdxto the env-bound form. This PR documents the formula as it exists on main, so whichever merges second needs a one-line follow-up: the recipe's.update(subscriberId)line becomes the env-bound form after feat(auth): bind the subscriber hash to its environment #79.sdk-reference.mdx; this PR intentionally does not duplicate it. A cross-link from the new page's realtime bullet can follow once feat(react): announce arrivals, document client sharing #78 lands.Addresses the remaining documentation gap from #34. That issue is an RFC meta-issue that also tracks child issues #35-#39, so closing it is the maintainers' call, not this PR's.