Skip to content

feat(rivetkit): convex support#4018

Open
NathanFlurry wants to merge 1 commit intomainfrom
convex-adapter
Open

feat(rivetkit): convex support#4018
NathanFlurry wants to merge 1 commit intomainfrom
convex-adapter

Conversation

@NathanFlurry
Copy link
Member

No description provided.

@railway-app
Copy link

railway-app bot commented Jan 23, 2026

🚅 Deployed to the rivet-pr-4018 environment in rivet-frontend

Service Status Web Updated (UTC)
mcp-hub ✅ Success (View Logs) Web Feb 17, 2026 at 8:58 am
website ❌ Build Failed (View Logs) Web Jan 30, 2026 at 8:22 am
frontend-cloud ❌ Build Failed (View Logs) Web Jan 30, 2026 at 8:21 am
frontend-inspector ❌ Build Failed (View Logs) Web Jan 30, 2026 at 8:20 am

Copy link
Member Author


How to use the Graphite Merge Queue

Add the label merge-queue to this PR to add it to the merge queue.

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

@claude
Copy link

claude bot commented Jan 23, 2026

PR Review: Split Node & Browser Entrypoints

Overview

This PR refactors RivetKit to properly separate Node.js and browser/edge runtime entrypoints through conditional exports. It adds a new Convex integration package and improves runtime compatibility. Overall, the changes are well-architected and follow good patterns for multi-platform support.


✅ Strengths

1. Clean Dependency Injection Pattern

The new utils/node.ts module provides a clean dependency injection approach that avoids conditional imports in core code. This is excellent for maintaining platform compatibility:

  • setNodeDependencies() and getter functions like getNodeFs(), getHonoNodeServer()
  • Clear error messages when Node dependencies are unavailable
  • Well-documented with JSDoc comments

2. Proper Package.json Conditional Exports

The package.json exports configuration correctly routes Node.js vs browser imports:

"node": {
  "import": "./dist/tsup/node-entry.js",
  "require": "./dist/tsup/node-entry.cjs"
},
"browser": {
  "default": "./dist/tsup/mod.js"
}

3. Polyfill Strategy

The node-setup.ts file properly injects WebSocket and EventSource polyfills into globalThis before the rest of the code loads. This is the right approach for Node.js environments lacking native support.

4. WebSocket Runtime Detection

engine/sdks/typescript/runner/src/websocket.ts has improved detection that checks both WebSocket and globalThis.WebSocket, handling various runtimes (browsers, Deno, Node 22+, edge runtimes).


⚠️ Issues & Concerns

1. Critical: Infinite Loop in Convex Action Handler 🔴

Location: rivetkit-typescript/packages/convex/src/action.ts:75 and src/http.ts:148

// For SSE endpoints like /start, keep the connection alive
const url = new URL(args.url);
if (url.pathname.endsWith("/start")) {
    // Keep the action running to maintain WebSocket connection
    // The SSE stream never completes, so we wait indefinitely
    await new Promise(() => {});
}

Problem: This creates an infinite loop that will never resolve. While the comment says "SSE stream never completes", this will:

  • Hang the Convex action indefinitely
  • Potentially hit Convex's action timeout limits
  • Block resources unnecessarily
  • Make it impossible to properly clean up connections

Recommendation: This needs a proper solution. Consider:

  • Using Convex's streaming response capabilities if available
  • Implementing a timeout with cleanup
  • Rethinking the architecture if SSE endpoints require indefinite connections
  • At minimum, document the expected timeout behavior and resource implications

2. Deprecated Function Without Migration Path

Location: rivetkit-typescript/packages/rivetkit/src/common/websocket.ts:22

/**
 * @deprecated Use getWebSocket() instead. This async version exists for backwards compatibility.
 */
export async function importWebSocket(): Promise<typeof WebSocket> {
    return getWebSocket();
}

Issue: The function is marked deprecated but:

  • No timeline for removal is specified
  • The async signature doesn't match the sync getWebSocket(), making migration non-trivial for users who rely on the async behavior
  • Same issue exists for importEventSource() in eventsource.ts

Recommendation: Add deprecation timeline and migration guide in comments, or consider if the async version serves a legitimate purpose.

3. WebSocket Import Bundler Workaround

Location: engine/sdks/typescript/runner/src/websocket.ts:32-40

const dynamicImport = new Function(
    "moduleName",
    "return import(moduleName)",
) as (moduleName: string) => Promise<any>;
const ws = await dynamicImport("ws");

Concerns:

  • This is a clever bundler evasion technique, but it's fragile
  • Type safety is lost (as (moduleName: string) => Promise<any>)
  • CSP (Content Security Policy) will block this in some environments
  • The comment explains the "why" well, but this could break in stricter environments

Recommendation: While this might be necessary for edge runtimes, consider:

  • Adding a comment about CSP implications
  • Testing this works in target Convex/Cloudflare environments
  • Using stronger typing than any

4. Optional Dependencies in Engine Runner

Location: engine/sdks/typescript/runner/package.json:33

"optionalDependencies": {
    "ws": "^8.18.3"
}

Issue: Moving ws to optionalDependencies means:

  • It might not be installed in some environments
  • The code has a try-catch fallback, but the MockWebSocket will crash at runtime if someone actually tries to use it
  • This could lead to confusing errors in Node.js < 22 without native WebSocket

Recommendation: Document clearly when ws is required vs optional. Consider if this should be a peerDependency with peerDependenciesMeta.optional = true instead.

5. Missing Validation in createFileSystemOrMemoryDriver

Location: rivetkit-typescript/packages/rivetkit/src/drivers/file-system/mod.ts:16-22

if (!hasNodeDependencies()) {
    throw new Error(
        "File system driver requires Node.js. " +
            "Ensure you are importing from 'rivetkit' in a Node.js environment.",
    );
}

Good: Validates Node dependencies are available.

Missing: No validation that customPath (if provided) is valid or accessible. Consider adding basic path validation.


🔍 Code Quality Observations

1. Consistent Error Messages

Error messages throughout are helpful and actionable:

"Node.js dependencies not available. This feature requires Node.js. If you're in Node.js, ensure you're importing from 'rivetkit' (not 'rivetkit/browser')."

2. Documentation Quality

Most new code has excellent JSDoc comments with usage examples, especially in:

  • rivetkit-typescript/packages/convex/src/action.ts
  • rivetkit-typescript/packages/convex/src/http.ts
  • rivetkit-typescript/packages/rivetkit/src/utils/node.ts

3. Convex Example Structure

The new examples/convex/ follows good practices:

  • Proper gitignore
  • Clear README
  • Working example code

🎯 Testing Recommendations

1. Runtime Detection Tests

Add tests to verify:

  • WebSocket detection works in Node.js 22+, older Node, browsers, Deno, Bun
  • Proper fallback to ws package when native WebSocket unavailable
  • Error messages when neither native nor package WebSocket available

2. Dependency Injection Tests

Test that:

  • hasNodeDependencies() returns false before setup, true after
  • Getter functions throw correct errors in browser/edge environments
  • All injected dependencies are correctly typed

3. Convex Integration Tests

Critical to test:

  • The /start endpoint behavior (especially the infinite loop issue)
  • Request/response serialization/deserialization
  • Error handling in both HTTP actions and Node.js actions

4. Package Exports Tests

Verify conditional exports work correctly:

  • import 'rivetkit' in Node.js loads node-entry.js
  • import 'rivetkit' in browser loads mod.js
  • All subpath exports resolve correctly

📋 Minor Issues

  1. Duplicate keyword in package.json:14 - "actors" appears twice in keywords array
  2. Engine requirement: "node": ">=22.0.0" is strict but the code supports older Node with ws package - consider if this should be >=18.0.0 with a note about native WebSocket

🔒 Security Considerations

  1. CSP Compatibility: The new Function() approach in websocket.ts will fail under strict CSP. Document this limitation.
  2. Input Validation: The Convex handlers accept v.any() for headers - consider validating header structure.

📊 Overall Assessment

Quality: Good overall with some critical issues to address

Readiness: Needs fixes before merge, particularly the infinite loop issue

Priority Fixes:

  1. 🔴 Critical: Fix the infinite await new Promise(() => {}) in Convex action handlers
  2. 🟡 Important: Clarify the async deprecation migration path
  3. 🟡 Important: Test and document the ws optional dependency behavior

Nice to Have:

  • Add path validation in createFileSystemDriver
  • Improve typing for dynamic imports
  • Add comprehensive runtime detection tests

✅ Summary

This is a well-architected refactoring that properly separates Node.js and browser concerns. The dependency injection pattern is clean and maintainable. However, the infinite loop issue in Convex action handlers must be fixed before merging. Once addressed, this will significantly improve RivetKit's multi-platform support.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Jan 23, 2026

More templates

@rivetkit/cloudflare-workers

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/cloudflare-workers@4018

@rivetkit/convex

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/convex@4018

@rivetkit/db

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/db@4018

@rivetkit/framework-base

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/framework-base@4018

@rivetkit/next-js

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/next-js@4018

@rivetkit/react

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/react@4018

rivetkit

pnpm add https://pkg.pr.new/rivet-dev/rivet/rivetkit@4018

@rivetkit/sql-loader

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/sql-loader@4018

@rivetkit/virtual-websocket

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/virtual-websocket@4018

@rivetkit/engine-runner

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/engine-runner@4018

@rivetkit/engine-runner-protocol

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/engine-runner-protocol@4018

commit: c45986d

@NathanFlurry NathanFlurry changed the title chore(rivetkit): split up node & browser entrypoints feat(rivetkit): convex support Jan 23, 2026
Comment on lines +68 to +90
const response = await registry.handler(request);

// For SSE endpoints like /start, keep the connection alive
const url = new URL(args.url);
if (url.pathname.endsWith("/start")) {
// Keep the action running to maintain WebSocket connection
// The SSE stream never completes, so we wait indefinitely
await new Promise(() => {});
}

// Convert response to serializable format
const responseBody = await response.text();
const responseHeaders: Record<string, string> = {};
response.headers.forEach((value, key) => {
responseHeaders[key] = value;
});

return {
status: response.status,
statusText: response.statusText,
headers: responseHeaders,
body: responseBody,
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Critical bug: unreachable code after infinite await. When the pathname ends with /start, the code awaits new Promise(() => {}) on line 75, which never resolves. This causes the function to hang until timeout, and the response serialization code (lines 79-90) will never execute.

The response object is created on line 68 but never returned for /start endpoints. Either:

  1. Remove the infinite await and return the response immediately, or
  2. Move the response serialization before the conditional check
// Handle request
const response = await registry.handler(request);

// Convert response to serializable format BEFORE the check
const responseBody = await response.text();
const responseHeaders: Record<string, string> = {};
response.headers.forEach((value, key) => {
    responseHeaders[key] = value;
});

const serialized = {
    status: response.status,
    statusText: response.statusText,
    headers: responseHeaders,
    body: responseBody,
};

// For SSE endpoints like /start, keep the connection alive
const url = new URL(args.url);
if (url.pathname.endsWith("/start")) {
    await new Promise(() => {});
}

return serialized;
Suggested change
const response = await registry.handler(request);
// For SSE endpoints like /start, keep the connection alive
const url = new URL(args.url);
if (url.pathname.endsWith("/start")) {
// Keep the action running to maintain WebSocket connection
// The SSE stream never completes, so we wait indefinitely
await new Promise(() => {});
}
// Convert response to serializable format
const responseBody = await response.text();
const responseHeaders: Record<string, string> = {};
response.headers.forEach((value, key) => {
responseHeaders[key] = value;
});
return {
status: response.status,
statusText: response.statusText,
headers: responseHeaders,
body: responseBody,
};
const response = await registry.handler(request);
// Convert response to serializable format
const responseBody = await response.text();
const responseHeaders: Record<string, string> = {};
response.headers.forEach((value, key) => {
responseHeaders[key] = value;
});
const serialized = {
status: response.status,
statusText: response.statusText,
headers: responseHeaders,
body: responseBody,
};
// For SSE endpoints like /start, keep the connection alive
const url = new URL(args.url);
if (url.pathname.endsWith("/start")) {
// Keep the action running to maintain WebSocket connection
// The SSE stream never completes, so we wait indefinitely
await new Promise(() => {});
}
return serialized;

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant