Skip to content

Serve realtime and MCP from the web app on Vercel - #87

Merged
pulkitxm merged 20 commits into
mainfrom
worktree-realtime-on-vercel
Aug 3, 2026
Merged

Serve realtime and MCP from the web app on Vercel#87
pulkitxm merged 20 commits into
mainfrom
worktree-realtime-on-vercel

Conversation

@pulkitxm

@pulkitxm pulkitxm commented Aug 3, 2026

Copy link
Copy Markdown
Member

Move the websocket hub and the MCP server into the Next.js app so Orbit deploys as one Vercel project, and off Bun built-ins so the node runtime can upgrade the socket.

pulkitxm added 13 commits August 2, 2026 15:03
The server owned both the Bun socket handling and every piece of realtime
state, so nothing could drive it from another transport. Move the
connections map, presence, authentication, Redis fan-out and heartbeat
into a hub that accepts an abstract socket, and leave Bun.serve as a thin
adapter over it. A second adapter can now attach a different socket
implementation to the same hub.
The web app needs the same hub to serve sockets from a route handler, and
cross-app code belongs in packages rather than being imported out of
another app. Move authentication, the connection, presence, the socket
abstraction and the hub into @orbit/realtime-server, leaving the app with
its Bun.serve adapter and entrypoint.
Vercel functions can upgrade a request through @vercel/functions, so the
web app can host the socket the client already expects instead of a
separate service. Attach the shared hub to the upgraded socket in a route
handler, and default the client to the same origin so no realtime host
has to be configured. NEXT_PUBLIC_REALTIME_URL still wins when it is set,
which keeps the local Bun server and any external host working.

A health route reports whether Redis is configured and what the hub is
holding, because publishing silently does nothing without REDIS_URL.
The Vercel route hands the hub a ws socket rather than a Bun one, and
nothing covered that adapter. Run authentication, scope authorisation and
redis delta delivery over a real ws server so the transport the web app
uses is exercised the same way the Bun one already is.
The MCP server ran as its own deployable, which the move to a single
Vercel project leaves nowhere to run, and /mcp has been returning 404
since the migration. Its transport was tied to node http, so swap it for
the SDK's web standard transport and expose a fetch handler that a route
can call directly.

Tools and the handler move into @orbit/mcp-server so they keep their own
suite instead of joining the web app's DOM tests, and the tests now drive
the handler through the client transport rather than a listening socket.
Vercel only upgrades websockets on its node runtime, and the bun runtime
was chosen because the data layer imports Bun.SQL. Move the driver to
postgres.js behind the same pool cache and transaction pooler detection.

Its unsafe path does not serialise a Date, which drizzle uses for raw
execute, so the cycle queries now pass timestamps as ISO strings.
Bun.RedisClient only exists on the bun runtime, and the hub has to run
inside a Vercel function on node. Move the publisher and the subscriber
to ioredis, which keeps working under bun for the local socket server.
Bun.S3Client cannot run on the node runtime the websocket upgrade needs.
Move to the aws sdk and its presigner, which take explicit credentials
without ever reaching for an ambient session token.

That removes the probe which refused to start when one leaked in, and
with it the need to unset AWS_SESSION_TOKEN before running the suite.
Vercel only injects the websocket upgrade bridge on its node runtime. The
bun runtime was selected because the server code reached for Bun built-ins,
and with those gone the pin can go too.

Passwords move to @node-rs/argon2 and sortable ids to uuid v7 behind a
shared helper. Both argon2 implementations read the same PHC digest, so
existing passwords keep verifying, which a test now covers.
The health route only described a hub that some other request had already
built, so it answered ok while the socket route was failing to reach
redis. Open the hub and surface its connection state or the error.
Awaiting the hub first meant the handler did redis work before the
upgrade, and the connection never reached a 101. Upgrade immediately,
then attach the hub, holding any frames that arrive in between.
@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
orbit Ready Ready Preview Aug 3, 2026 11:33am

Request Review

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

pulkitxm has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The change moves deployed services toward Node-compatible libraries, adds shared realtime and MCP packages, hosts WebSocket and MCP routes in the web app, replaces container and Kubernetes deployment with Vercel guidance, and updates storage, authentication, database, CI, and infrastructure configuration.

Changes

Runtime and platform migration

Layer / File(s) Summary
Runtime and service integrations
.env.example, CLAUDE.md, README.md, apps/web/package.json, packages/db/..., packages/services/..., packages/shared/...
Production integrations now use Node-compatible database, Redis, S3, filesystem, password, and UUID implementations.
Deployment and infrastructure
.github/workflows/ci.yml, apps/web/vercel.json, infra/*, k8s/*, Dockerfile.deps, .dockerignore, apps/*/Dockerfile
Deployment configuration now targets Vercel. Docker and Kubernetes deployment files are removed.
Validation and supporting tests
packages/services/src/markdown/*, packages/services/src/storage/round-trip.test.ts, packages/core/src/content/reaction-service.test.ts, apps/web/e2e/same-user-tabs.spec.ts
Fallback sanitization, storage documentation, reactions, and realtime-related selectors are covered by updated tests.

Shared realtime service

Layer / File(s) Summary
Realtime hub package
packages/realtime-server/*
Added socket adapters, ticket authentication, authorization, presence storage, Redis synchronization, lifecycle management, statistics, logging, and public exports.
Realtime application integration
apps/realtime/*
The Bun realtime application delegates connection handling, statistics, and shutdown to createRealtimeHub. Tests cover Node WebSocket sessions, authentication, Redis delivery, and scope authorization.
Web realtime endpoints
apps/web/src/app/api/ws/route.ts, apps/web/src/app/api/realtime/health/route.ts, apps/web/src/lib/realtime/*, apps/web/src/features/*realtime*
The web application exposes WebSocket upgrades and realtime health checks. Browser clients resolve same-origin WebSocket URLs when no explicit URL is configured.

MCP package and route

Layer / File(s) Summary
MCP transport and package boundary
packages/mcp-server/src/server.ts, packages/mcp-server/src/test-helpers.ts, packages/mcp-server/src/index.ts, packages/mcp-server/src/*test.ts, packages/mcp-server/package.json
The MCP server provides authenticated streamable HTTP handling through a reusable package and an in-process test transport.
MCP tools and domain models
packages/mcp-server/src/tools/*, packages/mcp-server/src/{comments,resolve,views}.ts
Added identity, administration, issue, planning, comment, relation, resolver, validation, publishing, and response-view functionality.
Web MCP route
apps/web/src/app/mcp/route.ts, apps/web/next.config.ts
The web application handles MCP requests through the shared request handler and configured public application URL.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant WebSocketRoute
  participant RealtimeHub
  participant Redis

  Browser->>WebSocketRoute: GET /api/ws upgrade
  WebSocketRoute->>RealtimeHub: Create or reuse hub
  WebSocketRoute->>RealtimeHub: Accept socket and buffer early messages
  RealtimeHub->>Redis: Authenticate and subscribe
  Redis-->>RealtimeHub: Delta or presence message
  RealtimeHub-->>Browser: Deliver authorized realtime update
Loading

Possibly related PRs

  • Noveum/orbit#85: Continues the EKS-to-Vercel deployment migration.
  • Noveum/orbit#9: Relates to relocating the MCP server into a reusable package and web route.
  • Noveum/orbit#24: Relates to the realtime runtime and implementation changes.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes moving the realtime and MCP services into the web app for Vercel deployment.
Description check ✅ Passed The description accurately states the migration of the WebSocket hub and MCP server into Next.js and the move away from Bun-specific runtime dependencies.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-realtime-on-vercel

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

Nothing is containerised now that the app ships as one Vercel project, and
the images job still built an mcp image whose app no longer exists, so it
could not pass. Remove the job, the Dockerfiles and the manifests.
@greptile-apps

greptile-apps Bot commented Aug 3, 2026

Copy link
Copy Markdown

Too many files changed for review. (116 files found, 100 file limit)

Bypass the limit by tagging @greptile-apps to review.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 12

🧹 Nitpick comments (10)
apps/web/src/lib/realtime/provider.tsx (1)

43-44: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the SSR-aware URL resolution into one shared hook.

The same typeof window === 'undefined' resolution also exists in apps/web/src/features/inbox/inbox-realtime.tsx and apps/web/src/features/pulls/pulls-realtime.tsx. Three copies will drift. Move the branch into a use-realtime-url.ts hook next to url.ts and call it from all three components.

♻️ Proposed refactor

Add apps/web/src/lib/realtime/use-realtime-url.ts:

'use client';

import { resolveRealtimeUrl } from './url.ts';

export function useRealtimeUrl(configured: string): string {
  if (typeof window === 'undefined') return configured;
  return resolveRealtimeUrl(configured, window.location.origin);
}

Then in provider.tsx:

-  const socketUrl =
-    typeof window === 'undefined' ? url : resolveRealtimeUrl(url, window.location.origin);
+  const socketUrl = useRealtimeUrl(url);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/lib/realtime/provider.tsx` around lines 43 - 44, Extract the
SSR-aware URL selection into a shared useRealtimeUrl hook beside url.ts,
preserving the configured URL during SSR and using resolveRealtimeUrl with
window.location.origin in the browser. Replace the duplicated typeof window
resolution in the provider, inbox realtime, and pulls realtime components with
this hook.
packages/realtime-server/src/hub.ts (1)

104-108: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider failing fast when REDIS_URL is not set.

The hub throws when BETTER_AUTH_SECRET is missing, but it silently falls back to redis://localhost:6380 when REDIS_URL is missing. The web app now creates this hub inside a serverless function, where localhost has no Redis. In that case clients connect and authenticate, and the hub then drops every delta while only logging Redis connection errors. Apply the same explicit failure, or keep the localhost default for local development only.

♻️ Proposed change
-  const redisUrl = options.redisUrl ?? process.env['REDIS_URL'] ?? 'redis://localhost:6380';
+  const configuredRedisUrl = options.redisUrl ?? process.env['REDIS_URL'];
+  if (configuredRedisUrl === undefined && process.env['NODE_ENV'] === 'production') {
+    throw new Error('REDIS_URL is required to run the realtime hub.');
+  }
+  const redisUrl = configuredRedisUrl ?? 'redis://localhost:6380';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/realtime-server/src/hub.ts` around lines 104 - 108, Update the Redis
configuration in the hub initialization to avoid silently using
redis://localhost:6380 when REDIS_URL is absent in serverless deployments. Apply
the same required-configuration validation used for ticketSecret to redisUrl, or
conditionally retain the localhost fallback only for explicit local-development
environments, while preserving configured REDIS_URL behavior.
apps/web/src/features/inbox/inbox-realtime.tsx (1)

28-33: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract the browser realtime URL resolution into one helper. Both components repeat the same typeof window === 'undefined' branch around resolveRealtimeUrl. A third copy exists in apps/web/src/lib/realtime/provider.tsx. Add one use-realtime-url.ts hook, or export a browserRealtimeUrl(configured) helper from apps/web/src/lib/realtime/url.ts, and call it from every site.

  • apps/web/src/features/inbox/inbox-realtime.tsx#L28-L33: replace the inline branch with the shared helper and keep passing the result to RealtimeProvider.
  • apps/web/src/features/pulls/pulls-realtime.tsx#L19-L24: replace the identical inline branch with the same shared helper.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/features/inbox/inbox-realtime.tsx` around lines 28 - 33, Extract
the repeated browser/SSR realtime URL selection into one shared helper, such as
browserRealtimeUrl in the realtime URL module, and update the RealtimeProvider
setup in apps/web/src/features/inbox/inbox-realtime.tsx lines 28-33 and
apps/web/src/features/pulls/pulls-realtime.tsx lines 19-24 to call it. Also
replace the existing equivalent branch in apps/web/src/lib/realtime/provider.tsx
with the same helper, preserving configured URLs during SSR and
resolveRealtimeUrl behavior in the browser.
packages/realtime-server/tsconfig.json (1)

3-6: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Ban accidental Bun globals in non-test source.

src/socket.ts requires the explicit ServerWebSocket type import for fromBunSocket, but the package source must not add Bun runtime dependencies. Add a lint rule for Bun references in non-test files, or isolate the Bun adapter from the source compiled for web imports.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/realtime-server/tsconfig.json` around lines 3 - 6, Update the
realtime-server TypeScript/lint configuration around the compilerOptions and
include entries to prevent Bun globals from being used in non-test source files,
while preserving the explicit ServerWebSocket type import required by
fromBunSocket. Add a rule scoped to non-test source, or isolate the Bun adapter
from code compiled for web imports without introducing Bun runtime dependencies.

Source: Coding guidelines

packages/mcp-server/src/logger.ts (1)

26-28: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Include the stack trace in errorFields.

errorFields keeps only error.message. Add error.stack when available. This helps diagnose crashes from the structured logs in server.ts and tools/support.ts without needing to reproduce the failure locally.

♻️ Proposed refactor
 export function errorFields(error: unknown): LogFields {
-  return { error: error instanceof Error ? error.message : String(error) };
+  return {
+    error: error instanceof Error ? error.message : String(error),
+    ...(error instanceof Error && error.stack ? { stack: error.stack } : {}),
+  };
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mcp-server/src/logger.ts` around lines 26 - 28, The errorFields
function currently omits available stack traces from structured error logs.
Update errorFields to include error.stack when the input is an Error with a
stack, while preserving the existing message and String(error) handling for all
other cases.
packages/mcp-server/src/resolve.ts (1)

31-36: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Consider archived teams in resolveTeam.

resolveTeam calls listTeams(principal) with default options, so archived teams are excluded. resolveProject at Line 77 passes includeArchived: true. The list_teams tool also exposes archived teams. A caller that reads an archived team from list_teams cannot then pass that team to list_states or search_issues. Align the two resolvers, or document the exclusion in the tool descriptions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mcp-server/src/resolve.ts` around lines 31 - 36, Update resolveTeam
to call listTeams with includeArchived: true, matching resolveProject and the
list_teams tool so archived team references resolve for downstream operations.
packages/mcp-server/src/views.ts (1)

74-84: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Load workflow states for all teams in parallel.

The loop awaits listWorkflowStates once per team, in sequence. search_issues returns up to 200 issues, which can span many teams for an admin principal. Each round trip adds latency to the tool response.

♻️ Proposed change
   const stateNames = new Map<string, string>();
-  for (const teamId of new Set(rows.map((row) => row.teamId))) {
-    for (const state of await listWorkflowStates(principal, teamId)) {
-      stateNames.set(state.id, state.name);
-    }
-  }
+  const teamIds = [...new Set(rows.map((row) => row.teamId))];
+  const stateLists = await Promise.all(
+    teamIds.map((teamId) => listWorkflowStates(principal, teamId)),
+  );
+  for (const states of stateLists) {
+    for (const state of states) stateNames.set(state.id, state.name);
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mcp-server/src/views.ts` around lines 74 - 84, Update the
workflow-state loading loop around listWorkflowStates to start requests for all
unique team IDs concurrently and await them together, then populate stateNames
from the aggregated results while preserving the existing state ID-to-name
mapping.
packages/mcp-server/src/tools/planning.ts (1)

88-89: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Resolve team references without one query per reference.

The loop awaits resolveTeam once per element, and resolveTeam runs a full listTeams query on every call. The teams array accepts up to 50 entries, so one create_project call can issue 50 sequential team queries. The same pattern exists in packages/mcp-server/src/tools/admin.ts at Lines 51-52 for invite_member.

Add a batch resolver in packages/mcp-server/src/resolve.ts that loads the team list once and resolves every reference against it, then use it in both tools.

♻️ Proposed batch resolver
// packages/mcp-server/src/resolve.ts
export async function resolveTeamIds(
  principal: Principal,
  refs: readonly string[],
): Promise<string[]> {
  if (refs.length === 0) return [];
  const teams = await listTeams(principal);
  return refs.map((ref) => {
    const found = pick(teams, ref, (team) => [team.id, team.key, team.name]);
    if (found === undefined) throw notFound(`No team matches "${ref}".`);
    return found.id;
  });
}
-      const teamIds: string[] = [];
-      for (const ref of args.teams ?? []) teamIds.push((await resolveTeam(principal, ref)).id);
+      const teamIds = await resolveTeamIds(principal, args.teams ?? []);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mcp-server/src/tools/planning.ts` around lines 88 - 89, Issue: Team
references are resolved with one full team-list query per reference. Add a
resolveTeamIds function in resolve.ts that returns early for empty refs, loads
listTeams once, matches each reference against team id, key, or name using the
existing pick/notFound behavior, and returns the matched IDs; replace the
per-reference resolveTeam loops in create_project planning.ts and invite_member
admin.ts with this batch resolver.
packages/mcp-server/src/tools/admin.ts (1)

45-45: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use z.email() for the email schema. Zod 4.4.3 deprecates z.string().email() in favor of the top-level format schema.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mcp-server/src/tools/admin.ts` at line 45, Update the email field
schema in the admin invite tool to use Zod’s top-level z.email() format schema
instead of z.string().email(), while preserving its existing description and
validation behavior.
packages/mcp-server/src/tools/support.ts (1)

59-60: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the redundant double assertion.

z.object(config.inputSchema) is assignable to z.ZodObject<Shape> with Zod 4.4.3. Pass it directly. In SDK 1.29.0, registerTool orders generics as <OutputArgs, InputArgs>; the first z.ZodRawShape is the output generic.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mcp-server/src/tools/support.ts` around lines 59 - 60, Remove the
redundant double assertion from the inputSchema initialization and pass
z.object(config.inputSchema) directly as the z.ZodObject<Shape> value. Preserve
the registerTool generic ordering, where z.ZodRawShape is the output type and
z.ZodObject<Shape> is the input type.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/app/`(app)/layout.tsx:
- Around line 15-16: Update realtimeUrl() to validate the configured realtime
URL with a Zod schema, accepting only non-empty ws: or wss: URLs while
preserving '' for the same-origin fallback. Ensure malformed or unsupported
values are rejected before they reach new WebSocket(options.url), using the
existing configuredRealtimeUrl() source.

In `@apps/web/src/app/api/realtime/health/route.ts`:
- Around line 35-38: Update the catch block in the health route to stop
returning describe(error) in the public response; use a generic hub error value
instead, and log the original error server-side while preserving the 503
response and other fields.

In `@apps/web/src/app/api/ws/route.ts`:
- Around line 11-42: Bound the pre-accept message queue in attach by enforcing a
maximum buffered-frame count before pushing new messages. When the cap is
exceeded, close the socket using the established overload/limit close behavior
and stop buffering or processing further frames; preserve normal buffering and
replay once realtimeHub resolves.

In `@apps/web/src/app/mcp/route.ts`:
- Around line 1-9: Update the handle function’s handleMcpRequest options to use
the centralized publicAppUrl() helper instead of reading NEXT_PUBLIC_APP_URL
through serverEnv(), preserving centralized URL normalization and the existing
request handling flow.

In `@apps/web/src/lib/auth/password.ts`:
- Around line 16-22: Update verifyPassword so it explicitly validates the
digest’s PHC format and returns false only for malformed input or an invalid
password result. Remove the catch-all error suppression around verify, allowing
unrelated Argon2/runtime failures to propagate; use the existing verifyPassword
symbol and the Argon2 API’s expected digest structure for validation.

In `@apps/web/src/lib/realtime/hub.ts`:
- Around line 5-11: Update realtimeHub so a rejection from the cached
createRealtimeHub promise removes globalForHub.orbitRealtimeHub before
propagating the error, allowing subsequent calls to retry creation while
preserving the existing promise reuse on success.

In `@apps/web/src/lib/realtime/url.ts`:
- Around line 3-5: Update configuredRealtimeUrl to use static
process.env.NEXT_PUBLIC_REALTIME_URL access, then validate the configured value
with Zod and return it only when it parses as a WebSocket URL with a ws: or wss:
origin; otherwise preserve the existing empty-string fallback before the value
reaches RealtimeProvider.

In `@CLAUDE.md`:
- Around line 57-60: Align the deployment and runtime documentation across
CLAUDE.md lines 57-60, CLAUDE.md lines 27-28, and README.md line 27: remove or
rewrite standalone Bun container/realtime/MCP service instructions to describe
the single Node-based Vercel app, replace Bun.password guidance with
`@node-rs/argon2`, and update the end-to-end runtime description to
Node-compatible integrations. Also ensure shipped web server code does not
import or call Bun built-ins.

In `@packages/mcp-server/src/comments.ts`:
- Around line 35-47: In the comment-creation transaction around the insert in
add_comment, validate any non-empty parsed.parentId by querying the parent
comment and requiring matching principal.organizationId and issue.id; reject a
missing parent with notFound and reject a parent belonging to another
organization or issue. Only insert after this validation, preserving null/empty
parent behavior.

In `@packages/mcp-server/src/tools/identity.ts`:
- Around line 82-104: The member-directory permission is missing, allowing
unauthorized email access through listMembers consumers. Define the intended
directory-read permission in the shared policy, enforce it within listMembers
before returning member data, and ensure list_users, list_members, and web
member endpoints use that protected path while preserving existing role
permissions.

In `@packages/mcp-server/src/tools/support.ts`:
- Around line 29-40: Update failed to stop spreading errorFields(error) into
logger.warn; log only the tool name, domain.code, and domain.status. Preserve
the existing response body and error handling behavior while ensuring
caller-supplied messages and identifiers are not written to logs.

In `@packages/mcp-server/src/views.ts`:
- Around line 89-93: Update describeIssue to throw the typed internal domain
error from `@orbit/shared/errors` instead of a bare Error when describeIssues
returns no view. Follow the existing internal(...) usage in comments.ts and
preserve the current message and control flow.

---

Nitpick comments:
In `@apps/web/src/features/inbox/inbox-realtime.tsx`:
- Around line 28-33: Extract the repeated browser/SSR realtime URL selection
into one shared helper, such as browserRealtimeUrl in the realtime URL module,
and update the RealtimeProvider setup in
apps/web/src/features/inbox/inbox-realtime.tsx lines 28-33 and
apps/web/src/features/pulls/pulls-realtime.tsx lines 19-24 to call it. Also
replace the existing equivalent branch in apps/web/src/lib/realtime/provider.tsx
with the same helper, preserving configured URLs during SSR and
resolveRealtimeUrl behavior in the browser.

In `@apps/web/src/lib/realtime/provider.tsx`:
- Around line 43-44: Extract the SSR-aware URL selection into a shared
useRealtimeUrl hook beside url.ts, preserving the configured URL during SSR and
using resolveRealtimeUrl with window.location.origin in the browser. Replace the
duplicated typeof window resolution in the provider, inbox realtime, and pulls
realtime components with this hook.

In `@packages/mcp-server/src/logger.ts`:
- Around line 26-28: The errorFields function currently omits available stack
traces from structured error logs. Update errorFields to include error.stack
when the input is an Error with a stack, while preserving the existing message
and String(error) handling for all other cases.

In `@packages/mcp-server/src/resolve.ts`:
- Around line 31-36: Update resolveTeam to call listTeams with includeArchived:
true, matching resolveProject and the list_teams tool so archived team
references resolve for downstream operations.

In `@packages/mcp-server/src/tools/admin.ts`:
- Line 45: Update the email field schema in the admin invite tool to use Zod’s
top-level z.email() format schema instead of z.string().email(), while
preserving its existing description and validation behavior.

In `@packages/mcp-server/src/tools/planning.ts`:
- Around line 88-89: Issue: Team references are resolved with one full team-list
query per reference. Add a resolveTeamIds function in resolve.ts that returns
early for empty refs, loads listTeams once, matches each reference against team
id, key, or name using the existing pick/notFound behavior, and returns the
matched IDs; replace the per-reference resolveTeam loops in create_project
planning.ts and invite_member admin.ts with this batch resolver.

In `@packages/mcp-server/src/tools/support.ts`:
- Around line 59-60: Remove the redundant double assertion from the inputSchema
initialization and pass z.object(config.inputSchema) directly as the
z.ZodObject<Shape> value. Preserve the registerTool generic ordering, where
z.ZodRawShape is the output type and z.ZodObject<Shape> is the input type.

In `@packages/mcp-server/src/views.ts`:
- Around line 74-84: Update the workflow-state loading loop around
listWorkflowStates to start requests for all unique team IDs concurrently and
await them together, then populate stateNames from the aggregated results while
preserving the existing state ID-to-name mapping.

In `@packages/realtime-server/src/hub.ts`:
- Around line 104-108: Update the Redis configuration in the hub initialization
to avoid silently using redis://localhost:6380 when REDIS_URL is absent in
serverless deployments. Apply the same required-configuration validation used
for ticketSecret to redisUrl, or conditionally retain the localhost fallback
only for explicit local-development environments, while preserving configured
REDIS_URL behavior.

In `@packages/realtime-server/tsconfig.json`:
- Around line 3-6: Update the realtime-server TypeScript/lint configuration
around the compilerOptions and include entries to prevent Bun globals from being
used in non-test source files, while preserving the explicit ServerWebSocket
type import required by fromBunSocket. Add a rule scoped to non-test source, or
isolate the Bun adapter from code compiled for web imports without introducing
Bun runtime dependencies.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4b53e228-e1d7-4ebf-be4b-829cddc0c7c0

📥 Commits

Reviewing files that changed from the base of the PR and between a57e3a8 and 693bfa3.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (96)
  • .env.example
  • .github/workflows/ci.yml
  • CLAUDE.md
  • README.md
  • apps/mcp/Dockerfile
  • apps/mcp/README.md
  • apps/mcp/src/env.ts
  • apps/mcp/src/index.ts
  • apps/mcp/src/server.ts
  • apps/realtime/package.json
  • apps/realtime/src/active-organization.test.ts
  • apps/realtime/src/client-reconnect.test.ts
  • apps/realtime/src/index.ts
  • apps/realtime/src/membership-revocation.test.ts
  • apps/realtime/src/node-socket.test.ts
  • apps/realtime/src/server.test.ts
  • apps/realtime/src/server.ts
  • apps/realtime/src/session-revocation.test.ts
  • apps/realtime/src/test-helpers.ts
  • apps/web/next.config.ts
  • apps/web/package.json
  • apps/web/src/app/(app)/inbox/page.tsx
  • apps/web/src/app/(app)/layout.tsx
  • apps/web/src/app/(app)/pulls/page.tsx
  • apps/web/src/app/api/integrations/github/route.ts
  • apps/web/src/app/api/realtime/health/route.ts
  • apps/web/src/app/api/webhooks/github/route.ts
  • apps/web/src/app/api/ws/route.ts
  • apps/web/src/app/mcp/route.ts
  • apps/web/src/features/inbox/inbox-realtime.tsx
  • apps/web/src/features/pulls/pulls-realtime.tsx
  • apps/web/src/lib/auth/password.test.ts
  • apps/web/src/lib/auth/password.ts
  • apps/web/src/lib/auth/server.ts
  • apps/web/src/lib/integrations/oauth-state.ts
  • apps/web/src/lib/realtime/hub.ts
  • apps/web/src/lib/realtime/provider.tsx
  • apps/web/src/lib/realtime/url.test.ts
  • apps/web/src/lib/realtime/url.ts
  • apps/web/vercel.json
  • packages/core/package.json
  • packages/core/src/analytics/burndown.ts
  • packages/core/src/realtime/publisher.ts
  • packages/db/package.json
  • packages/db/src/client.ts
  • packages/db/src/ensure-extensions.ts
  • packages/mcp-server/bunfig.toml
  • packages/mcp-server/package.json
  • packages/mcp-server/src/auth.test.ts
  • packages/mcp-server/src/comments.ts
  • packages/mcp-server/src/index.ts
  • packages/mcp-server/src/logger.ts
  • packages/mcp-server/src/resolve.ts
  • packages/mcp-server/src/server.ts
  • packages/mcp-server/src/test-helpers.ts
  • packages/mcp-server/src/tools.test.ts
  • packages/mcp-server/src/tools/admin.ts
  • packages/mcp-server/src/tools/identity.ts
  • packages/mcp-server/src/tools/index.ts
  • packages/mcp-server/src/tools/issues.ts
  • packages/mcp-server/src/tools/planning.ts
  • packages/mcp-server/src/tools/support.ts
  • packages/mcp-server/src/views.ts
  • packages/mcp-server/tests-preload.ts
  • packages/mcp-server/tsconfig.json
  • packages/realtime-server/bunfig.toml
  • packages/realtime-server/package.json
  • packages/realtime-server/src/auth.test.ts
  • packages/realtime-server/src/auth.ts
  • packages/realtime-server/src/connection.test.ts
  • packages/realtime-server/src/connection.ts
  • packages/realtime-server/src/hub.ts
  • packages/realtime-server/src/index.ts
  • packages/realtime-server/src/logger.ts
  • packages/realtime-server/src/presence.ts
  • packages/realtime-server/src/socket.ts
  • packages/realtime-server/tests-preload.ts
  • packages/realtime-server/tsconfig.json
  • packages/services/package.json
  • packages/services/src/email/email.test.ts
  • packages/services/src/email/index.ts
  • packages/services/src/github/apply.test.ts
  • packages/services/src/github/apply.ts
  • packages/services/src/github/install.test.ts
  • packages/services/src/github/install.ts
  • packages/services/src/notifications/index.ts
  • packages/services/src/notifications/notifications.test.ts
  • packages/services/src/slack/dispatch.test.ts
  • packages/services/src/slack/dispatch.ts
  • packages/services/src/storage/credentials.test.ts
  • packages/services/src/storage/credentials.ts
  • packages/services/src/storage/parent.test.ts
  • packages/services/src/storage/s3.ts
  • packages/services/src/storage/validate.ts
  • packages/shared/package.json
  • packages/shared/src/utils/index.ts
💤 Files with no reviewable changes (6)
  • apps/mcp/src/env.ts
  • .env.example
  • apps/mcp/README.md
  • apps/mcp/src/server.ts
  • apps/mcp/Dockerfile
  • apps/mcp/src/index.ts

Comment thread apps/web/src/app/(app)/layout.tsx
Comment thread apps/web/src/app/api/realtime/health/route.ts
Comment thread apps/web/src/app/api/ws/route.ts
Comment thread apps/web/src/app/mcp/route.ts
Comment thread apps/web/src/lib/auth/password.ts
Comment thread apps/web/src/lib/realtime/hub.ts
Comment thread apps/web/src/lib/realtime/url.ts
Comment thread CLAUDE.md

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 12

🧹 Nitpick comments (10)
apps/web/src/lib/realtime/provider.tsx (1)

43-44: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the SSR-aware URL resolution into one shared hook.

The same typeof window === 'undefined' resolution also exists in apps/web/src/features/inbox/inbox-realtime.tsx and apps/web/src/features/pulls/pulls-realtime.tsx. Three copies will drift. Move the branch into a use-realtime-url.ts hook next to url.ts and call it from all three components.

♻️ Proposed refactor

Add apps/web/src/lib/realtime/use-realtime-url.ts:

'use client';

import { resolveRealtimeUrl } from './url.ts';

export function useRealtimeUrl(configured: string): string {
  if (typeof window === 'undefined') return configured;
  return resolveRealtimeUrl(configured, window.location.origin);
}

Then in provider.tsx:

-  const socketUrl =
-    typeof window === 'undefined' ? url : resolveRealtimeUrl(url, window.location.origin);
+  const socketUrl = useRealtimeUrl(url);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/lib/realtime/provider.tsx` around lines 43 - 44, Extract the
SSR-aware URL selection into a shared useRealtimeUrl hook beside url.ts,
preserving the configured URL during SSR and using resolveRealtimeUrl with
window.location.origin in the browser. Replace the duplicated typeof window
resolution in the provider, inbox realtime, and pulls realtime components with
this hook.
packages/realtime-server/src/hub.ts (1)

104-108: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider failing fast when REDIS_URL is not set.

The hub throws when BETTER_AUTH_SECRET is missing, but it silently falls back to redis://localhost:6380 when REDIS_URL is missing. The web app now creates this hub inside a serverless function, where localhost has no Redis. In that case clients connect and authenticate, and the hub then drops every delta while only logging Redis connection errors. Apply the same explicit failure, or keep the localhost default for local development only.

♻️ Proposed change
-  const redisUrl = options.redisUrl ?? process.env['REDIS_URL'] ?? 'redis://localhost:6380';
+  const configuredRedisUrl = options.redisUrl ?? process.env['REDIS_URL'];
+  if (configuredRedisUrl === undefined && process.env['NODE_ENV'] === 'production') {
+    throw new Error('REDIS_URL is required to run the realtime hub.');
+  }
+  const redisUrl = configuredRedisUrl ?? 'redis://localhost:6380';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/realtime-server/src/hub.ts` around lines 104 - 108, Update the Redis
configuration in the hub initialization to avoid silently using
redis://localhost:6380 when REDIS_URL is absent in serverless deployments. Apply
the same required-configuration validation used for ticketSecret to redisUrl, or
conditionally retain the localhost fallback only for explicit local-development
environments, while preserving configured REDIS_URL behavior.
apps/web/src/features/inbox/inbox-realtime.tsx (1)

28-33: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract the browser realtime URL resolution into one helper. Both components repeat the same typeof window === 'undefined' branch around resolveRealtimeUrl. A third copy exists in apps/web/src/lib/realtime/provider.tsx. Add one use-realtime-url.ts hook, or export a browserRealtimeUrl(configured) helper from apps/web/src/lib/realtime/url.ts, and call it from every site.

  • apps/web/src/features/inbox/inbox-realtime.tsx#L28-L33: replace the inline branch with the shared helper and keep passing the result to RealtimeProvider.
  • apps/web/src/features/pulls/pulls-realtime.tsx#L19-L24: replace the identical inline branch with the same shared helper.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/features/inbox/inbox-realtime.tsx` around lines 28 - 33, Extract
the repeated browser/SSR realtime URL selection into one shared helper, such as
browserRealtimeUrl in the realtime URL module, and update the RealtimeProvider
setup in apps/web/src/features/inbox/inbox-realtime.tsx lines 28-33 and
apps/web/src/features/pulls/pulls-realtime.tsx lines 19-24 to call it. Also
replace the existing equivalent branch in apps/web/src/lib/realtime/provider.tsx
with the same helper, preserving configured URLs during SSR and
resolveRealtimeUrl behavior in the browser.
packages/realtime-server/tsconfig.json (1)

3-6: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Ban accidental Bun globals in non-test source.

src/socket.ts requires the explicit ServerWebSocket type import for fromBunSocket, but the package source must not add Bun runtime dependencies. Add a lint rule for Bun references in non-test files, or isolate the Bun adapter from the source compiled for web imports.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/realtime-server/tsconfig.json` around lines 3 - 6, Update the
realtime-server TypeScript/lint configuration around the compilerOptions and
include entries to prevent Bun globals from being used in non-test source files,
while preserving the explicit ServerWebSocket type import required by
fromBunSocket. Add a rule scoped to non-test source, or isolate the Bun adapter
from code compiled for web imports without introducing Bun runtime dependencies.

Source: Coding guidelines

packages/mcp-server/src/logger.ts (1)

26-28: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Include the stack trace in errorFields.

errorFields keeps only error.message. Add error.stack when available. This helps diagnose crashes from the structured logs in server.ts and tools/support.ts without needing to reproduce the failure locally.

♻️ Proposed refactor
 export function errorFields(error: unknown): LogFields {
-  return { error: error instanceof Error ? error.message : String(error) };
+  return {
+    error: error instanceof Error ? error.message : String(error),
+    ...(error instanceof Error && error.stack ? { stack: error.stack } : {}),
+  };
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mcp-server/src/logger.ts` around lines 26 - 28, The errorFields
function currently omits available stack traces from structured error logs.
Update errorFields to include error.stack when the input is an Error with a
stack, while preserving the existing message and String(error) handling for all
other cases.
packages/mcp-server/src/resolve.ts (1)

31-36: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Consider archived teams in resolveTeam.

resolveTeam calls listTeams(principal) with default options, so archived teams are excluded. resolveProject at Line 77 passes includeArchived: true. The list_teams tool also exposes archived teams. A caller that reads an archived team from list_teams cannot then pass that team to list_states or search_issues. Align the two resolvers, or document the exclusion in the tool descriptions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mcp-server/src/resolve.ts` around lines 31 - 36, Update resolveTeam
to call listTeams with includeArchived: true, matching resolveProject and the
list_teams tool so archived team references resolve for downstream operations.
packages/mcp-server/src/views.ts (1)

74-84: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Load workflow states for all teams in parallel.

The loop awaits listWorkflowStates once per team, in sequence. search_issues returns up to 200 issues, which can span many teams for an admin principal. Each round trip adds latency to the tool response.

♻️ Proposed change
   const stateNames = new Map<string, string>();
-  for (const teamId of new Set(rows.map((row) => row.teamId))) {
-    for (const state of await listWorkflowStates(principal, teamId)) {
-      stateNames.set(state.id, state.name);
-    }
-  }
+  const teamIds = [...new Set(rows.map((row) => row.teamId))];
+  const stateLists = await Promise.all(
+    teamIds.map((teamId) => listWorkflowStates(principal, teamId)),
+  );
+  for (const states of stateLists) {
+    for (const state of states) stateNames.set(state.id, state.name);
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mcp-server/src/views.ts` around lines 74 - 84, Update the
workflow-state loading loop around listWorkflowStates to start requests for all
unique team IDs concurrently and await them together, then populate stateNames
from the aggregated results while preserving the existing state ID-to-name
mapping.
packages/mcp-server/src/tools/planning.ts (1)

88-89: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Resolve team references without one query per reference.

The loop awaits resolveTeam once per element, and resolveTeam runs a full listTeams query on every call. The teams array accepts up to 50 entries, so one create_project call can issue 50 sequential team queries. The same pattern exists in packages/mcp-server/src/tools/admin.ts at Lines 51-52 for invite_member.

Add a batch resolver in packages/mcp-server/src/resolve.ts that loads the team list once and resolves every reference against it, then use it in both tools.

♻️ Proposed batch resolver
// packages/mcp-server/src/resolve.ts
export async function resolveTeamIds(
  principal: Principal,
  refs: readonly string[],
): Promise<string[]> {
  if (refs.length === 0) return [];
  const teams = await listTeams(principal);
  return refs.map((ref) => {
    const found = pick(teams, ref, (team) => [team.id, team.key, team.name]);
    if (found === undefined) throw notFound(`No team matches "${ref}".`);
    return found.id;
  });
}
-      const teamIds: string[] = [];
-      for (const ref of args.teams ?? []) teamIds.push((await resolveTeam(principal, ref)).id);
+      const teamIds = await resolveTeamIds(principal, args.teams ?? []);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mcp-server/src/tools/planning.ts` around lines 88 - 89, Issue: Team
references are resolved with one full team-list query per reference. Add a
resolveTeamIds function in resolve.ts that returns early for empty refs, loads
listTeams once, matches each reference against team id, key, or name using the
existing pick/notFound behavior, and returns the matched IDs; replace the
per-reference resolveTeam loops in create_project planning.ts and invite_member
admin.ts with this batch resolver.
packages/mcp-server/src/tools/admin.ts (1)

45-45: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use z.email() for the email schema. Zod 4.4.3 deprecates z.string().email() in favor of the top-level format schema.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mcp-server/src/tools/admin.ts` at line 45, Update the email field
schema in the admin invite tool to use Zod’s top-level z.email() format schema
instead of z.string().email(), while preserving its existing description and
validation behavior.
packages/mcp-server/src/tools/support.ts (1)

59-60: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the redundant double assertion.

z.object(config.inputSchema) is assignable to z.ZodObject<Shape> with Zod 4.4.3. Pass it directly. In SDK 1.29.0, registerTool orders generics as <OutputArgs, InputArgs>; the first z.ZodRawShape is the output generic.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mcp-server/src/tools/support.ts` around lines 59 - 60, Remove the
redundant double assertion from the inputSchema initialization and pass
z.object(config.inputSchema) directly as the z.ZodObject<Shape> value. Preserve
the registerTool generic ordering, where z.ZodRawShape is the output type and
z.ZodObject<Shape> is the input type.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/app/`(app)/layout.tsx:
- Around line 15-16: Update realtimeUrl() to validate the configured realtime
URL with a Zod schema, accepting only non-empty ws: or wss: URLs while
preserving '' for the same-origin fallback. Ensure malformed or unsupported
values are rejected before they reach new WebSocket(options.url), using the
existing configuredRealtimeUrl() source.

In `@apps/web/src/app/api/realtime/health/route.ts`:
- Around line 35-38: Update the catch block in the health route to stop
returning describe(error) in the public response; use a generic hub error value
instead, and log the original error server-side while preserving the 503
response and other fields.

In `@apps/web/src/app/api/ws/route.ts`:
- Around line 11-42: Bound the pre-accept message queue in attach by enforcing a
maximum buffered-frame count before pushing new messages. When the cap is
exceeded, close the socket using the established overload/limit close behavior
and stop buffering or processing further frames; preserve normal buffering and
replay once realtimeHub resolves.

In `@apps/web/src/app/mcp/route.ts`:
- Around line 1-9: Update the handle function’s handleMcpRequest options to use
the centralized publicAppUrl() helper instead of reading NEXT_PUBLIC_APP_URL
through serverEnv(), preserving centralized URL normalization and the existing
request handling flow.

In `@apps/web/src/lib/auth/password.ts`:
- Around line 16-22: Update verifyPassword so it explicitly validates the
digest’s PHC format and returns false only for malformed input or an invalid
password result. Remove the catch-all error suppression around verify, allowing
unrelated Argon2/runtime failures to propagate; use the existing verifyPassword
symbol and the Argon2 API’s expected digest structure for validation.

In `@apps/web/src/lib/realtime/hub.ts`:
- Around line 5-11: Update realtimeHub so a rejection from the cached
createRealtimeHub promise removes globalForHub.orbitRealtimeHub before
propagating the error, allowing subsequent calls to retry creation while
preserving the existing promise reuse on success.

In `@apps/web/src/lib/realtime/url.ts`:
- Around line 3-5: Update configuredRealtimeUrl to use static
process.env.NEXT_PUBLIC_REALTIME_URL access, then validate the configured value
with Zod and return it only when it parses as a WebSocket URL with a ws: or wss:
origin; otherwise preserve the existing empty-string fallback before the value
reaches RealtimeProvider.

In `@CLAUDE.md`:
- Around line 57-60: Align the deployment and runtime documentation across
CLAUDE.md lines 57-60, CLAUDE.md lines 27-28, and README.md line 27: remove or
rewrite standalone Bun container/realtime/MCP service instructions to describe
the single Node-based Vercel app, replace Bun.password guidance with
`@node-rs/argon2`, and update the end-to-end runtime description to
Node-compatible integrations. Also ensure shipped web server code does not
import or call Bun built-ins.

In `@packages/mcp-server/src/comments.ts`:
- Around line 35-47: In the comment-creation transaction around the insert in
add_comment, validate any non-empty parsed.parentId by querying the parent
comment and requiring matching principal.organizationId and issue.id; reject a
missing parent with notFound and reject a parent belonging to another
organization or issue. Only insert after this validation, preserving null/empty
parent behavior.

In `@packages/mcp-server/src/tools/identity.ts`:
- Around line 82-104: The member-directory permission is missing, allowing
unauthorized email access through listMembers consumers. Define the intended
directory-read permission in the shared policy, enforce it within listMembers
before returning member data, and ensure list_users, list_members, and web
member endpoints use that protected path while preserving existing role
permissions.

In `@packages/mcp-server/src/tools/support.ts`:
- Around line 29-40: Update failed to stop spreading errorFields(error) into
logger.warn; log only the tool name, domain.code, and domain.status. Preserve
the existing response body and error handling behavior while ensuring
caller-supplied messages and identifiers are not written to logs.

In `@packages/mcp-server/src/views.ts`:
- Around line 89-93: Update describeIssue to throw the typed internal domain
error from `@orbit/shared/errors` instead of a bare Error when describeIssues
returns no view. Follow the existing internal(...) usage in comments.ts and
preserve the current message and control flow.

---

Nitpick comments:
In `@apps/web/src/features/inbox/inbox-realtime.tsx`:
- Around line 28-33: Extract the repeated browser/SSR realtime URL selection
into one shared helper, such as browserRealtimeUrl in the realtime URL module,
and update the RealtimeProvider setup in
apps/web/src/features/inbox/inbox-realtime.tsx lines 28-33 and
apps/web/src/features/pulls/pulls-realtime.tsx lines 19-24 to call it. Also
replace the existing equivalent branch in apps/web/src/lib/realtime/provider.tsx
with the same helper, preserving configured URLs during SSR and
resolveRealtimeUrl behavior in the browser.

In `@apps/web/src/lib/realtime/provider.tsx`:
- Around line 43-44: Extract the SSR-aware URL selection into a shared
useRealtimeUrl hook beside url.ts, preserving the configured URL during SSR and
using resolveRealtimeUrl with window.location.origin in the browser. Replace the
duplicated typeof window resolution in the provider, inbox realtime, and pulls
realtime components with this hook.

In `@packages/mcp-server/src/logger.ts`:
- Around line 26-28: The errorFields function currently omits available stack
traces from structured error logs. Update errorFields to include error.stack
when the input is an Error with a stack, while preserving the existing message
and String(error) handling for all other cases.

In `@packages/mcp-server/src/resolve.ts`:
- Around line 31-36: Update resolveTeam to call listTeams with includeArchived:
true, matching resolveProject and the list_teams tool so archived team
references resolve for downstream operations.

In `@packages/mcp-server/src/tools/admin.ts`:
- Line 45: Update the email field schema in the admin invite tool to use Zod’s
top-level z.email() format schema instead of z.string().email(), while
preserving its existing description and validation behavior.

In `@packages/mcp-server/src/tools/planning.ts`:
- Around line 88-89: Issue: Team references are resolved with one full team-list
query per reference. Add a resolveTeamIds function in resolve.ts that returns
early for empty refs, loads listTeams once, matches each reference against team
id, key, or name using the existing pick/notFound behavior, and returns the
matched IDs; replace the per-reference resolveTeam loops in create_project
planning.ts and invite_member admin.ts with this batch resolver.

In `@packages/mcp-server/src/tools/support.ts`:
- Around line 59-60: Remove the redundant double assertion from the inputSchema
initialization and pass z.object(config.inputSchema) directly as the
z.ZodObject<Shape> value. Preserve the registerTool generic ordering, where
z.ZodRawShape is the output type and z.ZodObject<Shape> is the input type.

In `@packages/mcp-server/src/views.ts`:
- Around line 74-84: Update the workflow-state loading loop around
listWorkflowStates to start requests for all unique team IDs concurrently and
await them together, then populate stateNames from the aggregated results while
preserving the existing state ID-to-name mapping.

In `@packages/realtime-server/src/hub.ts`:
- Around line 104-108: Update the Redis configuration in the hub initialization
to avoid silently using redis://localhost:6380 when REDIS_URL is absent in
serverless deployments. Apply the same required-configuration validation used
for ticketSecret to redisUrl, or conditionally retain the localhost fallback
only for explicit local-development environments, while preserving configured
REDIS_URL behavior.

In `@packages/realtime-server/tsconfig.json`:
- Around line 3-6: Update the realtime-server TypeScript/lint configuration
around the compilerOptions and include entries to prevent Bun globals from being
used in non-test source files, while preserving the explicit ServerWebSocket
type import required by fromBunSocket. Add a rule scoped to non-test source, or
isolate the Bun adapter from code compiled for web imports without introducing
Bun runtime dependencies.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4b53e228-e1d7-4ebf-be4b-829cddc0c7c0

📥 Commits

Reviewing files that changed from the base of the PR and between a57e3a8 and 693bfa3.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (96)
  • .env.example
  • .github/workflows/ci.yml
  • CLAUDE.md
  • README.md
  • apps/mcp/Dockerfile
  • apps/mcp/README.md
  • apps/mcp/src/env.ts
  • apps/mcp/src/index.ts
  • apps/mcp/src/server.ts
  • apps/realtime/package.json
  • apps/realtime/src/active-organization.test.ts
  • apps/realtime/src/client-reconnect.test.ts
  • apps/realtime/src/index.ts
  • apps/realtime/src/membership-revocation.test.ts
  • apps/realtime/src/node-socket.test.ts
  • apps/realtime/src/server.test.ts
  • apps/realtime/src/server.ts
  • apps/realtime/src/session-revocation.test.ts
  • apps/realtime/src/test-helpers.ts
  • apps/web/next.config.ts
  • apps/web/package.json
  • apps/web/src/app/(app)/inbox/page.tsx
  • apps/web/src/app/(app)/layout.tsx
  • apps/web/src/app/(app)/pulls/page.tsx
  • apps/web/src/app/api/integrations/github/route.ts
  • apps/web/src/app/api/realtime/health/route.ts
  • apps/web/src/app/api/webhooks/github/route.ts
  • apps/web/src/app/api/ws/route.ts
  • apps/web/src/app/mcp/route.ts
  • apps/web/src/features/inbox/inbox-realtime.tsx
  • apps/web/src/features/pulls/pulls-realtime.tsx
  • apps/web/src/lib/auth/password.test.ts
  • apps/web/src/lib/auth/password.ts
  • apps/web/src/lib/auth/server.ts
  • apps/web/src/lib/integrations/oauth-state.ts
  • apps/web/src/lib/realtime/hub.ts
  • apps/web/src/lib/realtime/provider.tsx
  • apps/web/src/lib/realtime/url.test.ts
  • apps/web/src/lib/realtime/url.ts
  • apps/web/vercel.json
  • packages/core/package.json
  • packages/core/src/analytics/burndown.ts
  • packages/core/src/realtime/publisher.ts
  • packages/db/package.json
  • packages/db/src/client.ts
  • packages/db/src/ensure-extensions.ts
  • packages/mcp-server/bunfig.toml
  • packages/mcp-server/package.json
  • packages/mcp-server/src/auth.test.ts
  • packages/mcp-server/src/comments.ts
  • packages/mcp-server/src/index.ts
  • packages/mcp-server/src/logger.ts
  • packages/mcp-server/src/resolve.ts
  • packages/mcp-server/src/server.ts
  • packages/mcp-server/src/test-helpers.ts
  • packages/mcp-server/src/tools.test.ts
  • packages/mcp-server/src/tools/admin.ts
  • packages/mcp-server/src/tools/identity.ts
  • packages/mcp-server/src/tools/index.ts
  • packages/mcp-server/src/tools/issues.ts
  • packages/mcp-server/src/tools/planning.ts
  • packages/mcp-server/src/tools/support.ts
  • packages/mcp-server/src/views.ts
  • packages/mcp-server/tests-preload.ts
  • packages/mcp-server/tsconfig.json
  • packages/realtime-server/bunfig.toml
  • packages/realtime-server/package.json
  • packages/realtime-server/src/auth.test.ts
  • packages/realtime-server/src/auth.ts
  • packages/realtime-server/src/connection.test.ts
  • packages/realtime-server/src/connection.ts
  • packages/realtime-server/src/hub.ts
  • packages/realtime-server/src/index.ts
  • packages/realtime-server/src/logger.ts
  • packages/realtime-server/src/presence.ts
  • packages/realtime-server/src/socket.ts
  • packages/realtime-server/tests-preload.ts
  • packages/realtime-server/tsconfig.json
  • packages/services/package.json
  • packages/services/src/email/email.test.ts
  • packages/services/src/email/index.ts
  • packages/services/src/github/apply.test.ts
  • packages/services/src/github/apply.ts
  • packages/services/src/github/install.test.ts
  • packages/services/src/github/install.ts
  • packages/services/src/notifications/index.ts
  • packages/services/src/notifications/notifications.test.ts
  • packages/services/src/slack/dispatch.test.ts
  • packages/services/src/slack/dispatch.ts
  • packages/services/src/storage/credentials.test.ts
  • packages/services/src/storage/credentials.ts
  • packages/services/src/storage/parent.test.ts
  • packages/services/src/storage/s3.ts
  • packages/services/src/storage/validate.ts
  • packages/shared/package.json
  • packages/shared/src/utils/index.ts
💤 Files with no reviewable changes (6)
  • apps/mcp/src/env.ts
  • .env.example
  • apps/mcp/README.md
  • apps/mcp/src/server.ts
  • apps/mcp/Dockerfile
  • apps/mcp/src/index.ts
🛑 Comments failed to post (4)
packages/mcp-server/src/comments.ts (1)

35-47: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect the comment table definition and existing parentId validation.
set -euo pipefail

fd -t f -e ts . packages/db/src --exec rg -n -C 10 'comment\s*=\s*pgTable|parentId' {} \;

echo '--- commentCreateSchema ---'
rg -n -C 10 'commentCreateSchema' packages/shared/src

echo '--- other comment creation paths ---'
rg -n -C 5 'parentId' packages/core/src apps/web/src --type=ts

Repository: Noveum/orbit

Length of output: 18733


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- candidate files ---'
fd -t f -i 'comments|comment' packages/mcp-server packages/db packages/shared | head -80

echo '--- comments.ts structure ---'
ast-grep outline packages/mcp-server/src/comments.ts --view summary

echo '--- comments.ts relevant code ---'
rg -n -C 12 'createComment|add_comment|replyTo|parentId|transaction' packages/mcp-server/src/comments.ts

echo '--- query and error exports ---'
rg -n -C 5 'export.*(and|eq)|function notFound|const notFound|notFound' packages/db packages/shared --glob '*.ts'

Repository: Noveum/orbit

Length of output: 433


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- comments.ts structure ---'
ast-grep outline packages/mcp-server/src/comments.ts --view signatures

echo '--- comments.ts relevant code ---'
rg -n -C 14 'createComment|add_comment|replyTo|parentId|transaction' packages/mcp-server/src/comments.ts

echo '--- comment validator ---'
cat -n packages/shared/src/validators/comment.ts

echo '--- query and error exports ---'
rg -n -C 5 'export.*(and|eq)|function notFound|const notFound|notFound' packages/db packages/shared --glob '*.ts'

Repository: Noveum/orbit

Length of output: 12594


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- comments.ts ---'
cat -n packages/mcp-server/src/comments.ts

echo '--- add_comment and createComment call sites ---'
rg -n -C 16 'add_comment|createComment|replyTo' packages/mcp-server --glob '*.ts'

echo '--- all comment parent reads and writes ---'
rg -n -C 10 'schema\.comment|parentId|replyTo' packages/mcp-server packages/core apps packages/db --glob '*.{ts,tsx}'

echo '--- db barrel exports ---'
fd -t f -e ts . packages/db/src | sort | head -80
rg -n -C 5 'drizzle-orm|export.*and|export.*eq' packages/db/src --glob '*.ts'

Repository: Noveum/orbit

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- MCP source files ---'
git ls-files packages/mcp-server -- '*.ts'

echo '--- comments.ts ---'
cat -n packages/mcp-server/src/comments.ts

echo '--- MCP comment tool references ---'
rg -n -C 10 --glob '*.ts' 'add_comment|replyTo|createComment|commentCreateSchema' packages/mcp-server

echo '--- MCP comment reads ---'
rg -n -C 10 --glob '*.ts' 'schema\.comment|commentQuerySchema|parentId' packages/mcp-server

Repository: Noveum/orbit

Length of output: 41873


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

schema = Path("packages/db/src/schema/content.ts").read_text()
service = Path("packages/mcp-server/src/comments.ts").read_text()
tool = Path("packages/mcp-server/src/tools/issues.ts").read_text()
validator = Path("packages/shared/src/validators/comment.ts").read_text()

comment_block = re.search(
    r"export const comment = pgTable\(.*?\n\);",
    schema,
    re.S,
)
assert comment_block, "comment table not found"
comment = comment_block.group(0)

assert re.search(r"parentId: text\('parent_id'\)", comment)
assert not re.search(r"parentId:.*references\(", comment)

assert "parentId: parsed.parentId" in service
assert "replyTo: z.string().min(1).optional()" in tool
assert "parentId: args.replyTo ?? null" in tool
assert "parentId: idSchema.nullable().default(null)" in validator

print("comment.parentId: plain text, no foreign key")
print("createComment: inserts parsed.parentId without a parent query")
print("add_comment: accepts non-empty replyTo and passes it unchanged")
print("validator: checks identifier shape only")
PY

Repository: Noveum/orbit

Length of output: 360


Validate parsed.parentId against the target issue and organization.

comment.parentId has no foreign key, and add_comment passes any non-empty replyTo unchanged. Query the parent inside the transaction and reject it unless its organizationId matches principal.organizationId and its issueId matches issue.id. Reject nonexistent parents with notFound.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mcp-server/src/comments.ts` around lines 35 - 47, In the
comment-creation transaction around the insert in add_comment, validate any
non-empty parsed.parentId by querying the parent comment and requiring matching
principal.organizationId and issue.id; reject a missing parent with notFound and
reject a parent belonging to another organization or issue. Only insert after
this validation, preserving null/empty parent behavior.
packages/mcp-server/src/tools/identity.ts (1)

82-104: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check permissions related to member listing and how web routes gate it.
set -euo pipefail

echo '--- permissions and roles ---'
rg -n -C 3 'member:|PERMISSIONS_BY_ROLE' packages/shared/src/policy

echo '--- listMembers definition and callers ---'
rg -n -C 5 '\blistMembers\s*\(' packages/core/src apps/web/src packages/mcp-server/src --type=ts

Repository: Noveum/orbit

Length of output: 9675


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- policy definitions ---'
cat -n packages/shared/src/policy/index.ts | sed -n '1,125p'

echo '--- member service ---'
cat -n packages/core/src/org/member-service.ts | sed -n '1,115p'

echo '--- MCP identity and admin tools ---'
cat -n packages/mcp-server/src/tools/identity.ts | sed -n '1,135p'
cat -n packages/mcp-server/src/tools/admin.ts | sed -n '1,75p'

echo '--- principal and authorization usage ---'
rg -n -C 4 'assertCan|can\(|member:invite|member:manage' packages/core/src packages/mcp-server/src apps/web/src packages/shared/src --type=ts

Repository: Noveum/orbit

Length of output: 50369


Restrict member directory access.

listMembers returns every member’s email without authorization. The shared policy has no directory-read permission; member:manage is limited to admin, while member:invite is available to member and admin. Define the intended directory permission and enforce it in listMembers before list_users, list_members, and web member endpoints return email addresses.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mcp-server/src/tools/identity.ts` around lines 82 - 104, The
member-directory permission is missing, allowing unauthorized email access
through listMembers consumers. Define the intended directory-read permission in
the shared policy, enforce it within listMembers before returning member data,
and ensure list_users, list_members, and web member endpoints use that protected
path while preserving existing role permissions.
packages/mcp-server/src/tools/support.ts (1)

29-40: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Avoid writing caller-supplied identifiers into logs.

errorFields(error) returns the error message, and logger.warn writes it. Several resolver errors embed the raw reference in the message. For example resolveUserId in packages/mcp-server/src/resolve.ts at Line 47 throws No user matches "<ref>", and <ref> is often an email address. The same applies to team, label, project, and cycle references. Emails and other user identifiers then persist in the log store.

Log the tool name, the error code, and the status. Keep the free-text message out of the log line, or redact it for client errors.

🛡️ Proposed change
 export function failed(name: string, error: unknown): CallToolResult {
   const domain = asDomainError(error);
-  logger.warn('tool failed', { tool: name, code: domain.code, ...errorFields(error) });
+  logger.warn('tool failed', {
+    tool: name,
+    code: domain.code,
+    status: domain.status,
+    ...(domain.status >= 500 ? errorFields(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.

export function failed(name: string, error: unknown): CallToolResult {
  const domain = asDomainError(error);
  logger.warn('tool failed', {
    tool: name,
    code: domain.code,
    status: domain.status,
    ...(domain.status >= 500 ? errorFields(error) : {}),
  });
  const body =
    domain.status >= 500
      ? { error: { code: domain.code, message: 'Something went wrong on our side.' } }
      : domain.toJSON();
  return {
    isError: true,
    content: [{ type: 'text', text: JSON.stringify(body) }],
  };
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mcp-server/src/tools/support.ts` around lines 29 - 40, Update failed
to stop spreading errorFields(error) into logger.warn; log only the tool name,
domain.code, and domain.status. Preserve the existing response body and error
handling behavior while ensuring caller-supplied messages and identifiers are
not written to logs.
packages/mcp-server/src/views.ts (1)

89-93: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Throw a typed domain error instead of Error.

Line 91 throws a bare Error. support.ts maps unknown errors through toDomainError, which converts them to internal, so the behavior is close. A typed error keeps the code explicit and matches the rest of the package, for example internal(...) in packages/mcp-server/src/comments.ts at Line 47.

The coding guidelines require typed domain errors from @orbit/shared/errors.

♻️ Proposed change
+import { internal } from '`@orbit/shared/errors`';
+
 export async function describeIssue(principal: Principal, row: IssueRow): Promise<IssueView> {
   const [view] = await describeIssues(principal, [row]);
-  if (view === undefined) throw new Error('The issue could not be described.');
+  if (view === undefined) throw internal('The issue could not be described.');
   return view;
 }

As per coding guidelines: "Throw typed domain errors from @orbit/shared/errors, and have route handlers map them to responses; never silently swallow errors."

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

import { internal } from '`@orbit/shared/errors`';

export async function describeIssue(principal: Principal, row: IssueRow): Promise<IssueView> {
  const [view] = await describeIssues(principal, [row]);
  if (view === undefined) throw internal('The issue could not be described.');
  return view;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mcp-server/src/views.ts` around lines 89 - 93, Update describeIssue
to throw the typed internal domain error from `@orbit/shared/errors` instead of a
bare Error when describeIssues returns no view. Follow the existing
internal(...) usage in comments.ts and preserve the current message and control
flow.

Source: Coding guidelines

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
CLAUDE.md (2)

134-136: 🗄️ Data Integrity & Integration | 🔵 Trivial

Add a production migration release gate

CI updates only ephemeral test databases. Add an enforced release step that applies migrations and verifies the target schema before Vercel promotion. Otherwise, production code can start against an older schema.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLAUDE.md` around lines 134 - 136, Add an enforced production release gate
that applies migrations to the target database and verifies the resulting schema
before Vercel promotion. Update the deployment workflow or release configuration
referenced by the migration guidance, ensuring promotion cannot proceed when
migration application or schema verification fails.

120-132: 🩺 Stability & Availability | 🔵 Trivial

Add a preview WebSocket smoke test.

Authenticate against the preview, mint a realtime ticket, connect to /api/ws, and assert 101 Switching Protocols before sending the ticket frame. Remove the bunVersion assertion because Vercel supports experimental_upgradeWebSocket with the Bun runtime.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLAUDE.md` around lines 120 - 132, Add a preview WebSocket smoke test that
authenticates against the preview, mints a realtime ticket, connects to /api/ws,
and verifies a 101 Switching Protocols response before sending the ticket frame.
Remove the documentation’s bunVersion assertion, including the claim that the
node runtime is required for experimental_upgradeWebSocket, while preserving the
remaining route handshake guidance.

Source: MCP tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@CLAUDE.md`:
- Around line 134-136: Add an enforced production release gate that applies
migrations to the target database and verifies the resulting schema before
Vercel promotion. Update the deployment workflow or release configuration
referenced by the migration guidance, ensuring promotion cannot proceed when
migration application or schema verification fails.
- Around line 120-132: Add a preview WebSocket smoke test that authenticates
against the preview, mints a realtime ticket, connects to /api/ws, and verifies
a 101 Switching Protocols response before sending the ticket frame. Remove the
documentation’s bunVersion assertion, including the claim that the node runtime
is required for experimental_upgradeWebSocket, while preserving the remaining
route handshake guidance.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 81e306b8-ff6d-49cb-a03d-d88bd4e40da6

📥 Commits

Reviewing files that changed from the base of the PR and between 693bfa3 and ec5b88f.

📒 Files selected for processing (15)
  • .dockerignore
  • .github/workflows/ci.yml
  • CLAUDE.md
  • Dockerfile.deps
  • apps/realtime/Dockerfile
  • apps/web/Dockerfile
  • k8s/00-namespace.yaml
  • k8s/01-redis.yaml
  • k8s/02-web.yaml
  • k8s/03-realtime.yaml
  • k8s/04-mcp.yaml
  • k8s/05-ingress.yaml
  • k8s/README.md
  • k8s/apply.sh
  • k8s/s3-cors.json
💤 Files with no reviewable changes (14)
  • k8s/s3-cors.json
  • apps/realtime/Dockerfile
  • .dockerignore
  • k8s/README.md
  • k8s/apply.sh
  • k8s/01-redis.yaml
  • k8s/05-ingress.yaml
  • Dockerfile.deps
  • k8s/00-namespace.yaml
  • apps/web/Dockerfile
  • .github/workflows/ci.yml
  • k8s/04-mcp.yaml
  • k8s/02-web.yaml
  • k8s/03-realtime.yaml

The uploads bucket still needs a CORS rule because the browser PUTs
straight to storage, but the manifest directory that carried it is gone.
Move it to infra with the command that applies it.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@infra/README.md`:
- Around line 12-13: Update the aws s3api put-bucket-cors command to use the
existing S3_BUCKET environment variable instead of ORBIT_S3_BUCKET, keeping the
CORS configuration generation unchanged.

In `@packages/services/src/storage/round-trip.test.ts`:
- Around line 90-93: The test “documents how it reaches the bucket” should
assert the complete documented sed command and complete aws s3api
put-bucket-cors command from infra/README.md, including the expected
k8s/s3-cors.json source, temporary file, bucket variable, and command arguments,
rather than checking only partial substrings.
- Line 51: Update both test file reads in the round-trip tests to import and use
readFile from node:fs/promises with the utf8 encoding, replacing each
Bun.file(...).text() call while preserving the existing file paths and test
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c3b6a386-71a3-4927-a9ea-0470e0d2e8a2

📥 Commits

Reviewing files that changed from the base of the PR and between ec5b88f and 3beb6a0.

📒 Files selected for processing (3)
  • infra/README.md
  • infra/s3-cors.json
  • packages/services/src/storage/round-trip.test.ts

Comment thread infra/README.md Outdated
Comment thread packages/services/src/storage/round-trip.test.ts Outdated
Comment thread packages/services/src/storage/round-trip.test.ts Outdated
Only an end-to-end test touched this path, so a driver change could break
it without any suite noticing. Assert the row lands, the sync action
carries the scopes other tabs subscribe to, and toggling again removes it.
A failing spec only showed what the browser saw, so a server side error
during the run left no trace to read.
Sanitizing chose Bun's HTMLRewriter and otherwise fell back to a browser
document, so on the node runtime every render threw 'document is not
defined' and each comment POST answered 500.

Parse with linkedom when there is no document. The template element it
returns has no content fragment, so the cleaner walked nothing and handed
back the input untouched, which would have let a script tag through.
Hold the markup in a real element instead and compare node types rather
than instances, and cover the path the server now takes.
The browser branch handed back the template content fragment, which has
no innerHTML, so sanitizing returned undefined wherever a document
exists. Keep the element for serialising and walk the fragment's nodes.
Review findings from the pull request.

A rejected hub promise stayed cached for the life of the instance, so one
unreachable redis left every later connection closing with 1011. Drop the
entry when it rejects.

Frames arriving before the hub resolves were buffered without a limit and
before any rate limit applied, so cap the buffer and close past it.

The health route returned the raw error, the MCP route reached past
publicAppUrl, verify turned every argon2 failure into a wrong password,
and a malformed realtime url reached the WebSocket constructor.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/lib/auth/password.ts`:
- Line 4: Update the PHC_ARGON2 pattern used by password verification to accept
only the $argon2id$ prefix, excluding Argon2i and Argon2d hashes. Preserve the
existing verification flow while ensuring legacy algorithms are rejected unless
an explicit controlled migration path already exists.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7caf3c5e-b287-4a31-a924-c53dee3a1e8b

📥 Commits

Reviewing files that changed from the base of the PR and between d846ac3 and c7cb1bf.

📒 Files selected for processing (11)
  • CLAUDE.md
  • README.md
  • apps/web/src/app/api/realtime/health/route.ts
  • apps/web/src/app/api/ws/route.ts
  • apps/web/src/app/mcp/route.ts
  • apps/web/src/lib/auth/password.ts
  • apps/web/src/lib/realtime/hub.ts
  • apps/web/src/lib/realtime/url.test.ts
  • apps/web/src/lib/realtime/url.ts
  • infra/README.md
  • packages/services/src/storage/round-trip.test.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • apps/web/src/lib/realtime/url.ts
  • infra/README.md
  • packages/services/src/storage/round-trip.test.ts
  • apps/web/src/app/mcp/route.ts
  • apps/web/src/app/api/realtime/health/route.ts
  • CLAUDE.md

import { hash, verify } from '@node-rs/argon2';

const ARGON2ID = 2;
const PHC_ARGON2 = /^\$argon2(?:id|i|d)\$/;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- target file ---'
cat -n apps/web/src/lib/auth/password.ts
printf '%s\n' '--- related symbols and callers ---'
rg -n --glob '!node_modules' 'PHC_ARGON2|hashPassword|verifyPassword|argon2' apps packages
printf '%s\n' '--- relevant package configuration ---'
rg -n --glob 'package.json' --glob 'bun.lockb' --glob 'bun.lock' '`@node-rs/argon2`|argon2|password' .

Repository: Noveum/orbit

Length of output: 7529


🏁 Script executed:

#!/bin/bash
set -eu
cat -n apps/web/src/lib/auth/password.ts
rg -n --glob '!node_modules' 'PHC_ARGON2|hashPassword|verifyPassword|argon2' apps packages
rg -n --glob 'package.json' --glob 'bun.lockb' --glob 'bun.lock' '`@node-rs/argon2`|argon2|password' .

Repository: Noveum/orbit

Length of output: 7434


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- password tests ---'
cat -n apps/web/src/lib/auth/password.test.ts
printf '%s\n' '--- authentication path ---'
sed -n '1,140p' apps/web/src/lib/auth/server.ts
printf '%s\n' '--- standalone prefix behavior ---'
python3 - <<'PY'
import re
pattern = re.compile(r'^\$argon2(?:id|i|d)\$')
for digest in ('$argon2id$v=19$m=1,t=1,p=1$x$y',
               '$argon2i$v=19$m=1,t=1,p=1$x$y',
               '$argon2d$v=19$m=1,t=1,p=1$x$y',
               '$bcrypt$anything'):
    print(f'{digest.split("$")[1]}: {bool(pattern.match(digest))}')
PY

Repository: Noveum/orbit

Length of output: 6266


🌐 Web query:

@node-rs/argon2 verify Argon2id Argon2i Argon2d PHC encoded password support

💡 Result:

The @node-rs/argon2 package fully supports Argon2id, Argon2i, and Argon2d algorithms [1][2] and is designed to handle PHC-encoded password strings for verification [3]. The library's verify function expects a valid PHC-encoded hash string (e.g., $argon2id$v=19$m=65536,t=3,p=4$salt$hash) as the first argument [3]. When you pass this string along with the plaintext password, the library automatically parses the algorithm, version, and parameters embedded within the PHC string to perform the verification process [4][3]. This ensures that the parameters used during hashing (such as memory cost, time iterations, and parallelism) are correctly applied during verification [4]. It is recommended to wrap the verify function in a try...catch block, as passing an invalid or malformed PHC string will throw an error [3]. The package supports Argon2id (the default and recommended algorithm), Argon2i, and Argon2d variants [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu
cat -n apps/web/src/lib/auth/password.test.ts
sed -n '1,140p' apps/web/src/lib/auth/server.ts
python3 - <<'PY'
import re
pattern = re.compile(r'^\$argon2(?:id|i|d)\$')
for digest in ('$argon2id$v=19$m=1,t=1,p=1$x$y',
               '$argon2i$v=19$m=1,t=1,p=1$x$y',
               '$argon2d$v=19$m=1,t=1,p=1$x$y',
               '$bcrypt$anything'):
    print(f'{digest.split("$")[1]}: {bool(pattern.match(digest))}')
PY

Repository: Noveum/orbit

Length of output: 6180


Restrict password verification to Argon2id.

PHC_ARGON2 accepts $argon2i$ and $argon2d$, and @node-rs/argon2.verify processes those algorithms. Change the prefix check to /^\$argon2id\$/, or implement a controlled migration for legacy hashes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/lib/auth/password.ts` at line 4, Update the PHC_ARGON2 pattern
used by password verification to accept only the $argon2id$ prefix, excluding
Argon2i and Argon2d hashes. Preserve the existing verification flow while
ensuring legacy algorithms are rejected unless an explicit controlled migration
path already exists.

Source: Coding guidelines

@pulkitxm
pulkitxm merged commit df963d8 into main Aug 3, 2026
7 checks passed
@pulkitxm
pulkitxm deleted the worktree-realtime-on-vercel branch August 3, 2026 11:38
@pulkitxm

pulkitxm commented Aug 3, 2026

Copy link
Copy Markdown
Member Author

production after the deploy, the socket that was failing in the console now upgrades:

$ bun wstest.ts wss://orbit.noveum.ai/api/ws
RESULT: OPEN, handshake succeeded (101)
RESULT: closed code=4001 reason=unauthorized

4001 is the hub rejecting a deliberately invalid ticket, so auth is running too.

$ curl -sS -i -X POST https://orbit.noveum.ai/mcp -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}"
HTTP/2 401
www-authenticate: Bearer resource_metadata="https://orbit.noveum.ai/.well-known/oauth-protected-resource/mcp"

/mcp was a 404 before this.

$ curl -sS https://orbit.noveum.ai/api/realtime/health
{"status":"ok","redisConfigured":true,"hub":{"connections":0,"subscriptions":0,"redis":"ready"},"runtime":{"bun":null,"node":"24.18.0"}}

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