Skip to content

docs: server-side hash recipe and Novu divergence guide#81

Open
ravidsrk wants to merge 7 commits into
dodopayments:mainfrom
ravidsrk:ravidsrk/cs-34-docs-hmac-gap
Open

docs: server-side hash recipe and Novu divergence guide#81
ravidsrk wants to merge 7 commits into
dodopayments:mainfrom
ravidsrk:ravidsrk/cs-34-docs-hmac-gap

Conversation

@ravidsrk

Copy link
Copy Markdown

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 in meta.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:

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.

ravidsrk added 4 commits July 16, 2026 01:29
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.
@ravidsrk

Copy link
Copy Markdown
Author

@claude review this PR. @codex review this PR.

@greptile-apps

greptile-apps Bot commented Jul 15, 2026

Copy link
Copy Markdown

Greptile Summary

This PR closes two documentation gaps from the #34 parity audit: a security callout plus a secure hash endpoint recipe in auth.mdx, and a new coming-from-novu.mdx page cataloguing deliberate divergences from Novu with migration guidance.

  • auth.mdx gains an error callout warning against signing a client-supplied subscriber id, accompanied by a complete Express example that derives the id from the server-side session — the confused-deputy risk is clearly explained and the fix is shown.
  • coming-from-novu.mdx (new) provides a five-row divergence table (context/contextHash multi-tenancy, WebSocket transport, HTML rendering, schedule/quiet hours, keyless/plan gating) with rationale, then three "in practice" migration bullets; the last two divergences have no corresponding practice entry.
  • meta.json registers the new page in the docs navigation.

Confidence Score: 4/5

Safe to merge — all changes are documentation only, the auth advice is correct, and the new page's claims align with the codebase invariants in CLAUDE.md.

The auth.mdx additions are accurate and directly address a real confused-deputy risk. The coming-from-novu page is accurate but leaves two of its five divergence table rows without a corresponding "in practice" entry, which may leave migrators without actionable guidance on those points. The frontmatter description also embeds JSX-like syntax that will appear as raw angle-bracket text in meta tags.

docs/content/docs/coming-from-novu.mdx — the "What this means in practice" section and the frontmatter description both warrant a quick look before merging.

Important Files Changed

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
Loading
%%{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
Loading

Fix All in Claude Code

Reviews (1): Last reviewed commit: "docs(auth): reword endpoint example comm..." | Re-trigger Greptile

Comment on lines +25 to +41
## 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 "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.

Fix in Claude Code

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/content/docs/coming-from-novu.mdx Outdated
@@ -0,0 +1,41 @@
---
title: Coming from Novu
description: What Chimely's <Inbox /> covers, and where it deliberately diverges.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Suggested change
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!

Fix in Claude Code

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@ravidsrk

Copy link
Copy Markdown
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant