Skip to content

feat: add redis stream support for realtime messaging - #26

Closed
Rieranthony wants to merge 2 commits into
mainfrom
codex/adapt-upstash/realtime-for-our-codebase
Closed

Rieranthony wants to merge 2 commits into
mainfrom
codex/adapt-upstash/realtime-for-our-codebase

Conversation

@Rieranthony

@Rieranthony Rieranthony commented Oct 9, 2025 •

Copy link
Copy Markdown
Contributor

Summary

  • replace the realtime stream implementation to use ioredis-based xadd/xrange handling and expose helper hooks for visitors and dashboards
  • add API streaming routes with authentication that proxy message-created events from Redis streams to visitors and dashboard clients
  • wire the web dashboard and support widget to consume the new SSE hooks while still running websocket handlers for other events

Testing

  • bunx turbo run lint --filter=@cossistant/realtime --filter=@cossistant/api --filter=@cossistant/web

https://chatgpt.com/codex/tasks/task_e_68e7952b9b28832b9d6f041c8db8c71a

Summary by CodeRabbit

  • New Features

    • Introduces real-time streams for visitors and dashboards, delivering live message updates.
    • Adds React hooks to easily subscribe to visitor and dashboard message events.
    • Client integration supports custom realtime endpoints and query parameters.
  • Improvements

    • Better authentication, filtering, and response headers for realtime endpoints.
    • More robust streaming with error handling, reconnection, and missed-message replay.
  • Chores

    • Migrated Redis client for improved performance and reliability.
    • Updated build tooling; no user-facing changes.
  • Style

    • Minor formatting cleanup.

@vercel

vercel Bot commented Oct 9, 2025 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
web Error Error Oct 9, 2025 0:27am

@coderabbitai

coderabbitai Bot commented Oct 9, 2025 •

Copy link
Copy Markdown
Contributor

Walkthrough

Introduces Redis/ioredis–backed realtime streaming with new server handlers and client hooks. Adds API routes for visitor and dashboard SSE streams, forwards MESSAGE_CREATED events into both channels, and updates React providers to consume via new hooks. Refactors server handler to support filtering, middleware, and response headers; updates build config and dependencies.

Changes

Cohort / File(s) Summary
API entrypoint wiring
apps/api/src/index.ts
Imports and invokes registerRealtimeRoutes(app) to register realtime endpoints during API initialization.
API realtime modules
apps/api/src/realtime/routes.ts, apps/api/src/realtime/emitter.ts, apps/api/src/realtime/streams.ts, apps/api/src/realtime/stream-dispatcher.ts
Adds SSE handlers for visitor and dashboard channels; defines Redis-backed realtimeStreams and shared MESSAGE_CREATED schema; forwards MESSAGE_CREATED events to both channels; integrates forwarding in emitter with non-blocking error handling.
Client realtime hooks (library)
packages/realtime/src/client/use-realtime.ts, packages/realtime/src/client/use-visitor-realtime.ts, packages/realtime/src/client/use-dashboard-realtime.ts, packages/realtime/src/client/index.ts
Extends useRealtime with endpoint and params; adds visitor/dashboard hooks that expose onMessageCreated and channel/param wiring; re-exports new hooks.
App providers (web and react pkg)
apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsx, packages/react/src/realtime/support-provider.tsx
Replaces inline MESSAGE_CREATED handling with useDashboardRealtime/useVisitorRealtime callbacks; routes payloads to existing handlers and context.
Realtime server internals
packages/realtime/src/server/handler.ts, packages/realtime/src/server/realtime.ts
Refactors SSE handler to support middleware, message filtering, response headers, and robust Redis subscription/replay; switches to ioredis stream/publish with JSON payloads and XADD MAXLEN.
Build and dependencies
packages/realtime/package.json, packages/realtime/tsdown.config.ts, apps/web/package.json
Swaps devDependency to ioredis; moves build to tsdown with broader entries and sourcemaps; adds newline in web package.json.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant API as API Server
  participant Emitter as Event Emitter
  participant Dispatcher as Stream Dispatcher
  participant RT as realtimeStreams (Redis)
  participant Redis as Redis (Streams+Pub/Sub)
  participant Handler as SSE Handler
  participant ClientV as Visitor Client
  participant ClientD as Dashboard Client

  rect rgb(245,248,255)
  note over API: Startup
  API->>API: registerRealtimeRoutes(app)
  API-->>User: Endpoints ready (/v1/realtime/visitor, /v1/realtime/dashboard)
  end

  rect rgb(245,255,245)
  note over Emitter: On MESSAGE_CREATED
  Emitter->>Dispatcher: forwardMessageEventToRealtimeStreams(event)
  alt type == MESSAGE_CREATED
    Dispatcher->>RT: emit visitor.MESSAGE_CREATED
    Dispatcher->>RT: emit dashboard.MESSAGE_CREATED
    RT->>Redis: XADD (trim) + PUBLISH JSON
  end
  end

  rect rgb(255,248,245)
  note over Handler: SSE subscriptions
  ClientV->>Handler: GET /v1/realtime/visitor?websiteId... (SSE)
  Handler->>Redis: duplicate, subscribe, replay
  ClientD->>Handler: GET /v1/realtime/dashboard?websiteId... (SSE)
  Handler->>Redis: duplicate, subscribe, replay
  Redis-->>Handler: message payloads
  Handler->>Handler: filter by channel/path
  Handler-->>ClientV: event: MESSAGE_CREATED (visitor)
  Handler-->>ClientD: event: MESSAGE_CREATED (dashboard)
  end
Loading
sequenceDiagram
  autonumber
  participant Hook as useVisitorRealtime/useDashboardRealtime
  participant URL as URL Builder
  participant SSE as EventSource
  participant App as App Handlers

  Hook->>URL: Build endpoint + params (channel, visitorId/publicKey)
  URL-->>Hook: Validated URL or error
  alt valid
    Hook->>SSE: new EventSource(url)
    SSE-->>Hook: open
    SSE-->>Hook: message (MESSAGE_CREATED)
    Hook->>App: onMessageCreated(event)
  else invalid
    Hook-->>App: status=error, abort
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

A rabbit taps the Redis drum,
Streams hum-softly: here they come!
Visitor, dashboard, messages glide,
Hooks hold paws on either side.
Routes now whisper, clients cheer,
SSE breezes, loud and clear.
Carrots queued, events appear. 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.69% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title “feat: add redis stream support for realtime messaging” succinctly captures the primary enhancement introduced by the pull request, namely integrating Redis streams for real-time messaging, and aligns with the feature-focused nature of the changeset without unnecessary detail.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/adapt-upstash/realtime-for-our-codebase

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (7)
apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsx (1)

87-89: Remove unnecessary Array.isArray check.

handleMessageCreated is always a single function (from createMessageCreatedHandler), not an array. The defensive Array.isArray normalization adds complexity without benefit.

Apply this diff to simplify:

-			const handlers = Array.isArray(handleMessageCreated)
-				? handleMessageCreated
-				: [handleMessageCreated];
-
-			for (const handler of handlers) {
-				handler(event.payload, { event, context: realtimeContext });
-			}
+			handleMessageCreated(event.payload, { event, context: realtimeContext });
apps/api/src/realtime/streams.ts (1)

6-13: Consider deriving schema from RealtimeEvent type.

The messageCreatedEventSchema manually duplicates the structure of RealtimeEvent<"MESSAGE_CREATED">. If the event type evolves, this schema must be updated separately.

For tighter coupling and reduced maintenance, consider using a type-to-schema utility or extracting a shared schema definition that both the type and validation use.

apps/api/src/realtime/routes.ts (1)

14-30: Handle origin parsing errors gracefully.

The try-catch in parseOriginHeaders silently swallows URL parsing errors. While this prevents crashes, it may hide malformed origin headers.

Consider logging the error when parsing fails:

 	} catch {
+		console.warn("[Realtime] Failed to parse origin header", origin);
 		return { origin };
 	}
packages/realtime/src/client/use-dashboard-realtime.ts (2)

13-18: Add JSDoc documentation for the public options interface.

The UseDashboardRealtimeOptions interface is part of the public API and should include JSDoc comments documenting each field, especially the nullability semantics and the purpose of onMessageCreated.

As per coding guidelines, add documentation like:

+/**
+ * Options for the useDashboardRealtime hook.
+ * 
+ * @property websiteId - The website ID to subscribe to dashboard events for. Required for connection.
+ * @property endpoint - The realtime endpoint URL. Defaults to "/v1/realtime/dashboard".
+ * @property enabled - Whether the realtime connection should be active. Defaults to true.
+ * @property onMessageCreated - Callback invoked when a MESSAGE_CREATED event is received.
+ */
 export type UseDashboardRealtimeOptions = {
   websiteId: string | null | undefined;
   endpoint?: string;
   enabled?: boolean;
   onMessageCreated?: (event: DashboardRealtimeEvent) => void;
 };

20-63: Add JSDoc documentation for the public hook.

The useDashboardRealtime hook is a public API and should include comprehensive JSDoc documenting its purpose, parameters, return value, and example usage.

As per coding guidelines, add documentation like:

+/**
+ * React hook for subscribing to dashboard realtime events via Server-Sent Events (SSE).
+ * 
+ * Establishes a realtime connection when enabled and required parameters are provided.
+ * Uses a ref-based handler to avoid stale closures when the callback changes.
+ * 
+ * @param options - Configuration options for the realtime connection.
+ * @returns An object containing the connection status.
+ * 
+ * @example
+ * ```tsx
+ * const { status } = useDashboardRealtime({
+ *   websiteId: "ws_123",
+ *   onMessageCreated: (event) => {
+ *     console.log("New message:", event.data);
+ *   },
+ * });
+ * ```
+ */
 export function useDashboardRealtime({
   websiteId,
   endpoint = "/v1/realtime/dashboard",
   enabled = true,
   onMessageCreated,
 }: UseDashboardRealtimeOptions) {
packages/realtime/src/client/use-visitor-realtime.ts (2)

13-20: Add JSDoc documentation for the public options interface.

Similar to UseDashboardRealtimeOptions, this public interface should include JSDoc comments documenting each field and the authentication requirements.

As per coding guidelines:

+/**
+ * Options for the useVisitorRealtime hook.
+ * 
+ * @property websiteId - The website ID to subscribe to visitor events for. Required for connection.
+ * @property visitorId - The visitor ID for authentication. Required for connection.
+ * @property publicKey - The public key for authentication. Required for connection.
+ * @property endpoint - The realtime endpoint URL. Defaults to "/v1/realtime/visitor".
+ * @property enabled - Whether the realtime connection should be active. Defaults to true.
+ * @property onMessageCreated - Callback invoked when a MESSAGE_CREATED event is received.
+ */
 export type UseVisitorRealtimeOptions = {
   websiteId: string | null | undefined;
   visitorId: string | null | undefined;
   publicKey: string | null | undefined;
   endpoint?: string;
   enabled?: boolean;
   onMessageCreated?: (event: VisitorRealtimeEvent) => void;
 };

22-68: Add JSDoc and consider extracting shared hook logic.

The implementation is correct but duplicates significant logic from useDashboardRealtime. Consider extracting the shared pattern into a generic factory function or base hook to reduce duplication.

1. Add JSDoc documentation:

+/**
+ * React hook for subscribing to visitor realtime events via Server-Sent Events (SSE).
+ * 
+ * Establishes a realtime connection when enabled and all required authentication parameters are provided.
+ * Uses a ref-based handler to avoid stale closures when the callback changes.
+ * 
+ * @param options - Configuration options including authentication credentials.
+ * @returns An object containing the connection status.
+ * 
+ * @example
+ * ```tsx
+ * const { status } = useVisitorRealtime({
+ *   websiteId: "ws_123",
+ *   visitorId: "vis_456",
+ *   publicKey: "pk_789",
+ *   onMessageCreated: (event) => {
+ *     console.log("New message:", event.data);
+ *   },
+ * });
+ * ```
+ */
 export function useVisitorRealtime({

2. Reduce code duplication:

Both useDashboardRealtime and useVisitorRealtime share the same implementation pattern (handlerRef, useEffect, shouldEnable, events useMemo, useRealtime delegation). Consider extracting this into a generic helper to improve maintainability:

function createRealtimeHook<TEvent, TSchema>(config: {
  eventNamespace: string;
  defaultEndpoint: string;
  buildParams: (options: any) => Record<string, any>;
  shouldEnableCheck: (options: any) => boolean;
}) {
  // shared implementation
}

This would eliminate duplication while keeping the public API surfaces distinct for dashboard and visitor use cases.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8bd8040 and ac804c5.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (15)
  • apps/api/src/index.ts (2 hunks)
  • apps/api/src/realtime/emitter.ts (3 hunks)
  • apps/api/src/realtime/routes.ts (1 hunks)
  • apps/api/src/realtime/stream-dispatcher.ts (1 hunks)
  • apps/api/src/realtime/streams.ts (1 hunks)
  • apps/web/package.json (1 hunks)
  • apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsx (2 hunks)
  • packages/react/src/realtime/support-provider.tsx (4 hunks)
  • packages/realtime/package.json (1 hunks)
  • packages/realtime/src/client/index.ts (1 hunks)
  • packages/realtime/src/client/use-dashboard-realtime.ts (1 hunks)
  • packages/realtime/src/client/use-realtime.ts (3 hunks)
  • packages/realtime/src/client/use-visitor-realtime.ts (1 hunks)
  • packages/realtime/src/server/handler.ts (7 hunks)
  • packages/realtime/src/server/realtime.ts (3 hunks)
🧰 Additional context used
📓 Path-based instructions (15)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/global-rules.mdc)

**/*.{ts,tsx}: Manage shared component state via React Context
Implement full TypeScript support for all public APIs
Provide proper type definitions for all props and refs
Use appropriate generic constraints where necessary
Export type definitions for the public API
Avoid TypeScript enums; prefer const objects with 'as const' or Object.freeze
Provide clear, descriptive runtime errors for missing context/props and misuse
Expose dedicated hooks for context access that throw helpful errors when context is missing
Add explicit error messages for: outside-context usage, missing required props, invalid prop combinations, and validation failures
Document each component with JSDoc: purpose, props (required/optional), context requirements, example usage, pitfalls
State management: use React Context for global state, perform correct state updates, and handle side effects appropriately
Validation: implement runtime checks and validate prop combinations with clear errors
Create context with createContext<ContextType|null>(null) and a useContext hook that throws a clear error when missing

**/*.{ts,tsx}: Don't use primitive type aliases or misleading types
Don't use empty type parameters in type aliases and interfaces
Don't use any or unknown as type constraints
Don't have unused function parameters
Don't have unused private class members
Don't return a value from a function that has a 'void' return type
Don't use the TypeScript directive @ts-ignore
Ensure switch-case statements are exhaustive (especially with unions)
Don't use TypeScript enums
Don't add type annotations to variables, parameters, and class properties initialized with literal expressions
Don't use TypeScript namespaces
Don't use non-null assertions with the ! postfix operator
Don't use parameter properties in class constructors
Don't use user-defined types (where discouraged by policy)
Use as const instead of literal types and type annotations
Use either T[] or Array consistently
Use consistent accessibili...

Files:

  • apps/api/src/realtime/routes.ts
  • packages/realtime/src/client/index.ts
  • apps/api/src/realtime/emitter.ts
  • apps/api/src/realtime/stream-dispatcher.ts
  • packages/realtime/src/client/use-visitor-realtime.ts
  • apps/api/src/realtime/streams.ts
  • packages/realtime/src/client/use-dashboard-realtime.ts
  • apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsx
  • packages/realtime/src/server/handler.ts
  • packages/realtime/src/client/use-realtime.ts
  • packages/realtime/src/server/realtime.ts
  • apps/api/src/index.ts
  • packages/react/src/realtime/support-provider.tsx
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

**/*.{js,jsx,ts,tsx}: Don't use the accessKey attribute on any HTML element
Use correct ISO language/country codes for the lang attribute
Don't hardcode sensitive data like API keys and tokens
Use === and !==

Files:

  • apps/api/src/realtime/routes.ts
  • packages/realtime/src/client/index.ts
  • apps/api/src/realtime/emitter.ts
  • apps/api/src/realtime/stream-dispatcher.ts
  • packages/realtime/src/client/use-visitor-realtime.ts
  • apps/api/src/realtime/streams.ts
  • packages/realtime/src/client/use-dashboard-realtime.ts
  • apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsx
  • packages/realtime/src/server/handler.ts
  • packages/realtime/src/client/use-realtime.ts
  • packages/realtime/src/server/realtime.ts
  • apps/api/src/index.ts
  • packages/react/src/realtime/support-provider.tsx
**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

**/*.{js,ts}: Don't use consecutive spaces in regular expression literals
Don't use the arguments object
Don't use the comma operator
Don't use unnecessary boolean casts
Don't use unnecessary callbacks with flatMap
Prefer for...of statements instead of Array.forEach
Don't use this and super in static contexts
Don't use unnecessary catch clauses
Don't use unnecessary constructors
Don't use unnecessary continue statements
Don't export empty modules that don't change anything
Don't use unnecessary escape sequences in regular expression literals
Don't use unnecessary labels
Don't use unnecessary nested block statements
Don't rename imports, exports, and destructured assignments to the same name
Don't use unnecessary string or template literal concatenation
Don't use String.raw in template literals when there are no escape sequences
Don't use useless case statements in switch statements
Avoid ternary operators when simpler alternatives exist
Don't use useless this aliasing
Don't initialize variables to undefined
Don't use void operators
Use Date.now() to get milliseconds since the Unix Epoch
Prefer .flatMap() over map().flat() when possible
Use literal property access instead of computed property access
Don't use parseInt() or Number.parseInt() when numeric literals suffice (binary/octal/hex)
Use concise optional chaining instead of chained logical expressions
Prefer regular expression literals over the RegExp constructor when possible
Don't use non–base-10 or underscore-separated number literal object member names
Remove redundant terms from logical expressions
Prefer while loops over for loops when no initializer/update is needed
Don't reassign const variables
Don't use constant expressions in conditions
Don't use Math.min/Math.max to clamp values when result is constant
Don't use empty character classes in regular expression literals
Don't use empty destructuring patterns
Don't call global object properties as functions
Don't declare functions and vars that are acce...

Files:

  • apps/api/src/realtime/routes.ts
  • packages/realtime/src/client/index.ts
  • apps/api/src/realtime/emitter.ts
  • apps/api/src/realtime/stream-dispatcher.ts
  • packages/realtime/src/client/use-visitor-realtime.ts
  • apps/api/src/realtime/streams.ts
  • packages/realtime/src/client/use-dashboard-realtime.ts
  • packages/realtime/src/server/handler.ts
  • packages/realtime/src/client/use-realtime.ts
  • packages/realtime/src/server/realtime.ts
  • apps/api/src/index.ts
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

**/*.{js,ts,tsx}: Don't create classes that only have static members
Don't return a value from a constructor
Don't use super() incorrectly inside classes; ensure super() is called in derived constructors
Ensure super() is called exactly once on every code path in a class constructor before this is accessed if the class has a superclass
Place getters and setters for the same property next to each other in class and object definitions
Ensure JSDoc comment lines start with a single asterisk, except for the first one
Don't use duplicate class members

Files:

  • apps/api/src/realtime/routes.ts
  • packages/realtime/src/client/index.ts
  • apps/api/src/realtime/emitter.ts
  • apps/api/src/realtime/stream-dispatcher.ts
  • packages/realtime/src/client/use-visitor-realtime.ts
  • apps/api/src/realtime/streams.ts
  • packages/realtime/src/client/use-dashboard-realtime.ts
  • apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsx
  • packages/realtime/src/server/handler.ts
  • packages/realtime/src/client/use-realtime.ts
  • packages/realtime/src/server/realtime.ts
  • apps/api/src/index.ts
  • packages/react/src/realtime/support-provider.tsx
**/*.{js,tsx,ts,jsx}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

Use arrow functions instead of function expressions

Files:

  • apps/api/src/realtime/routes.ts
  • packages/realtime/src/client/index.ts
  • apps/api/src/realtime/emitter.ts
  • apps/api/src/realtime/stream-dispatcher.ts
  • packages/realtime/src/client/use-visitor-realtime.ts
  • apps/api/src/realtime/streams.ts
  • packages/realtime/src/client/use-dashboard-realtime.ts
  • apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsx
  • packages/realtime/src/server/handler.ts
  • packages/realtime/src/client/use-realtime.ts
  • packages/realtime/src/server/realtime.ts
  • apps/api/src/index.ts
  • packages/react/src/realtime/support-provider.tsx
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

**/*.{js,ts,jsx,tsx}: Prevent duplicate polyfills from Polyfill.io
Don't use irregular whitespace characters

Files:

  • apps/api/src/realtime/routes.ts
  • packages/realtime/src/client/index.ts
  • apps/api/src/realtime/emitter.ts
  • apps/api/src/realtime/stream-dispatcher.ts
  • packages/realtime/src/client/use-visitor-realtime.ts
  • apps/api/src/realtime/streams.ts
  • packages/realtime/src/client/use-dashboard-realtime.ts
  • apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsx
  • packages/realtime/src/server/handler.ts
  • packages/realtime/src/client/use-realtime.ts
  • packages/realtime/src/server/realtime.ts
  • apps/api/src/index.ts
  • packages/react/src/realtime/support-provider.tsx
**/*.{js,ts,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

Don't redeclare variables, functions, classes, and types in the same scope

Files:

  • apps/api/src/realtime/routes.ts
  • packages/realtime/src/client/index.ts
  • apps/api/src/realtime/emitter.ts
  • apps/api/src/realtime/stream-dispatcher.ts
  • packages/realtime/src/client/use-visitor-realtime.ts
  • apps/api/src/realtime/streams.ts
  • packages/realtime/src/client/use-dashboard-realtime.ts
  • apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsx
  • packages/realtime/src/server/handler.ts
  • packages/realtime/src/client/use-realtime.ts
  • packages/realtime/src/server/realtime.ts
  • apps/api/src/index.ts
  • packages/react/src/realtime/support-provider.tsx
packages/**

📄 CodeRabbit inference engine (AGENTS.md)

Put shared contracts and utilities in packages/* (e.g., core, types, transactional)

Files:

  • packages/realtime/src/client/index.ts
  • packages/realtime/src/client/use-visitor-realtime.ts
  • packages/realtime/package.json
  • packages/realtime/src/client/use-dashboard-realtime.ts
  • packages/realtime/src/server/handler.ts
  • packages/realtime/src/client/use-realtime.ts
  • packages/realtime/src/server/realtime.ts
  • packages/react/src/realtime/support-provider.tsx
**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/global-rules.mdc)

**/*.tsx: Use the Compound Components pattern with namespaced exports (e.g., FormWizard.Root, FormWizard.Step) so pieces compose under a single parent
Ensure accessibility: keyboard navigation support
Ensure accessibility: include appropriate ARIA attributes
Ensure accessibility: use semantic markup for screen readers
Maintain proper focus management in interactive components
Structure components with forwardRef and typed props/refs as shown (ComponentRef, HTMLAttributes, ReactNode)
Root component acts as main context provider, manages global state, and handles initialization
Child components must be used within Root, read context via hooks, support asChild, and follow consistent props
Props ordering: list required props first; optional props have clear defaults; support common HTML attributes; expose className
Support composition with an asChild prop using Radix UI Slot to allow element replacement
Styling: support className, use data-* attributes for state, and allow style customization
Event handling: support standard DOM events, add custom events when needed, and manage propagation correctly
Base component pattern: forwardRef, choose Slot or intrinsic element via asChild, pass ref/className and spread props

Files:

  • apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsx
  • packages/react/src/realtime/support-provider.tsx
**/*.{jsx,tsx}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

**/*.{jsx,tsx}: Don't set aria-hidden="true" on focusable elements
Don't add ARIA roles, states, and properties to elements that don't support them
Don't use distracting elements like or
Only use the scope prop on elements
Don't assign non-interactive ARIA roles to interactive HTML elements
Ensure label elements have text content and are associated with an input
Don't assign interactive ARIA roles to non-interactive HTML elements
Don't assign tabIndex to non-interactive HTML elements
Don't use positive integers for tabIndex property
Don't include the words "image", "picture", or "photo" in img alt prop
Don't use an explicit role that's the same as the element's implicit role
Static elements with click handlers must have a valid role attribute
Always include a <title> element within SVG elements
Provide meaningful alt text for elements that require alt text
Ensure anchors have content accessible to screen readers
Assign tabIndex to non-interactive elements only when using aria-activedescendant
Include all required ARIA attributes for elements with ARIA roles
Ensure ARIA properties are valid for the element's supported roles
Always include a type attribute for button elements
Elements with interactive roles and handlers must be focusable
Heading elements must have content accessible to screen readers (not hidden with aria-hidden)
Always include a lang attribute on the html element
Always include a title attribute for iframe elements
Accompany onClick with at least one of: onKeyUp, onKeyDown, or onKeyPress
Accompany onMouseOver/onMouseOut with onFocus/onBlur
Include caption tracks for audio and video elements
Prefer semantic elements instead of role attributes in JSX
Ensure all anchors are valid and navigable
Ensure all aria-* properties are valid
Use valid, non-abstract ARIA roles for elements with ARIA roles
Use valid ARIA state and property values
Use valid values for the autocomplete attribute on input elements
Don't use unnecessary fragmen...

Files:

  • apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsx
  • packages/react/src/realtime/support-provider.tsx
**/*.{jsx,tsx,html}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

**/*.{jsx,tsx,html}: Ensure the preconnect attribute is used when using Google Fonts
Don't use target="_blank" without rel="noopener"
Use a recommended display strategy with Google Fonts

Files:

  • apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsx
  • packages/react/src/realtime/support-provider.tsx
apps/web/src/app/**

📄 CodeRabbit inference engine (AGENTS.md)

Place Next.js App Router pages/layouts/routes under apps/web/src/app

Files:

  • apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsx
apps/web/src/{app,components}/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

Use snake_case filenames for React components and layouts in the web app

Files:

  • apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsx
packages/react/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Develop headless React components (including and primitives) under packages/react/src/

Files:

  • packages/react/src/realtime/support-provider.tsx
packages/react/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/react-package-rules.mdc)

packages/react/**/*.{ts,tsx}: Components must render with no built-in styles (headless, unstyled)
Components must accept className, style, and asChild props to enable external styling/control
Do not use emotion, styled-components, or style-prop APIs; styling is via Tailwind/CSS through className/style
Use useRenderElement to create elements and encapsulate render logic (supporting intrinsic tags and render prop overrides)
Components with internal state must be controllable via props (e.g., value/defaultValue/onChange or isOpen/onOpenChange)
Expose internal state via a hook (e.g., useInput, useWindow) or a render prop function child
May reference Base UI logic internally, but never expose Base UI or MUI APIs; do not leak getRootProps/getInputProps; prefer useRenderElement
Prefer small, composable primitives (e.g., Provider, Bubble, Window, MessageList, Message, Input, SendButton, Typing, Event) over monolithic components
Provide a render prop API: render?: (props, state) => ReactElement, and allow className to be string or (state) => string
Support developer-owned rendering: components should not impose visual structure and should allow parent-driven markup via render prop/asChild

Files:

  • packages/react/src/realtime/support-provider.tsx
🧠 Learnings (1)
📚 Learning: 2025-09-17T09:00:28.751Z
Learnt from: CR
PR: cossistantcom/cossistant#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-17T09:00:28.751Z
Learning: Use Bun as the package manager and executor across the monorepo (install, dev, build, lint, format, type-check)

Applied to files:

  • apps/web/package.json
🧬 Code graph analysis (10)
apps/api/src/realtime/routes.ts (7)
packages/realtime/src/server/handler.ts (1)
  • handle (34-279)
apps/api/src/realtime/streams.ts (1)
  • realtimeStreams (17-28)
apps/api/src/lib/auth-validation.ts (2)
  • performAuthentication (366-415)
  • AuthValidationError (25-33)
apps/api/src/db/index.ts (1)
  • db (34-39)
apps/api/src/db/queries/visitor.ts (1)
  • findVisitorForWebsite (141-160)
apps/api/src/db/queries/session.ts (1)
  • getTRPCSession (104-111)
apps/api/src/db/queries/website.ts (1)
  • getWebsiteByIdWithAccess (247-302)
apps/api/src/realtime/emitter.ts (4)
packages/types/src/realtime-events.ts (1)
  • RealtimeEvent (108-115)
apps/api/src/ws/socket.ts (2)
  • sendEventToVisitor (85-96)
  • sendEventToWebsite (98-109)
apps/api/src/realtime/stream-dispatcher.ts (1)
  • forwardMessageEventToRealtimeStreams (28-39)
apps/api/src/ws/router.ts (1)
  • routeEvent (254-280)
apps/api/src/realtime/stream-dispatcher.ts (1)
apps/api/src/realtime/streams.ts (1)
  • realtimeStreams (17-28)
packages/realtime/src/client/use-visitor-realtime.ts (1)
packages/realtime/src/client/use-realtime.ts (1)
  • useRealtime (26-240)
apps/api/src/realtime/streams.ts (3)
packages/types/src/realtime-events.ts (1)
  • RealtimeEvents (9-100)
apps/api/src/redis.ts (1)
  • getRedis (7-39)
packages/realtime/src/server/realtime.ts (2)
  • Realtime (121-129)
  • Realtime (138-140)
packages/realtime/src/client/use-dashboard-realtime.ts (1)
packages/realtime/src/client/use-realtime.ts (1)
  • useRealtime (26-240)
apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsx (3)
packages/realtime/src/client/use-dashboard-realtime.ts (1)
  • useDashboardRealtime (20-63)
packages/types/src/realtime-events.ts (1)
  • RealtimeEvent (108-115)
apps/web/src/app/(dashboard)/[websiteSlug]/providers/events/handlers/message-created.ts (1)
  • handleMessageCreated (73-132)
packages/realtime/src/server/handler.ts (2)
packages/realtime/src/types.ts (2)
  • UserEvent (8-12)
  • SystemEvent (1-6)
packages/realtime/src/server/realtime.ts (4)
  • Opts (11-16)
  • Realtime (121-129)
  • Realtime (138-140)
  • channel (105-110)
apps/api/src/index.ts (1)
apps/api/src/realtime/routes.ts (1)
  • registerRealtimeRoutes (142-152)
packages/react/src/realtime/support-provider.tsx (4)
packages/react/src/provider.tsx (1)
  • useSupport (215-241)
packages/types/src/realtime-events.ts (1)
  • RealtimeEvent (108-115)
packages/react/src/realtime/use-realtime.ts (1)
  • useRealtime (68-144)
packages/realtime/src/client/use-visitor-realtime.ts (1)
  • useVisitorRealtime (22-68)
🔇 Additional comments (19)
apps/web/package.json (1)

109-109: No functional change; LGTM.

End-of-file newline only. Nothing else to review here.

packages/realtime/src/client/use-realtime.ts (1)

91-127: LGTM! Robust URL construction with proper guards.

The refactored EventSource URL construction adds important safety measures:

  • Lines 91-93: Server-side guard prevents SSR-related crashes
  • Lines 95-102: Try-catch around URL construction handles malformed endpoints gracefully
  • Lines 104-107: Early exit on invalid URL with error state
  • Lines 109-126: Structured URL parameter building is more maintainable than string concatenation
  • Proper filtering of undefined/null param values

The new endpoint and params options expand the API surface while maintaining backward compatibility through default values.

packages/realtime/src/server/handler.ts (5)

271-277: Verify responseHeaders don't override critical SSE headers.

The responseHeaders function result is spread after the default SSE headers (Line 376), which means user-provided headers will override defaults like Content-Type, Cache-Control, and CORS headers. This could break SSE streaming if users accidentally override critical headers.

Consider reversing the spread order or documenting this behavior:

-       ...(init?.headers ?? {}),
+       ...(init?.headers ?? {}),

Should be:

        "Content-Type": "text/event-stream",
        "Cache-Control": "no-cache",
        Connection: "keep-alive",
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Headers": "Cache-Control",
-       ...(init?.headers ?? {}),
+       // User headers intentionally last to allow overrides
+       ...(init?.headers ?? {}),

Or protect critical headers:

+       const userHeaders = init?.headers ?? {};
+       const protectedKeys = ["Content-Type", "Cache-Control", "Connection"];
+       const filteredHeaders = Object.fromEntries(
+         Object.entries(userHeaders).filter(([k]) => !protectedKeys.includes(k))
+       );
        "Content-Type": "text/event-stream",
        "Cache-Control": "no-cache",
        Connection: "keep-alive",
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Headers": "Cache-Control",
-       ...(init?.headers ?? {}),
+       ...filteredHeaders,

Document whether overriding SSE-critical headers is intentional or should be prevented.


81-263: LGTM! Well-structured streaming handler with proper lifecycle management.

The refactored handler introduces excellent patterns:

  • Lines 81-105: safeEnqueue and sendUserEvent properly encapsulate event emission with filter integration and error handling
  • Lines 107-131: closeStream comprehensively cleans up all resources (subscriber, timeouts, event listeners)
  • Lines 163-196: replayMissedMessages handles both cold starts (sending last cursor) and reconnects (replaying gap) correctly
  • Lines 200-220: Proper error and unsubscribe event handling with system events
  • Lines 222-256: Message parsing with parsePublishedPayload distinguishes ping vs. user events safely
  • Lines 259-263: Keep-alive mechanism publishes pings to maintain connection

The middleware, filter, and responseHeaders hooks provide excellent extensibility points for authentication, authorization, and custom headers.


281-359: LGTM! Robust parsing helpers with proper type guards.

The parsing utilities are well-implemented:

  • Lines 281-314: parsePublishedPayload safely discriminates between ping and user event payloads with proper type narrowing
  • Lines 316-359: parseStreamEntry correctly parses Redis stream entry format [id, [k1, v1, k2, v2, ...]] and extracts the JSON payload field
  • Both functions return null on parse failures, enabling graceful degradation

The type definitions (PublishedPayload, StreamEntry, ParsedStreamEntry) clearly model the data structures.


141-143: Duplicate/connect/subscribe pattern is correct as implemented
The code explicitly awaits subscriber.connect() before calling subscribe(), matching ioredis documentation.


66-66: No change needed for subscriber type. ReturnType<typeof redis.duplicate> already resolves to the Redis type, so swapping to an explicit Redis import is unnecessary.

Likely an incorrect or invalid review comment.

packages/realtime/src/client/index.ts (1)

2-3: LGTM! Public API expanded with visitor and dashboard hooks.

The new exports align with the PR objective to expose specialized realtime hooks for visitors and dashboards alongside the generic use-realtime hook. This provides a cleaner API surface for consumers.

packages/realtime/src/server/realtime.ts (1)

69-97: Approve Redis streaming enhancements
The updated JSON payload serialization, xadd with MAXLEN ~100, and publishing with __stream_id align with ioredis API and best practices.

packages/realtime/package.json (1)

54-54: Ignore incorrect @types/react version warning The registry shows ^19.1.16 exists; no changes needed.

Likely an incorrect or invalid review comment.

apps/api/src/realtime/emitter.ts (1)

150-154: Confirm fail-open behavior is intentional.

The stream forwarding is wrapped in try-catch with error logging, but failures don't prevent the event from being routed via routeEvent. This means if Redis streams are temporarily unavailable, events still propagate through the WebSocket system.

Verify this fail-open approach aligns with your reliability requirements. If stream forwarding is critical, consider tracking failures (metrics/alerts) or implementing retry logic.

apps/api/src/realtime/routes.ts (1)

103-140: LGTM: Dashboard handler has proper authentication and authorization.

The dashboard stream handler correctly:

  • Validates channel is present (line 106-108)
  • Authenticates via TRPC session (line 110-114)
  • Authorizes website access using getWebsiteByIdWithAccess (line 116-123)
  • Filters events by dashboard namespace (line 125-127)
  • Includes CORS headers when origin is present (line 128-139)

This ensures only authenticated dashboard users with explicit website access can consume the dashboard stream.

apps/api/src/realtime/stream-dispatcher.ts (1)

7-9: Confirmed upstream websiteId validation. RealtimeEmitter.emit throws an error if websiteId is missing, so the early returns in stream-dispatcher are purely defensive.

packages/realtime/src/client/use-dashboard-realtime.ts (3)

1-11: LGTM! Clean type definitions.

The imports and type definitions are well-structured. The type alias DashboardRealtimeEvent provides clear semantic meaning even though it's structurally identical to VisitorRealtimeEvent in the companion file.


26-52: LGTM! Correct ref-based handler pattern.

The implementation correctly uses a ref to store the latest handler, preventing stale closures. The useMemo optimization and type safety with satisfies are both appropriate.


54-62: LGTM! Proper delegation to useRealtime.

The hook correctly delegates to useRealtime with appropriate parameters, properly handling null/undefined conversions and conditional enabling.

packages/realtime/src/client/use-visitor-realtime.ts (3)

1-11: LGTM! Consistent type definitions.

The imports and type definitions follow the same pattern as the dashboard hook, maintaining consistency across the realtime client API.


30-56: LGTM! Consistent implementation pattern.

The ref-based handler pattern and events construction correctly mirror the dashboard hook, ensuring consistent behavior across both realtime hook variants.


58-67: LGTM! Proper visitor-specific parameters.

The hook correctly passes visitor authentication parameters (visitorId and publicKey) to the underlying useRealtime implementation, with proper null-to-undefined conversion.

Comment on lines +96 to +99
const payloadVisitorId =
data.payload.message.visitorId ?? data.visitorId ?? null;

return !payloadVisitorId || payloadVisitorId === visitorId;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Verify visitor filter privacy: null visitorId events sent to all visitors.

The filter returns true when payloadVisitorId is null (line 99: !payloadVisitorId || ...), meaning MESSAGE_CREATED events without a visitorId in the payload are forwarded to all visitors on that website.

Confirm this is intentional. If MESSAGE_CREATED events should only reach the specific visitor who owns the conversation, change the logic to reject null visitorId:

Apply this diff to enforce strict visitor matching:

-		return !payloadVisitorId || payloadVisitorId === visitorId;
+		return payloadVisitorId === visitorId;

If null visitorId events are valid broadcast messages (e.g., system announcements), document this behavior clearly and verify the upstream event creator never accidentally omits visitorId for private conversations.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const payloadVisitorId =
data.payload.message.visitorId ?? data.visitorId ?? null;
return !payloadVisitorId || payloadVisitorId === visitorId;
const payloadVisitorId =
data.payload.message.visitorId ?? data.visitorId ?? null;
return payloadVisitorId === visitorId;
🤖 Prompt for AI Agents
In apps/api/src/realtime/routes.ts around lines 96 to 99, the current filter
treats a null payloadVisitorId as a match and will forward MESSAGE_CREATED
events with no visitorId to all visitors; if messages must only reach their
owning visitor, change the condition to require a non-null visitor id and exact
match (i.e., only return true when payloadVisitorId is present and equals
visitorId), update any related tests, and add a short comment or docs clarifying
that null payloadVisitorId is a broadcast only if intentionally allowed.

"@types/react": "^19.1.16",
"@types/react-dom": "^19.1.9",
"@upstash/redis": "^1.35.4",
"ioredis": "^5.4.2",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Move ioredis to dependencies or peerDependencies.

ioredis is listed as a devDependency but is imported and used at runtime in packages/realtime/src/server/realtime.ts (line 4) and packages/realtime/src/server/handler.ts. DevDependencies are not installed in production, which will cause runtime failures.

Apply this diff to move ioredis to dependencies:

  "devDependencies": {
    "zod": "^4.1.11",
    "@types/node": "^24.6.0",
    "@types/react": "^19.1.16",
    "@types/react-dom": "^19.1.9",
-   "ioredis": "^5.4.2",
    "typescript": "^5.9.2"
  },
- "dependencies": {},
+ "dependencies": {
+   "ioredis": "^5.4.2"
+ },
  "peerDependencies": {

Alternatively, if you want consuming applications to provide their own Redis client:

  "devDependencies": {
    "zod": "^4.1.11",
    "@types/node": "^24.6.0",
    "@types/react": "^19.1.16",
    "@types/react-dom": "^19.1.9",
-   "ioredis": "^5.4.2",
    "typescript": "^5.9.2"
  },
  "dependencies": {},
  "peerDependencies": {
    "zod": "^3.25.0 || ^4.0.0",
+   "ioredis": "^5.0.0",
    "react": ">=16.8.0",
    "react-dom": ">=16.8.0"
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"ioredis": "^5.4.2",
"devDependencies": {
"zod": "^4.1.11",
"@types/node": "^24.6.0",
"@types/react": "^19.1.16",
"@types/react-dom": "^19.1.9",
"typescript": "^5.9.2"
},
"dependencies": {
"ioredis": "^5.4.2"
},
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0",
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
🤖 Prompt for AI Agents
In packages/realtime/package.json around line 56, ioredis is currently listed as
a devDependency but is required at runtime by
packages/realtime/src/server/realtime.ts and handler.ts; update package.json to
move ioredis from devDependencies into either "dependencies" (so it is installed
for production) or into "peerDependencies" (if you want consumers to supply
their own Redis client), and remove it from devDependencies; after updating, run
npm/yarn install and rebuild to ensure the runtime dependency is available.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/realtime/src/client/use-realtime.ts (1)

225-239: Reconnect when endpoint/params change

Currently only channel/enabled trigger reconnect; params/endpoint updates won’t. This can leave a stale connection.

Apply this diff:

-  }, [channel, enabled]);
+  }, [channel, enabled, endpoint, JSON.stringify(params)]);
🧹 Nitpick comments (12)
packages/realtime/src/server/realtime.ts (1)

80-88: Verify MAXLEN constraint is sufficient for your traffic patterns.

The MAXLEN approximate trimming (~) set to 100 entries may be too small for high-traffic channels. This limits the stream to roughly 100 recent events, which could result in event loss if consumers fall behind or reconnect after a delay.

Consider:

  • Calculating expected message rate per channel and consumer reconnect latency to determine an appropriate MAXLEN value.
  • If 100 entries is insufficient, increase the limit or implement a time-based expiration strategy (e.g., MAXLEN with a higher cap + TTL on the stream key).

Do you want me to generate a configuration option to make MAXLEN tunable per channel or deployment environment?

packages/realtime/tsdown.config.ts (2)

4-4: Broad entry pattern may include unintended files.

The entry pattern src/**/*.{ts,tsx} will match all TypeScript files under src/, including potential test files, fixtures, or internal utilities not intended for distribution. This could bloat the build output and expose internal implementation details.

Consider explicitly listing entry points or using a more restrictive pattern:

-  entry: ["src/**/*.{ts,tsx}"],
+  entry: [
+    "src/client/index.ts",
+    "src/server/index.ts",
+    "src/client/use-realtime.ts",
+    "src/client/use-visitor-realtime.ts",
+    "src/client/use-dashboard-realtime.ts",
+  ],

Or exclude test/internal files:

entry: ["src/**/*.{ts,tsx}", "!src/**/*.test.{ts,tsx}", "!src/**/__tests__/**"],

10-35: Uncomment and configure external dependencies.

The commented-out external configuration suggests uncertainty about bundling strategy. For a library package, you should externalize peer dependencies and regular dependencies to avoid bundle bloat and version conflicts.

Apply this diff to externalize dependencies:

-  // external: [
-  //   // React ecosystem (peer dependencies)
-  //   "react",
-  //   "react-dom",
-  //   "react/jsx-runtime",
-
-  //   // Peer dependencies
-  //   "@tanstack/react-query",
-  //   "motion",
-  //   "motion/*",
-  //   "tailwindcss",
-
-  //   // Internal workspace packages that should be bundled
-  //   // Remove "@cossistant/*" to bundle core and types
-  //   "@cossistant/core",
-  //   "@cossistant/types",
-
-  //   // Regular dependencies that should stay external
-  //   "react-use-websocket",
-  //   "react-markdown",
-  //   "zustand",
-  //   "zustand/middleware",
-  //   "nanoid",
-  //   "ulid",
-  //   "tailwind-merge",
-  // ],
+  external: [
+    // React ecosystem (peer dependencies)
+    "react",
+    "react-dom",
+    "react/jsx-runtime",
+
+    // Zod (peer dependency)
+    "zod",
+    /^zod\//,
+
+    // ioredis (server-side, should be external for consumers to provide)
+    "ioredis",
+  ],

This ensures:

  • Client-side code doesn't bundle React (consumers provide it)
  • Server-side code doesn't bundle ioredis (consumers provide it)
  • Reduces bundle size and prevents version conflicts
apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsx (1)

84-95: Clarify the dual-hook pattern rationale.

The component now uses two separate realtime hooks:

  1. useRealtime for CONVERSATION_CREATED, CONVERSATION_SEEN, CONVERSATION_TYPING (lines 72-82)
  2. useDashboardRealtime for MESSAGE_CREATED (lines 84-95)

This dual-hook pattern raises questions:

  1. Connection efficiency: Does this establish two separate connections (WebSocket + SSE)? If so, this could increase resource usage.
  2. Event consistency: Why handle MESSAGE_CREATED separately? Are there technical constraints (e.g., MESSAGE_CREATED uses SSE while others use WebSocket)?
  3. Handler normalization: Lines 87-89 normalize handleMessageCreated to an array, suggesting uncertainty about its type. This should be resolved at the type level.

Consider documenting why MESSAGE_CREATED requires a separate hook, or if possible, consolidate all events under a single realtime connection to reduce complexity.

Do you want me to investigate whether useDashboardRealtime and useRealtime can be unified, or if there's a technical reason (e.g., authentication scopes, transport protocols) requiring this split?

apps/api/src/realtime/emitter.ts (1)

150-154: Replace console.error with project logger

Avoid console usage in JS/TS. Route through a logger (e.g., shared logger util) with structured context.

apps/api/src/realtime/routes.ts (1)

73-75: Replace console.error with project logger

Use a centralized logger; avoid console in server code.

packages/realtime/src/client/use-realtime.ts (1)

70-73: Avoid console in library code; provide a logger or gate by env

Replace console.* with a pluggable logger (opts.logger?) or at least gate in non-production builds to reduce noise.

Also applies to: 81-83, 99-101, 158-159, 187-188, 199-200, 206-208

packages/realtime/src/client/use-dashboard-realtime.ts (2)

1-4: Add an explicit return type for the exported hook (per guidelines).

Annotate the hook’s return using a shared type from use-realtime to keep the public API stable.

Apply:

 import { useRealtime } from "./use-realtime";
+import type { UseRealtimeReturn } from "./use-realtime";
@@
-export function useDashboardRealtime({
+export function useDashboardRealtime({
   websiteId,
   endpoint = "/v1/realtime/dashboard",
   enabled = true,
   onMessageCreated,
-}: UseDashboardRealtimeOptions) {
+}: UseDashboardRealtimeOptions): UseRealtimeReturn {

Outside this file (in use-realtime.ts), export a compatible return type:

// in packages/realtime/src/client/use-realtime.ts
export type UseRealtimeReturn = { status: ConnectionStatus };

This keeps the hook’s surface explicit without leaking internals.

Also applies to: 20-26


5-18: Document the public API with JSDoc (types and hook).

Add concise JSDoc for purpose, params, and example usage.

-export type DashboardRealtimeEvent = RealtimeEvent<"MESSAGE_CREATED">;
+/** Dashboard stream payload for MESSAGE_CREATED events. */
+export type DashboardRealtimeEvent = RealtimeEvent<"MESSAGE_CREATED">;
@@
-export type UseDashboardRealtimeOptions = {
+/**
+ * Options for the dashboard realtime hook.
+ * - websiteId: channel identifier
+ * - endpoint: SSE endpoint (defaults to /v1/realtime/dashboard)
+ * - enabled: allow connecting
+ * - onMessageCreated: handler for MESSAGE_CREATED
+ */
+export type UseDashboardRealtimeOptions = {
@@
-export function useDashboardRealtime({
+/**
+ * Subscribe to dashboard realtime events for a website.
+ * Connects only if both websiteId and onMessageCreated are provided.
+ */
+export function useDashboardRealtime({

Also applies to: 20-25

packages/realtime/src/server/handler.ts (3)

177-195: Bound and chunk the XRANGE backfill to avoid large bursts.

Unbounded XRANGE can pull a large backlog, blocking the event loop and flooding the client. Iterate with COUNT and advance the start id.

Example:

-          const startId = `(${last_ack}`;
-          const missingMessages = await redis.xrange(streamKey, startId, "+");
-
-          sendConnectedEvent();
-
-          for (const entry of missingMessages) {
-            const parsed = parseStreamEntry(entry);
-            if (!parsed) {
-              continue;
-            }
-            const userEvent: UserEvent = {
-              data: parsed.data,
-              __event_path: parsed.__event_path,
-              __stream_id: parsed.id,
-            };
-            await sendUserEvent(userEvent);
-          }
+          let next = `(${last_ack}`;
+          sendConnectedEvent();
+          // Fetch in chunks to avoid long event loops on large backfills.
+          for (;;) {
+            const batch = await redis.xrange(streamKey, next, "+", "COUNT", 100);
+            if (!batch.length) break;
+            for (const entry of batch) {
+              const parsed = parseStreamEntry(entry);
+              if (!parsed) continue;
+              const userEvent: UserEvent = {
+                data: parsed.data,
+                __event_path: parsed.__event_path,
+                __stream_id: parsed.id,
+              };
+              await sendUserEvent(userEvent);
+            }
+            next = `(${batch[batch.length - 1][0]}`;
+          }

371-377: Revisit default CORS headers for credentialed SSE.

Wildcard ACAO conflicts with credentials and may be unsafe. Prefer delegating CORS via config.responseHeaders; at minimum add Vary: Origin and avoid setting "*" by default when auth is required.

Apply:

-        "Access-Control-Allow-Origin": "*",
-        "Access-Control-Allow-Headers": "Cache-Control",
+        // Prefer per-route CORS via config.responseHeaders; set minimal defaults here.
+        "Vary": "Origin",

If cross-origin with credentials is needed, set:

  • Access-Control-Allow-Origin:
  • Access-Control-Allow-Credentials: true

via responseHeaders({ request, channel }).


361-363: Reuse TextEncoder to reduce allocations.

Small but hot path optimization.

Apply:

+const encoder = new TextEncoder();
 function json<T>(data: SystemEvent | UserEvent<T>) {
-  return new TextEncoder().encode(`data: ${JSON.stringify(data)}\n\n`);
+  return encoder.encode(`data: ${JSON.stringify(data)}\n\n`);
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8bd8040 and 0ca3b5b.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (16)
  • apps/api/src/index.ts (2 hunks)
  • apps/api/src/realtime/emitter.ts (3 hunks)
  • apps/api/src/realtime/routes.ts (1 hunks)
  • apps/api/src/realtime/stream-dispatcher.ts (1 hunks)
  • apps/api/src/realtime/streams.ts (1 hunks)
  • apps/web/package.json (1 hunks)
  • apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsx (2 hunks)
  • packages/react/src/realtime/support-provider.tsx (4 hunks)
  • packages/realtime/package.json (1 hunks)
  • packages/realtime/src/client/index.ts (1 hunks)
  • packages/realtime/src/client/use-dashboard-realtime.ts (1 hunks)
  • packages/realtime/src/client/use-realtime.ts (3 hunks)
  • packages/realtime/src/client/use-visitor-realtime.ts (1 hunks)
  • packages/realtime/src/server/handler.ts (7 hunks)
  • packages/realtime/src/server/realtime.ts (3 hunks)
  • packages/realtime/tsdown.config.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (15)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/global-rules.mdc)

**/*.{ts,tsx}: Manage shared component state via React Context
Implement full TypeScript support for all public APIs
Provide proper type definitions for all props and refs
Use appropriate generic constraints where necessary
Export type definitions for the public API
Avoid TypeScript enums; prefer const objects with 'as const' or Object.freeze
Provide clear, descriptive runtime errors for missing context/props and misuse
Expose dedicated hooks for context access that throw helpful errors when context is missing
Add explicit error messages for: outside-context usage, missing required props, invalid prop combinations, and validation failures
Document each component with JSDoc: purpose, props (required/optional), context requirements, example usage, pitfalls
State management: use React Context for global state, perform correct state updates, and handle side effects appropriately
Validation: implement runtime checks and validate prop combinations with clear errors
Create context with createContext<ContextType|null>(null) and a useContext hook that throws a clear error when missing

**/*.{ts,tsx}: Don't use primitive type aliases or misleading types
Don't use empty type parameters in type aliases and interfaces
Don't use any or unknown as type constraints
Don't have unused function parameters
Don't have unused private class members
Don't return a value from a function that has a 'void' return type
Don't use the TypeScript directive @ts-ignore
Ensure switch-case statements are exhaustive (especially with unions)
Don't use TypeScript enums
Don't add type annotations to variables, parameters, and class properties initialized with literal expressions
Don't use TypeScript namespaces
Don't use non-null assertions with the ! postfix operator
Don't use parameter properties in class constructors
Don't use user-defined types (where discouraged by policy)
Use as const instead of literal types and type annotations
Use either T[] or Array consistently
Use consistent accessibili...

Files:

  • apps/api/src/realtime/stream-dispatcher.ts
  • packages/realtime/src/client/use-visitor-realtime.ts
  • apps/api/src/realtime/streams.ts
  • packages/realtime/src/client/use-realtime.ts
  • packages/realtime/src/server/handler.ts
  • apps/api/src/realtime/routes.ts
  • packages/realtime/src/client/index.ts
  • packages/realtime/tsdown.config.ts
  • apps/api/src/realtime/emitter.ts
  • apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsx
  • apps/api/src/index.ts
  • packages/realtime/src/client/use-dashboard-realtime.ts
  • packages/realtime/src/server/realtime.ts
  • packages/react/src/realtime/support-provider.tsx
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

**/*.{js,jsx,ts,tsx}: Don't use the accessKey attribute on any HTML element
Use correct ISO language/country codes for the lang attribute
Don't hardcode sensitive data like API keys and tokens
Use === and !==

Files:

  • apps/api/src/realtime/stream-dispatcher.ts
  • packages/realtime/src/client/use-visitor-realtime.ts
  • apps/api/src/realtime/streams.ts
  • packages/realtime/src/client/use-realtime.ts
  • packages/realtime/src/server/handler.ts
  • apps/api/src/realtime/routes.ts
  • packages/realtime/src/client/index.ts
  • packages/realtime/tsdown.config.ts
  • apps/api/src/realtime/emitter.ts
  • apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsx
  • apps/api/src/index.ts
  • packages/realtime/src/client/use-dashboard-realtime.ts
  • packages/realtime/src/server/realtime.ts
  • packages/react/src/realtime/support-provider.tsx
**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

**/*.{js,ts}: Don't use consecutive spaces in regular expression literals
Don't use the arguments object
Don't use the comma operator
Don't use unnecessary boolean casts
Don't use unnecessary callbacks with flatMap
Prefer for...of statements instead of Array.forEach
Don't use this and super in static contexts
Don't use unnecessary catch clauses
Don't use unnecessary constructors
Don't use unnecessary continue statements
Don't export empty modules that don't change anything
Don't use unnecessary escape sequences in regular expression literals
Don't use unnecessary labels
Don't use unnecessary nested block statements
Don't rename imports, exports, and destructured assignments to the same name
Don't use unnecessary string or template literal concatenation
Don't use String.raw in template literals when there are no escape sequences
Don't use useless case statements in switch statements
Avoid ternary operators when simpler alternatives exist
Don't use useless this aliasing
Don't initialize variables to undefined
Don't use void operators
Use Date.now() to get milliseconds since the Unix Epoch
Prefer .flatMap() over map().flat() when possible
Use literal property access instead of computed property access
Don't use parseInt() or Number.parseInt() when numeric literals suffice (binary/octal/hex)
Use concise optional chaining instead of chained logical expressions
Prefer regular expression literals over the RegExp constructor when possible
Don't use non–base-10 or underscore-separated number literal object member names
Remove redundant terms from logical expressions
Prefer while loops over for loops when no initializer/update is needed
Don't reassign const variables
Don't use constant expressions in conditions
Don't use Math.min/Math.max to clamp values when result is constant
Don't use empty character classes in regular expression literals
Don't use empty destructuring patterns
Don't call global object properties as functions
Don't declare functions and vars that are acce...

Files:

  • apps/api/src/realtime/stream-dispatcher.ts
  • packages/realtime/src/client/use-visitor-realtime.ts
  • apps/api/src/realtime/streams.ts
  • packages/realtime/src/client/use-realtime.ts
  • packages/realtime/src/server/handler.ts
  • apps/api/src/realtime/routes.ts
  • packages/realtime/src/client/index.ts
  • packages/realtime/tsdown.config.ts
  • apps/api/src/realtime/emitter.ts
  • apps/api/src/index.ts
  • packages/realtime/src/client/use-dashboard-realtime.ts
  • packages/realtime/src/server/realtime.ts
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

**/*.{js,ts,tsx}: Don't create classes that only have static members
Don't return a value from a constructor
Don't use super() incorrectly inside classes; ensure super() is called in derived constructors
Ensure super() is called exactly once on every code path in a class constructor before this is accessed if the class has a superclass
Place getters and setters for the same property next to each other in class and object definitions
Ensure JSDoc comment lines start with a single asterisk, except for the first one
Don't use duplicate class members

Files:

  • apps/api/src/realtime/stream-dispatcher.ts
  • packages/realtime/src/client/use-visitor-realtime.ts
  • apps/api/src/realtime/streams.ts
  • packages/realtime/src/client/use-realtime.ts
  • packages/realtime/src/server/handler.ts
  • apps/api/src/realtime/routes.ts
  • packages/realtime/src/client/index.ts
  • packages/realtime/tsdown.config.ts
  • apps/api/src/realtime/emitter.ts
  • apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsx
  • apps/api/src/index.ts
  • packages/realtime/src/client/use-dashboard-realtime.ts
  • packages/realtime/src/server/realtime.ts
  • packages/react/src/realtime/support-provider.tsx
**/*.{js,tsx,ts,jsx}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

Use arrow functions instead of function expressions

Files:

  • apps/api/src/realtime/stream-dispatcher.ts
  • packages/realtime/src/client/use-visitor-realtime.ts
  • apps/api/src/realtime/streams.ts
  • packages/realtime/src/client/use-realtime.ts
  • packages/realtime/src/server/handler.ts
  • apps/api/src/realtime/routes.ts
  • packages/realtime/src/client/index.ts
  • packages/realtime/tsdown.config.ts
  • apps/api/src/realtime/emitter.ts
  • apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsx
  • apps/api/src/index.ts
  • packages/realtime/src/client/use-dashboard-realtime.ts
  • packages/realtime/src/server/realtime.ts
  • packages/react/src/realtime/support-provider.tsx
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

**/*.{js,ts,jsx,tsx}: Prevent duplicate polyfills from Polyfill.io
Don't use irregular whitespace characters

Files:

  • apps/api/src/realtime/stream-dispatcher.ts
  • packages/realtime/src/client/use-visitor-realtime.ts
  • apps/api/src/realtime/streams.ts
  • packages/realtime/src/client/use-realtime.ts
  • packages/realtime/src/server/handler.ts
  • apps/api/src/realtime/routes.ts
  • packages/realtime/src/client/index.ts
  • packages/realtime/tsdown.config.ts
  • apps/api/src/realtime/emitter.ts
  • apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsx
  • apps/api/src/index.ts
  • packages/realtime/src/client/use-dashboard-realtime.ts
  • packages/realtime/src/server/realtime.ts
  • packages/react/src/realtime/support-provider.tsx
**/*.{js,ts,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

Don't redeclare variables, functions, classes, and types in the same scope

Files:

  • apps/api/src/realtime/stream-dispatcher.ts
  • packages/realtime/src/client/use-visitor-realtime.ts
  • apps/api/src/realtime/streams.ts
  • packages/realtime/src/client/use-realtime.ts
  • packages/realtime/src/server/handler.ts
  • apps/api/src/realtime/routes.ts
  • packages/realtime/src/client/index.ts
  • packages/realtime/tsdown.config.ts
  • apps/api/src/realtime/emitter.ts
  • apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsx
  • apps/api/src/index.ts
  • packages/realtime/src/client/use-dashboard-realtime.ts
  • packages/realtime/src/server/realtime.ts
  • packages/react/src/realtime/support-provider.tsx
packages/**

📄 CodeRabbit inference engine (AGENTS.md)

Put shared contracts and utilities in packages/* (e.g., core, types, transactional)

Files:

  • packages/realtime/src/client/use-visitor-realtime.ts
  • packages/realtime/src/client/use-realtime.ts
  • packages/realtime/src/server/handler.ts
  • packages/realtime/src/client/index.ts
  • packages/realtime/package.json
  • packages/realtime/tsdown.config.ts
  • packages/realtime/src/client/use-dashboard-realtime.ts
  • packages/realtime/src/server/realtime.ts
  • packages/react/src/realtime/support-provider.tsx
**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/global-rules.mdc)

**/*.tsx: Use the Compound Components pattern with namespaced exports (e.g., FormWizard.Root, FormWizard.Step) so pieces compose under a single parent
Ensure accessibility: keyboard navigation support
Ensure accessibility: include appropriate ARIA attributes
Ensure accessibility: use semantic markup for screen readers
Maintain proper focus management in interactive components
Structure components with forwardRef and typed props/refs as shown (ComponentRef, HTMLAttributes, ReactNode)
Root component acts as main context provider, manages global state, and handles initialization
Child components must be used within Root, read context via hooks, support asChild, and follow consistent props
Props ordering: list required props first; optional props have clear defaults; support common HTML attributes; expose className
Support composition with an asChild prop using Radix UI Slot to allow element replacement
Styling: support className, use data-* attributes for state, and allow style customization
Event handling: support standard DOM events, add custom events when needed, and manage propagation correctly
Base component pattern: forwardRef, choose Slot or intrinsic element via asChild, pass ref/className and spread props

Files:

  • apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsx
  • packages/react/src/realtime/support-provider.tsx
**/*.{jsx,tsx}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

**/*.{jsx,tsx}: Don't set aria-hidden="true" on focusable elements
Don't add ARIA roles, states, and properties to elements that don't support them
Don't use distracting elements like or
Only use the scope prop on elements
Don't assign non-interactive ARIA roles to interactive HTML elements
Ensure label elements have text content and are associated with an input
Don't assign interactive ARIA roles to non-interactive HTML elements
Don't assign tabIndex to non-interactive HTML elements
Don't use positive integers for tabIndex property
Don't include the words "image", "picture", or "photo" in img alt prop
Don't use an explicit role that's the same as the element's implicit role
Static elements with click handlers must have a valid role attribute
Always include a <title> element within SVG elements
Provide meaningful alt text for elements that require alt text
Ensure anchors have content accessible to screen readers
Assign tabIndex to non-interactive elements only when using aria-activedescendant
Include all required ARIA attributes for elements with ARIA roles
Ensure ARIA properties are valid for the element's supported roles
Always include a type attribute for button elements
Elements with interactive roles and handlers must be focusable
Heading elements must have content accessible to screen readers (not hidden with aria-hidden)
Always include a lang attribute on the html element
Always include a title attribute for iframe elements
Accompany onClick with at least one of: onKeyUp, onKeyDown, or onKeyPress
Accompany onMouseOver/onMouseOut with onFocus/onBlur
Include caption tracks for audio and video elements
Prefer semantic elements instead of role attributes in JSX
Ensure all anchors are valid and navigable
Ensure all aria-* properties are valid
Use valid, non-abstract ARIA roles for elements with ARIA roles
Use valid ARIA state and property values
Use valid values for the autocomplete attribute on input elements
Don't use unnecessary fragmen...

Files:

  • apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsx
  • packages/react/src/realtime/support-provider.tsx
**/*.{jsx,tsx,html}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

**/*.{jsx,tsx,html}: Ensure the preconnect attribute is used when using Google Fonts
Don't use target="_blank" without rel="noopener"
Use a recommended display strategy with Google Fonts

Files:

  • apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsx
  • packages/react/src/realtime/support-provider.tsx
apps/web/src/app/**

📄 CodeRabbit inference engine (AGENTS.md)

Place Next.js App Router pages/layouts/routes under apps/web/src/app

Files:

  • apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsx
apps/web/src/{app,components}/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

Use snake_case filenames for React components and layouts in the web app

Files:

  • apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsx
packages/react/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Develop headless React components (including and primitives) under packages/react/src/

Files:

  • packages/react/src/realtime/support-provider.tsx
packages/react/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/react-package-rules.mdc)

packages/react/**/*.{ts,tsx}: Components must render with no built-in styles (headless, unstyled)
Components must accept className, style, and asChild props to enable external styling/control
Do not use emotion, styled-components, or style-prop APIs; styling is via Tailwind/CSS through className/style
Use useRenderElement to create elements and encapsulate render logic (supporting intrinsic tags and render prop overrides)
Components with internal state must be controllable via props (e.g., value/defaultValue/onChange or isOpen/onOpenChange)
Expose internal state via a hook (e.g., useInput, useWindow) or a render prop function child
May reference Base UI logic internally, but never expose Base UI or MUI APIs; do not leak getRootProps/getInputProps; prefer useRenderElement
Prefer small, composable primitives (e.g., Provider, Bubble, Window, MessageList, Message, Input, SendButton, Typing, Event) over monolithic components
Provide a render prop API: render?: (props, state) => ReactElement, and allow className to be string or (state) => string
Support developer-owned rendering: components should not impose visual structure and should allow parent-driven markup via render prop/asChild

Files:

  • packages/react/src/realtime/support-provider.tsx
🧠 Learnings (2)
📚 Learning: 2025-09-17T09:00:28.751Z
Learnt from: CR
PR: cossistantcom/cossistant#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-17T09:00:28.751Z
Learning: Use Bun as the package manager and executor across the monorepo (install, dev, build, lint, format, type-check)

Applied to files:

  • apps/web/package.json
📚 Learning: 2025-09-17T09:00:28.751Z
Learnt from: CR
PR: cossistantcom/cossistant#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-17T09:00:28.751Z
Learning: Applies to packages/types/src/**/*.ts : Keep shared TypeScript types and Zod schemas in packages/types/src/ and update them alongside schema changes

Applied to files:

  • packages/realtime/tsdown.config.ts
🧬 Code graph analysis (10)
apps/api/src/realtime/stream-dispatcher.ts (1)
apps/api/src/realtime/streams.ts (1)
  • realtimeStreams (17-28)
packages/realtime/src/client/use-visitor-realtime.ts (1)
packages/realtime/src/client/use-realtime.ts (1)
  • useRealtime (26-240)
apps/api/src/realtime/streams.ts (3)
packages/types/src/realtime-events.ts (1)
  • RealtimeEvents (9-100)
apps/api/src/redis.ts (1)
  • getRedis (7-39)
packages/realtime/src/server/realtime.ts (2)
  • Realtime (121-129)
  • Realtime (138-140)
packages/realtime/src/server/handler.ts (2)
packages/realtime/src/types.ts (2)
  • UserEvent (8-12)
  • SystemEvent (1-6)
packages/realtime/src/server/realtime.ts (4)
  • Opts (11-16)
  • Realtime (121-129)
  • Realtime (138-140)
  • channel (105-110)
apps/api/src/realtime/routes.ts (6)
packages/realtime/src/server/handler.ts (1)
  • handle (34-279)
apps/api/src/realtime/streams.ts (1)
  • realtimeStreams (17-28)
apps/api/src/lib/auth-validation.ts (2)
  • performAuthentication (366-415)
  • AuthValidationError (25-33)
apps/api/src/db/queries/visitor.ts (1)
  • findVisitorForWebsite (141-160)
apps/api/src/db/queries/session.ts (1)
  • getTRPCSession (104-111)
apps/api/src/db/queries/website.ts (1)
  • getWebsiteByIdWithAccess (247-302)
apps/api/src/realtime/emitter.ts (4)
packages/types/src/realtime-events.ts (1)
  • RealtimeEvent (108-115)
apps/api/src/ws/socket.ts (2)
  • sendEventToVisitor (85-96)
  • sendEventToWebsite (98-109)
apps/api/src/realtime/stream-dispatcher.ts (1)
  • forwardMessageEventToRealtimeStreams (28-39)
apps/api/src/ws/router.ts (1)
  • routeEvent (254-280)
apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsx (3)
packages/realtime/src/client/use-dashboard-realtime.ts (1)
  • useDashboardRealtime (20-63)
packages/types/src/realtime-events.ts (1)
  • RealtimeEvent (108-115)
apps/web/src/app/(dashboard)/[websiteSlug]/providers/events/handlers/message-created.ts (1)
  • handleMessageCreated (73-132)
apps/api/src/index.ts (1)
apps/api/src/realtime/routes.ts (1)
  • registerRealtimeRoutes (142-152)
packages/realtime/src/client/use-dashboard-realtime.ts (1)
packages/realtime/src/client/use-realtime.ts (1)
  • useRealtime (26-240)
packages/react/src/realtime/support-provider.tsx (4)
packages/react/src/provider.tsx (1)
  • useSupport (215-241)
packages/types/src/realtime-events.ts (1)
  • RealtimeEvent (108-115)
packages/react/src/realtime/use-realtime.ts (1)
  • useRealtime (68-144)
packages/realtime/src/client/use-visitor-realtime.ts (1)
  • useVisitorRealtime (22-68)
🔇 Additional comments (11)
packages/realtime/package.json (1)

56-56: LGTM! Dependency swap aligns with the PR objective.

The switch from @upstash/redis to ioredis version 5.4.2 is consistent with the PR's goal to adopt ioredis-based XADD/XRANGE handling. Based on learnings, ioredis 5.4.2 is the current stable release with TypeScript-first support.

apps/web/package.json (1)

109-109: LGTM! Standard formatting improvement.

The trailing newline follows common file-ending conventions.

packages/realtime/src/client/index.ts (1)

2-3: LGTM! Clean hook exports.

The new useVisitorRealtime and useDashboardRealtime hooks are properly exported alongside the existing useRealtime export, enabling consumers to leverage the specialized SSE-based realtime functionality introduced in this PR.

packages/react/src/realtime/support-provider.tsx (2)

33-43: LGTM! Proper memoization with useCallback.

The handleMessageCreated callback is correctly memoized with useCallback and includes appropriate dependencies ([client, website?.id]). The site ownership check (line 35-37) prevents processing events from other websites.


108-113: Document the dual-hook pattern rationale.

Similar to the dashboard provider, this component now uses two separate realtime hooks:

  1. useRealtime for CONVERSATION_SEEN and CONVERSATION_TYPING (lines 99-104)
  2. useVisitorRealtime for MESSAGE_CREATED (lines 108-113)

This creates the same architectural questions as in the dashboard provider:

  1. Connection efficiency: Two separate connections (WebSocket + SSE)?
  2. Event segregation: Why is MESSAGE_CREATED handled differently?
  3. Consistency: Both providers follow this pattern, suggesting it's intentional, but the reasoning should be documented.

Based on the PR objectives mentioning "continues to run websocket handlers for other events," it appears MESSAGE_CREATED was intentionally migrated to SSE while other events remain on WebSocket. If this is correct, consider adding a comment explaining this architecture decision.

// MESSAGE_CREATED events are delivered via SSE (useVisitorRealtime)
// for better scalability and connection persistence.
// Other events (SEEN, TYPING) continue using WebSocket (useRealtime)
// for lower latency bi-directional communication.
useVisitorRealtime({
  websiteId: realtimeContext.websiteId,
  visitorId: realtimeContext.visitorId,
  publicKey: publicKey ?? null,
  onMessageCreated: handleMessageCreated,
});
apps/api/src/index.ts (1)

143-144: Authentication present in realtime routes visitorStreamHandler enforces publicKey & visitorId query params; dashboardStreamHandler validates session via getTRPCSession and checks user.id.

apps/api/src/realtime/emitter.ts (1)

130-137: Event construction looks correct

Includes required fields; timestamp via Date.now(); types align.

apps/api/src/realtime/streams.ts (1)

6-13: Redis-backed Realtime schema and wiring LGTM

Schema matches MESSAGE_CREATED. Namespaces and verbosity are sane.

Also applies to: 17-29

apps/api/src/realtime/stream-dispatcher.ts (1)

28-39: Forwarder implementation is solid

Type guard + concurrent emits with allSettled. Good non-blocking behavior.

packages/realtime/src/client/use-visitor-realtime.ts (1)

22-68: Hook API and behavior look good

Gated enablement, stable handler ref, and events mapping are sound.

packages/realtime/src/client/use-dashboard-realtime.ts (1)

42-51: Sanity-check event path and client re-export

  • Verify the server emits the ["dashboard","MESSAGE_CREATED"] event path.
  • Ensure use-dashboard-realtime is re-exported in packages/realtime/src/client/index.ts.

Comment on lines +32 to +101
const visitorStreamHandler = handle({
realtime: realtimeStreams,
middleware: async ({ request, channel }) => {
const url = new URL(request.url);
const publicKey = url.searchParams.get("publicKey");
const visitorId = url.searchParams.get("visitorId");

if (!publicKey || !visitorId) {
return new Response("Missing credentials", { status: 401 });
}

try {
const authResult = await performAuthentication(
undefined,
publicKey,
db,
parseOriginHeaders(request)
);

const websiteId = authResult.apiKey.website?.id;

if (!websiteId) {
return new Response("Website not found", { status: 403 });
}

if (channel !== websiteId) {
return new Response("Channel mismatch", { status: 403 });
}

const visitor = await findVisitorForWebsite(db, {
visitorId,
websiteId,
});

if (!visitor || visitor.blockedAt) {
return new Response("Visitor not allowed", { status: 403 });
}
} catch (error) {
if (error instanceof AuthValidationError) {
return new Response(error.message, { status: error.statusCode });
}
console.error("[Realtime] Visitor auth failed", error);
return new Response("Authentication failed", { status: 401 });
}
},
filter: async ({ event, request }) => {
if (!Array.isArray(event.__event_path)) {
return false;
}

if (event.__event_path[0] !== "visitor") {
return false;
}

const data = event.data as RealtimeEvent;
if (!data || data.type !== "MESSAGE_CREATED") {
return false;
}

const visitorId = new URL(request.url).searchParams.get("visitorId");
if (!visitorId) {
return false;
}

const payloadVisitorId =
data.payload.message.visitorId ?? data.visitorId ?? null;

return !payloadVisitorId || payloadVisitorId === visitorId;
},
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add CORS headers to visitor stream handler

Cross-origin EventSource requires ACAO. Mirror dashboard’s approach to avoid connection failures.

Apply this diff to add responseHeaders:

 const visitorStreamHandler = handle({
   realtime: realtimeStreams,
   middleware: async ({ request, channel }) => {
     …
   },
   filter: async ({ event, request }) => {
     …
-  },
+  },
+  responseHeaders: ({ request }) => {
+    const origin = request.headers.get("origin");
+    if (!origin) {
+      return undefined;
+    }
+    return {
+      "Access-Control-Allow-Origin": origin,
+      "Access-Control-Allow-Credentials": "true",
+      Vary: "Origin",
+    } satisfies HeadersInit;
+  },
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const visitorStreamHandler = handle({
realtime: realtimeStreams,
middleware: async ({ request, channel }) => {
const url = new URL(request.url);
const publicKey = url.searchParams.get("publicKey");
const visitorId = url.searchParams.get("visitorId");
if (!publicKey || !visitorId) {
return new Response("Missing credentials", { status: 401 });
}
try {
const authResult = await performAuthentication(
undefined,
publicKey,
db,
parseOriginHeaders(request)
);
const websiteId = authResult.apiKey.website?.id;
if (!websiteId) {
return new Response("Website not found", { status: 403 });
}
if (channel !== websiteId) {
return new Response("Channel mismatch", { status: 403 });
}
const visitor = await findVisitorForWebsite(db, {
visitorId,
websiteId,
});
if (!visitor || visitor.blockedAt) {
return new Response("Visitor not allowed", { status: 403 });
}
} catch (error) {
if (error instanceof AuthValidationError) {
return new Response(error.message, { status: error.statusCode });
}
console.error("[Realtime] Visitor auth failed", error);
return new Response("Authentication failed", { status: 401 });
}
},
filter: async ({ event, request }) => {
if (!Array.isArray(event.__event_path)) {
return false;
}
if (event.__event_path[0] !== "visitor") {
return false;
}
const data = event.data as RealtimeEvent;
if (!data || data.type !== "MESSAGE_CREATED") {
return false;
}
const visitorId = new URL(request.url).searchParams.get("visitorId");
if (!visitorId) {
return false;
}
const payloadVisitorId =
data.payload.message.visitorId ?? data.visitorId ?? null;
return !payloadVisitorId || payloadVisitorId === visitorId;
},
});
const visitorStreamHandler = handle({
realtime: realtimeStreams,
middleware: async ({ request, channel }) => {
const url = new URL(request.url);
const publicKey = url.searchParams.get("publicKey");
const visitorId = url.searchParams.get("visitorId");
if (!publicKey || !visitorId) {
return new Response("Missing credentials", { status: 401 });
}
try {
const authResult = await performAuthentication(
undefined,
publicKey,
db,
parseOriginHeaders(request)
);
const websiteId = authResult.apiKey.website?.id;
if (!websiteId) {
return new Response("Website not found", { status: 403 });
}
if (channel !== websiteId) {
return new Response("Channel mismatch", { status: 403 });
}
const visitor = await findVisitorForWebsite(db, {
visitorId,
websiteId,
});
if (!visitor || visitor.blockedAt) {
return new Response("Visitor not allowed", { status: 403 });
}
} catch (error) {
if (error instanceof AuthValidationError) {
return new Response(error.message, { status: error.statusCode });
}
console.error("[Realtime] Visitor auth failed", error);
return new Response("Authentication failed", { status: 401 });
}
},
filter: async ({ event, request }) => {
if (!Array.isArray(event.__event_path)) {
return false;
}
if (event.__event_path[0] !== "visitor") {
return false;
}
const data = event.data as RealtimeEvent;
if (!data || data.type !== "MESSAGE_CREATED") {
return false;
}
const visitorId = new URL(request.url).searchParams.get("visitorId");
if (!visitorId) {
return false;
}
const payloadVisitorId =
data.payload.message.visitorId ?? data.visitorId ?? null;
return !payloadVisitorId || payloadVisitorId === visitorId;
},
responseHeaders: ({ request }) => {
const origin = request.headers.get("origin");
if (!origin) {
return undefined;
}
return {
"Access-Control-Allow-Origin": origin,
"Access-Control-Allow-Credentials": "true",
Vary: "Origin",
} satisfies HeadersInit;
},
});
🤖 Prompt for AI Agents
In apps/api/src/realtime/routes.ts around lines 32 to 101, the visitor stream
handler lacks CORS response headers which breaks cross-origin EventSource
connections; add a responseHeaders property to the handler that mirrors the
dashboard approach: set "Access-Control-Allow-Origin" to the request origin
(fallback to "*" if absent), include "Access-Control-Allow-Credentials": "true",
and add "Vary": "Origin" so browsers accept the EventSource connection from
other origins.

Comment on lines +96 to +100
const payloadVisitorId =
data.payload.message.visitorId ?? data.visitorId ?? null;

return !payloadVisitorId || payloadVisitorId === visitorId;
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Tighten visitor filter to prevent unintended broadcasts

Allowing null payloadVisitorId sends events to all visitors. Require an exact match to the requesting visitor.

Apply this diff:

-                const payloadVisitorId =
-                        data.payload.message.visitorId ?? data.visitorId ?? null;
-
-                return !payloadVisitorId || payloadVisitorId === visitorId;
+                const payloadVisitorId =
+                  data.payload.message.visitorId ?? data.visitorId ?? null;
+
+                if (!payloadVisitorId) {
+                  return false;
+                }
+                return payloadVisitorId === visitorId;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const payloadVisitorId =
data.payload.message.visitorId ?? data.visitorId ?? null;
return !payloadVisitorId || payloadVisitorId === visitorId;
},
const payloadVisitorId =
data.payload.message.visitorId ?? data.visitorId ?? null;
if (!payloadVisitorId) {
return false;
}
return payloadVisitorId === visitorId;
🤖 Prompt for AI Agents
In apps/api/src/realtime/routes.ts around lines 96 to 100, the current filter
treats a null/undefined payloadVisitorId as a wildcard and broadcasts to all
visitors; change the condition so only an exact match passes. Replace the
existing logic that computes payloadVisitorId and returns !payloadVisitorId ||
payloadVisitorId === visitorId with a comparison that returns payloadVisitorId
!== null && payloadVisitorId !== undefined && payloadVisitorId === visitorId (or
simply return payloadVisitorId === visitorId), ensuring null/undefined values do
not match and thus prevent unintended broadcasts.

Comment on lines 258 to 263
keepaliveInterval = setInterval(async () => {
await redis.publish(`channel:${channel}`, {
type: "ping",
timestamp: Date.now(),
});
await redis.publish(
streamKey,
JSON.stringify({ type: "ping", timestamp: Date.now() })
);
}, 10_000);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid O(n²) ping fan-out; send per-connection pings instead of publishing.

Current keepalive publishes to the shared channel; with N clients, you produce N messages that each client receives → O(n²) traffic. Emit pings directly to this SSE stream.

Apply:

-        keepaliveInterval = setInterval(async () => {
-          await redis.publish(
-            streamKey,
-            JSON.stringify({ type: "ping", timestamp: Date.now() })
-          );
-        }, 10_000);
+        keepaliveInterval = setInterval(() => {
+          safeEnqueue(
+            json({ type: "ping", timestamp: Date.now() } as SystemEvent)
+          );
+        }, 10_000);

You can keep the subscriber ping handling for externally-generated pings; this change prevents per-connection ping storms.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
keepaliveInterval = setInterval(async () => {
await redis.publish(`channel:${channel}`, {
type: "ping",
timestamp: Date.now(),
});
await redis.publish(
streamKey,
JSON.stringify({ type: "ping", timestamp: Date.now() })
);
}, 10_000);
keepaliveInterval = setInterval(() => {
safeEnqueue(
json({ type: "ping", timestamp: Date.now() } as SystemEvent)
);
}, 10_000);
🤖 Prompt for AI Agents
In packages/realtime/src/server/handler.ts around lines 258-263, the keepalive
currently calls redis.publish to send a JSON ping to the shared channel which
causes O(n²) fan-out; change the interval callback to emit the ping directly to
the SSE response for this connection (e.g., res.write a ping/comment or an SSE
event with type "ping" and timestamp) instead of publishing to Redis, leaving
the existing subscriber-side handling intact for externally-generated pings;
ensure the interval still runs every 10_000ms and is cleared on connection
close.

Comment on lines +356 to +357
console.warn("Failed to parse stream entry", { error });
return null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Don’t use console; route through logger or silence.

Replace console.warn with the internal logger, or remove to satisfy guidelines.

Apply:

-    console.warn("Failed to parse stream entry", { error });
+    // Consider routing via the provided realtime logger if needed.
+    // logger.warn("Failed to parse stream entry", { error });

Optionally add a logger param to parseStreamEntry and call with the scoped logger where used.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
console.warn("Failed to parse stream entry", { error });
return null;
// Consider routing via the provided realtime logger if needed.
// logger.warn("Failed to parse stream entry", { error });
return null;
🤖 Prompt for AI Agents
In packages/realtime/src/server/handler.ts around lines 356-357, replace the
console.warn call with the internal logger (or remove it) so logging follows
project guidelines: update parseStreamEntry to accept a logger parameter (or
import/use the scoped logger available in this module), call logger.warn with
the same message and error object instead of console.warn, and update all call
sites to pass the scoped logger; ensure types/signatures are updated accordingly
and tests/linters pass.

Comment on lines +69 to +72
const streamPayload = JSON.stringify({
data,
__event_path: [outerKey, innerKey],
};
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Add error handling for JSON.stringify.

JSON.stringify can throw on circular references, BigInt values, or other non-serializable data. Consider wrapping this in a try-catch to provide a clearer error message or fallback behavior.

Apply this diff to add error handling:

-            const streamPayload = JSON.stringify({
-              data,
-              __event_path: [outerKey, innerKey],
-            });
+            let streamPayload: string;
+            try {
+              streamPayload = JSON.stringify({
+                data,
+                __event_path: [outerKey, innerKey],
+              });
+            } catch (error) {
+              this._logger.error("Failed to serialize event payload:", error);
+              throw new Error(`Event serialization failed: ${error instanceof Error ? error.message : String(error)}`);
+            }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const streamPayload = JSON.stringify({
data,
__event_path: [outerKey, innerKey],
};
});
let streamPayload: string;
try {
streamPayload = JSON.stringify({
data,
__event_path: [outerKey, innerKey],
});
} catch (error) {
this._logger.error("Failed to serialize event payload:", error);
throw new Error(
`Event serialization failed: ${
error instanceof Error ? error.message : String(error)
}`
);
}
🤖 Prompt for AI Agents
In packages/realtime/src/server/realtime.ts around lines 69 to 72,
JSON.stringify can throw for circular refs or non-serializable values; wrap the
JSON.stringify call in a try-catch, and on error log/emit a clear diagnostic
including outerKey and innerKey and the error message, then use a safe fallback
payload (for example an object indicating serialization failed with
error.message and data type info, or a sanitized version converting BigInt to
string) so the process doesn’t crash; ensure the streamPayload variable is set
to either the successful JSON string or the fallback string and preserve enough
context to debug.

Comment on lines +90 to +97
await this._redis.publish(
`channel:${channel}`,
JSON.stringify({
data,
__event_path: [outerKey, innerKey],
__stream_id: id,
})
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Add error handling for publish JSON.stringify.

Similar to the stream payload, the publish call uses JSON.stringify without error handling.

Apply this diff to add error handling:

-            await this._redis.publish(
-              `channel:${channel}`,
-              JSON.stringify({
-                data,
-                __event_path: [outerKey, innerKey],
-                __stream_id: id,
-              })
-            );
+            let publishPayload: string;
+            try {
+              publishPayload = JSON.stringify({
+                data,
+                __event_path: [outerKey, innerKey],
+                __stream_id: id,
+              });
+            } catch (error) {
+              this._logger.error("Failed to serialize publish payload:", error);
+              throw new Error(`Publish serialization failed: ${error instanceof Error ? error.message : String(error)}`);
+            }
+            
+            await this._redis.publish(
+              `channel:${channel}`,
+              publishPayload
+            );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await this._redis.publish(
`channel:${channel}`,
JSON.stringify({
data,
__event_path: [outerKey, innerKey],
__stream_id: id,
})
);
let publishPayload: string;
try {
publishPayload = JSON.stringify({
data,
__event_path: [outerKey, innerKey],
__stream_id: id,
});
} catch (error) {
this._logger.error("Failed to serialize publish payload:", error);
throw new Error(`Publish serialization failed: ${error instanceof Error ? error.message : String(error)}`);
}
await this._redis.publish(
`channel:${channel}`,
publishPayload
);
🤖 Prompt for AI Agents
In packages/realtime/src/server/realtime.ts around lines 90 to 97, the
JSON.stringify call inside the publish lacks error handling which can throw and
crash the flow; wrap the JSON.stringify(...) in a try/catch, on error log the
serialization failure with contextual info (channel, outerKey, innerKey, id and
the data that failed) and do not call publish with an invalid payload (either
skip publishing or publish a safe error/placeholder payload), ensuring any
caught exception does not bubble out.

bundle: false,
dts: true,
entry: ["src/**/*.{ts,tsx}"],
platform: "browser",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Platform "browser" may not be appropriate for server-side code.

The platform: "browser" setting is applied globally, but this package contains both client code (src/client/) and server code (src/server/). Server-side modules (e.g., realtime.ts using ioredis) require Node.js runtime features and should be built with platform: "node".

Consider one of the following solutions:

Solution 1 (recommended): Split build configuration

 export default defineConfig({
-  entry: ["src/**/*.{ts,tsx}"],
-  platform: "browser",
+  entry: {
+    "client/index": "src/client/index.ts",
+    "server/index": "src/server/index.ts",
+  },
+  platform: {
+    "client/*": "browser",
+    "server/*": "node",
+  },

Solution 2: Use conditional platform based on entry path

Check tsdown documentation to see if it supports per-entry platform overrides or use separate build configs for client and server.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
platform: "browser",
export default defineConfig({
entry: {
"client/index": "src/client/index.ts",
"server/index": "src/server/index.ts",
},
platform: {
"client/*": "browser",
"server/*": "node",
},
// …other existing config options (e.g. commented externals, outdir, etc.)…
});
🤖 Prompt for AI Agents
In packages/realtime/tsdown.config.ts around line 5, the config sets platform:
"browser" globally but this package contains server code (src/server/* and files
like realtime.ts using ioredis) that must be built for Node; change the config
to apply "browser" only to client entries and "node" to server entries by either
(recommended) splitting into two tsdown config files (e.g.,
tsdown.client.config.ts and tsdown.server.config.ts) or configuring
per-entry/platform overrides if tsdown supports it; update build scripts to
invoke the appropriate config for client and server builds and ensure server
entries are built with platform: "node" so Node runtime APIs are preserved.

@Rieranthony Rieranthony closed this Oct 9, 2025

This branch had an error being deployed

1 failed deployment
Preview — 0ca3b5bd Deployed Oct 9, 2025 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant