Skip to content

swap to queue for emails#394

Merged
InfinityBowman merged 10 commits into
mainfrom
switch-to-email-queue
Mar 7, 2026
Merged

swap to queue for emails#394
InfinityBowman merged 10 commits into
mainfrom
switch-to-email-queue

Conversation

@InfinityBowman
Copy link
Copy Markdown
Owner

@InfinityBowman InfinityBowman commented Feb 26, 2026

Switch from Durable Object to Queue for email handling for all emails.


Note

Medium Risk
Switches the app’s outbound email path to Cloudflare Queues (new producer/consumer bindings and retry behavior), touching auth/contact/invitations/billing flows where delivery failures can impact user-facing workflows. Also tweaks Yjs sync/persistence and Stripe period/version handling, which could surface as collaboration or billing regressions if edge cases are missed.

Overview
Email delivery is migrated from a Durable Object to Cloudflare Queues. The EmailQueue DO implementation/tests and /api/email route are removed; a new queueEmail() producer helper is added and the worker now consumes queue batches in index.ts, handling retries with backoff and acking on success.

All email-producing flows are rewired to enqueue payloads (auth verification/magic links/password resets, contact form with replyTo, account-merge verification, billing dunning, and project invitations). Invitation emails are deduplicated behind a new sendInvitationEmail() helper, and invitation endpoints now return a “created but delivery may be delayed” message when queuing fails.

Infra and correctness updates. wrangler.jsonc adds queue bindings/consumer config and a migration deleting the EmailQueue class; health checks/tests are updated accordingly. Also includes small Yjs/Durable Object robustness fixes (ProjectDoc awareness clientId capture + webSocketError init), frontend Yjs persistence guards (Dexie corruption handling, awareness handler try/catch, re-entrancy guard), and Stripe API/version + subscription-period field adjustments.

Written by Cursor Bugbot for commit f2a922c. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

  • Refactor

    • Switched email delivery to Cloudflare Queues; all email flows now enqueue messages (contact form, invites, verification, password reset). Invitation flows delegate to a shared queued helper and responses indicate queued/delayed delivery.
    • Health/status no longer lists the removed durable-object email queue.
  • Documentation

    • Architecture and status docs updated to show Cloudflare Queue-based email delivery (Cloudflare Queue + Postmark) and removed prior Durable Object/Postmark references.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Feb 26, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
corates f2a922c Commit Preview URL

Branch Preview URL
Mar 07 2026, 02:37 AM

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 26, 2026

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

Migrates email delivery from a Durable Object (EmailQueue) to Cloudflare Queues: removes the EmailQueue DO, its HTTP route and tests; adds a queue producer (queueEmail), a queue consumer handler, a send-invitation helper, updates callers to enqueue emails, and adjusts wrangler config and tests accordingly.

Changes

Cohort / File(s) Summary
Docs & Architecture
.cursor/rules/durable-objects.mdc, packages/docs/STATUS.md, packages/docs/architecture/diagrams/02-system-architecture.md
Removed EmailQueue DO references; replaced with Cloudflare Queue + Postmark delivery text and diagram updates.
Durable Object removal
packages/workers/src/durable-objects/EmailQueue.ts, packages/workers/src/durable-objects/__tests__/EmailQueue.test.js
Deleted the EmailQueue durable object implementation and its comprehensive test suite (queuing, retry/backoff, alarms, dead-letter handling).
Queue producer & consumer
packages/workers/src/lib/email-queue.ts, packages/workers/src/index.ts, packages/workers/wrangler.jsonc
Added EmailPayload type and queueEmail(env,payload) producer; index exposes a queue consumer handler; wrangler config adds queue producer/consumer bindings and removes the DO binding.
Invitation helper
packages/workers/src/lib/send-invitation-email.ts
New helper that generates magic links and enqueues project-invitation emails (returns { emailQueued }).
Auth & email refactor
packages/workers/src/auth/config.ts, packages/workers/src/auth/email.ts, packages/workers/src/config/constants.ts
Switched direct sends to queueEmail, removed sendProjectInvitation, added optional replyTo on send params, and removed EMAIL_RETRY_CONFIG.
Routes moved to queue usage
packages/workers/src/routes/email.ts (removed), packages/workers/src/routes/contact.ts, packages/workers/src/routes/account-merge.ts, packages/workers/src/routes/members.ts, packages/workers/src/routes/orgs/*.ts
Deleted the email HTTP route; contact, account-merge, members and org invitation routes now enqueue emails via queueEmail/send-invitation-email instead of direct Postmark/DO calls.
Billing & tests
packages/workers/src/routes/billing/handlers/dunning.ts, packages/workers/src/routes/billing/handlers/invoiceHandlers.ts, packages/workers/src/routes/billing/__tests__/invoiceHandlers.test.ts
Switched dunning/invoice email enqueue path to queueEmail; tests updated to mock EMAIL_QUEUE.send/sendBatch and assertions adjusted.
Health & test infra
packages/workers/src/routes/health.ts, packages/workers/src/__tests__/helpers.ts, packages/workers/src/__tests__/health.test.js, packages/workers/src/routes/__tests__/*
Removed EMAIL_QUEUE from health bindings; test helpers and assertions updated to use a mock queue object with send/sendBatch; removed tests for the deleted route.
Removed HTTP tests
packages/workers/src/routes/__tests__/email.test.js
Deleted the email route test suite for the removed /queue endpoint.
Web & DO minor changes
packages/web/src/..., packages/workers/src/durable-objects/ProjectDoc.ts, packages/workers/src/durable-objects/UserSession.ts
Formatting, defensive wrappers, and Y.Doc persistence guards; unrelated to email logic.
Configs & deps
package.json, packages/*/package.json, pnpm-workspace.yaml, .mcp.json, .cursor/mcp.json, .vscode/mcp.json, eslint.config.js
Added Cloudflare queues/migrations in wrangler, dependency bumps, new MCP server entries, pnpm built-deps additions, and added globals for Message/MessageBatch.

Sequence Diagram

sequenceDiagram
    participant App as Application / Route
    participant Queue as Cloudflare Queue (Producer)
    participant Consumer as Queue Consumer (Worker)
    participant Postmark as Postmark API

    App->>Queue: queueEmail(env, payload)
    Note over Queue: Message enqueued (ack)
    Queue-->>App: Enqueue ack

    Consumer->>Queue: Receive batch
    Queue->>Consumer: EmailPayload batch
    Consumer->>Consumer: Validate payload
    Consumer->>Postmark: Send email (html/text, replyTo?)
    Postmark-->>Consumer: Success / Error

    alt Success
        Consumer->>Consumer: Log success
    else Failure
        Consumer->>Queue: Retry attempts (consumer-managed)
        Note over Consumer: Dead-letter after max retries
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.83% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'swap to queue for emails' directly summarizes the main change: migrating email delivery from Durable Objects to Cloudflare Queues.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch switch-to-email-queue

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

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (2)
packages/workers/src/routes/billing/__tests__/invoiceHandlers.test.ts (1)

232-251: ⚠️ Potential issue | 🟡 Minor

Missing assertion: test does not verify email was queued.

The test is named "queues dunning email when user found and EMAIL_QUEUE available" but never asserts that mockEmailQueue.send was actually called. This makes the test ineffective at verifying the email queuing behavior.

Proposed fix to add assertion
       const ctx = createTestContext(db, { env: { EMAIL_QUEUE: mockEmailQueue } });
       const result = await handleInvoicePaymentFailed(invoice, ctx);
 
       expect(result.handled).toBe(true);
       expect(result.result).toBe('payment_failed_processed');
+      expect(mockEmailQueue.send).toHaveBeenCalledTimes(1);
+      expect(mockEmailQueue.send).toHaveBeenCalledWith(
+        expect.objectContaining({
+          to: 'billing@example.com',
+        }),
+      );
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/workers/src/routes/billing/__tests__/invoiceHandlers.test.ts` around
lines 232 - 251, The test for "queues dunning email when user found and
EMAIL_QUEUE available" is missing an assertion that the email queue was invoked;
update the test in invoiceHandlers.test.ts to assert that mockEmailQueue.send
was called with the expected payload after calling
handleInvoicePaymentFailed(invoice, ctx). Locate the test that creates
mockEmailQueue (mockEmailQueue = { send: vi.fn().mockResolvedValue(undefined) })
and add an assertion like expect(mockEmailQueue.send).toHaveBeenCalled() and
optionally verify arguments (e.g., matching invoice.id, customer or a dunning
template) to ensure handleInvoicePaymentFailed triggers the queue send.
packages/workers/src/routes/contact.ts (1)

179-207: ⚠️ Potential issue | 🟡 Minor

Add replyTo field to enable contact form replies.

The HTML email mentions "You can reply directly to this email to respond to ${name}" but the email payload doesn't include a replyTo header. To enable this, add the field to the email queue infrastructure:

  1. Update EmailPayload interface in packages/workers/src/lib/email-queue.ts:
export interface EmailPayload {
  to: string;
  subject: string;
+ replyTo?: string;
  html?: string;
  text?: string;
}
  1. Update SendEmailParams interface in packages/workers/src/auth/email.ts:
interface SendEmailParams {
  to: string;
  subject: string;
+ replyTo?: string;
  html: string;
  text: string;
}
  1. Update Postmark API call in packages/workers/src/auth/email.ts:
const response = await postmark.sendEmail({
  From: `CoRATES <${env.EMAIL_FROM || 'noreply@corates.org'}>`,
  To: to,
  Subject: subject,
+ ReplyTo: replyTo,
  HtmlBody: html,
  TextBody: text,
  MessageStream: 'outbound',
});
  1. Update contact.ts to pass the field:
await queueEmail(env, {
  to: contactEmail,
  subject: `[Contact Form] ${subject || 'New Inquiry'}`,
+ replyTo: email,
  text: `New contact form submission:\n\nName: ${name}\nEmail: ${email}\nSubject: ${subject || 'Not specified'}\n\nMessage:\n${message}`,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/workers/src/routes/contact.ts` around lines 179 - 207, The HTML
promises reply-by-email but the outgoing payload never sets a reply address; add
a replyTo field through the email stack: extend the EmailPayload type in
EmailPayload (packages/workers/src/lib/email-queue.ts) to include replyTo?:
string, update SendEmailParams in the auth email module (SendEmailParams in
packages/workers/src/auth/email.ts) to accept replyTo?: string, ensure the
Postmark/Postmark client call that sends mail (the function that maps
SendEmailParams to the Postmark API request in
packages/workers/src/auth/email.ts) includes replyTo when constructing the API
payload, and finally pass replyTo: email (the submitter address) when calling
queueEmail from contact.ts (the queueEmail(...) invocation shown) so replies
route back to the form submitter.
🧹 Nitpick comments (6)
packages/workers/src/routes/billing/handlers/invoiceHandlers.ts (1)

202-202: Make the comment intent-focused, not action-described.

Line 202 repeats the code action and doesn’t add context. Prefer a brief “why” comment (or remove it if unnecessary), e.g., that dunning is queued to keep webhook handling non-blocking and resilient.

As per coding guidelines, "Comments should explain why something is being done, not what the code is doing" and "Reserve comments for explaining intent, context, workarounds, assumptions, edge cases, or limitations."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/workers/src/routes/billing/handlers/invoiceHandlers.ts` at line 202,
Replace the action-focused inline comment "// Queue dunning email" with an
intent-focused comment that explains why dunning is queued (for example: "Queue
dunning email to keep webhook processing non-blocking and resilient, retrying
delivery asynchronously") or remove the comment if the code is self-explanatory;
update the comment near the dunning queueing call in invoiceHandlers.ts so it
documents intent/context/assumptions rather than restating the action.
packages/workers/src/routes/billing/__tests__/invoiceHandlers.test.ts (1)

266-287: Consider asserting send was not called when user is missing.

To strengthen this test's verification that dunning email is skipped, consider asserting that mockEmailQueue.send was never invoked.

Proposed enhancement
       expect(result.handled).toBe(true);
       expect(ctx.logger.stripe).toHaveBeenCalledWith('dunning_email_skipped_no_user', {
         subscriptionId: 'sub-local-1',
         stripeCustomerId: 'cus_orphan',
       });
+      expect(mockEmailQueue.send).not.toHaveBeenCalled();
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/workers/src/routes/billing/__tests__/invoiceHandlers.test.ts` around
lines 266 - 287, The test should also assert that the email queue's send method
was not invoked when there is no user; add an assertion using the existing
mockEmailQueue (mockEmailQueue.send) after calling handleInvoicePaymentFailed to
verify it was never called, keeping the rest of the test (invoice object, ctx
creation, and assertions on ctx.logger.stripe and result.handled) unchanged.
packages/workers/src/routes/health.ts (1)

120-138: Consider adding queue health check for operational visibility.

Since email delivery is now handled by a Cloudflare Queue, you may want to verify the EMAIL_QUEUE queue binding is available in health checks. This provides visibility into queue availability alongside other critical infrastructure.

Suggested addition (optional)
+  // Check Queue bindings are available
+  try {
+    checks.services.queues = {
+      status: c.env.EMAIL_QUEUE ? 'healthy' : 'unhealthy',
+      type: 'Cloudflare Queue',
+      bindings: {
+        EMAIL_QUEUE: !!c.env.EMAIL_QUEUE,
+      },
+    };
+  } catch (error) {
+    const err = error as Error;
+    checks.services.queues = {
+      status: 'unhealthy',
+      type: 'Cloudflare Queue',
+      error: err.message,
+    };
+    checks.status = 'degraded';
+  }

This would also require updating ServiceStatusSchema, HealthResponseSchema, and HealthChecks interface to include queues.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/workers/src/routes/health.ts` around lines 120 - 138, Add a health
check for the Cloudflare Queue binding by verifying c.env.EMAIL_QUEUE and adding
a new entry under checks.queues (or checks.services.queues) similar to the
Durable Objects check: set status to 'healthy' when c.env.EMAIL_QUEUE is
present, include type 'Queue' and bindings.EMAIL_QUEUE flag, and mark overall
checks.status 'degraded' on failure; then update the ServiceStatusSchema,
HealthResponseSchema, and the HealthChecks interface to include the new queues
property so the response type covers queue status. Target symbols: checks (in
the health route), c.env.EMAIL_QUEUE, ServiceStatusSchema, HealthResponseSchema,
and HealthChecks.
packages/workers/src/__tests__/helpers.ts (1)

384-394: Consider using vi.fn() for the default mockQueue to enable assertions.

The mockQueue uses plain async functions, which prevents tests using createTestEnv() from asserting that EMAIL_QUEUE.send was called. Tests like contact.test.js work around this by providing their own vi.fn() mocks via envOverrides, but tests relying on the default env won't be able to verify email queuing behavior.

If email-send assertions are commonly needed, consider:

Proposed change
  const mockQueue = {
-   send: async () => {},
-   sendBatch: async () => {},
+   send: vi.fn(async () => {}),
+   sendBatch: vi.fn(async () => {}),
  };

This would require importing vi from vitest, which may have implications for non-test usage of this helper.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/workers/src/__tests__/helpers.ts` around lines 384 - 394, The
default mockQueue uses plain async functions which prevents call assertions;
update the mockQueue in createTestEnv (symbols: mockQueue, EMAIL_QUEUE) to use
vi.fn() for send and sendBatch so tests can assert calls (e.g.,
EMAIL_QUEUE.send). Import vi from 'vitest' at the top of the test helper and
replace the async arrow functions with vi.fn(async () => ...) or vi.fn(). Note
that this introduces a test-only dependency (vitest) into the helper, so ensure
the helper remains test-scoped or conditionally import vi if needed.
packages/workers/src/lib/send-invitation-email.ts (1)

49-54: Consider static imports for core dependencies.

The dynamic imports for better-auth, drizzle-orm/d1, and @/db/schema.js add latency to each invocation. Since this helper is called during invitation flows which are user-facing, static imports at the module level would improve cold-start performance.

Suggested refactor
+import { betterAuth } from 'better-auth';
+import { magicLink } from 'better-auth/plugins';
+import { drizzleAdapter } from 'better-auth/adapters/drizzle';
+import { drizzle } from 'drizzle-orm/d1';
+import * as schema from '@/db/schema.js';
+import { MAGIC_LINK_EXPIRY_MINUTES } from '@/auth/emailTemplates.js';
+import { getProjectInvitationEmailHtml, getProjectInvitationEmailText } from '@/auth/emailTemplates.js';
+import { escapeHtml } from '@/lib/escapeHtml.js';
 import { createDomainError, SYSTEM_ERRORS } from '@corates/shared';
 import type { Env } from '../types';
 import { queueEmail } from './email-queue';

Then remove the corresponding await import(...) calls inside the function.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/workers/src/lib/send-invitation-email.ts` around lines 49 - 54,
Replace the dynamic in-function imports with top-level static imports to reduce
cold-start latency: remove the await import(...) calls inside
send-invitation-email and instead import betterAuth, magicLink, drizzleAdapter,
drizzle, schema, and MAGIC_LINK_EXPIRY_MINUTES as normal ES module imports at
the module scope (referencing the same symbols betterAuth, magicLink,
drizzleAdapter, drizzle, schema, MAGIC_LINK_EXPIRY_MINUTES) and update any
usages in the function to use those statically imported bindings.
packages/workers/src/routes/billing/handlers/dunning.ts (1)

99-100: Consider using static import for queueEmail.

The dynamic import here adds unnecessary runtime overhead since queueEmail is a straightforward module with no circular dependency concerns. Static imports are preferred for simpler bundling and better error detection at build time.

Suggested static import

Add to the imports at the top of the file:

import { queueEmail } from '../../../lib/email-queue';

Then simplify the usage:

  try {
-   const { queueEmail } = await import('../../../lib/email-queue');
    await queueEmail(env, emailPayload);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/workers/src/routes/billing/handlers/dunning.ts` around lines 99 -
100, Replace the dynamic runtime import of queueEmail with a static import: add
a top-level import for queueEmail (import { queueEmail } from
'../../../lib/email-queue') and remove the await import(...) usage so you call
queueEmail(env, emailPayload) directly; update any references in the dunning
handler (where queueEmail is currently dynamically imported) to use the
statically imported symbol.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/web/src/components/project/overview-tab/OverviewTab.jsx`:
- Around line 303-313: The TooltipTrigger usage incorrectly passes a render
function to the asChild prop; change TooltipTrigger (from `@ark-ui/solid`) to use
the boolean pattern by setting asChild (e.g., asChild or asChild={true}) and
move the span element inside TooltipTrigger as its single child, removing the
render-function signature and the {...item} spread; keep the existing span
classes and FiPlus element so the trigger behavior is merged onto that span.

---

Outside diff comments:
In `@packages/workers/src/routes/billing/__tests__/invoiceHandlers.test.ts`:
- Around line 232-251: The test for "queues dunning email when user found and
EMAIL_QUEUE available" is missing an assertion that the email queue was invoked;
update the test in invoiceHandlers.test.ts to assert that mockEmailQueue.send
was called with the expected payload after calling
handleInvoicePaymentFailed(invoice, ctx). Locate the test that creates
mockEmailQueue (mockEmailQueue = { send: vi.fn().mockResolvedValue(undefined) })
and add an assertion like expect(mockEmailQueue.send).toHaveBeenCalled() and
optionally verify arguments (e.g., matching invoice.id, customer or a dunning
template) to ensure handleInvoicePaymentFailed triggers the queue send.

In `@packages/workers/src/routes/contact.ts`:
- Around line 179-207: The HTML promises reply-by-email but the outgoing payload
never sets a reply address; add a replyTo field through the email stack: extend
the EmailPayload type in EmailPayload (packages/workers/src/lib/email-queue.ts)
to include replyTo?: string, update SendEmailParams in the auth email module
(SendEmailParams in packages/workers/src/auth/email.ts) to accept replyTo?:
string, ensure the Postmark/Postmark client call that sends mail (the function
that maps SendEmailParams to the Postmark API request in
packages/workers/src/auth/email.ts) includes replyTo when constructing the API
payload, and finally pass replyTo: email (the submitter address) when calling
queueEmail from contact.ts (the queueEmail(...) invocation shown) so replies
route back to the form submitter.

---

Nitpick comments:
In `@packages/workers/src/__tests__/helpers.ts`:
- Around line 384-394: The default mockQueue uses plain async functions which
prevents call assertions; update the mockQueue in createTestEnv (symbols:
mockQueue, EMAIL_QUEUE) to use vi.fn() for send and sendBatch so tests can
assert calls (e.g., EMAIL_QUEUE.send). Import vi from 'vitest' at the top of the
test helper and replace the async arrow functions with vi.fn(async () => ...) or
vi.fn(). Note that this introduces a test-only dependency (vitest) into the
helper, so ensure the helper remains test-scoped or conditionally import vi if
needed.

In `@packages/workers/src/lib/send-invitation-email.ts`:
- Around line 49-54: Replace the dynamic in-function imports with top-level
static imports to reduce cold-start latency: remove the await import(...) calls
inside send-invitation-email and instead import betterAuth, magicLink,
drizzleAdapter, drizzle, schema, and MAGIC_LINK_EXPIRY_MINUTES as normal ES
module imports at the module scope (referencing the same symbols betterAuth,
magicLink, drizzleAdapter, drizzle, schema, MAGIC_LINK_EXPIRY_MINUTES) and
update any usages in the function to use those statically imported bindings.

In `@packages/workers/src/routes/billing/__tests__/invoiceHandlers.test.ts`:
- Around line 266-287: The test should also assert that the email queue's send
method was not invoked when there is no user; add an assertion using the
existing mockEmailQueue (mockEmailQueue.send) after calling
handleInvoicePaymentFailed to verify it was never called, keeping the rest of
the test (invoice object, ctx creation, and assertions on ctx.logger.stripe and
result.handled) unchanged.

In `@packages/workers/src/routes/billing/handlers/dunning.ts`:
- Around line 99-100: Replace the dynamic runtime import of queueEmail with a
static import: add a top-level import for queueEmail (import { queueEmail } from
'../../../lib/email-queue') and remove the await import(...) usage so you call
queueEmail(env, emailPayload) directly; update any references in the dunning
handler (where queueEmail is currently dynamically imported) to use the
statically imported symbol.

In `@packages/workers/src/routes/billing/handlers/invoiceHandlers.ts`:
- Line 202: Replace the action-focused inline comment "// Queue dunning email"
with an intent-focused comment that explains why dunning is queued (for example:
"Queue dunning email to keep webhook processing non-blocking and resilient,
retrying delivery asynchronously") or remove the comment if the code is
self-explanatory; update the comment near the dunning queueing call in
invoiceHandlers.ts so it documents intent/context/assumptions rather than
restating the action.

In `@packages/workers/src/routes/health.ts`:
- Around line 120-138: Add a health check for the Cloudflare Queue binding by
verifying c.env.EMAIL_QUEUE and adding a new entry under checks.queues (or
checks.services.queues) similar to the Durable Objects check: set status to
'healthy' when c.env.EMAIL_QUEUE is present, include type 'Queue' and
bindings.EMAIL_QUEUE flag, and mark overall checks.status 'degraded' on failure;
then update the ServiceStatusSchema, HealthResponseSchema, and the HealthChecks
interface to include the new queues property so the response type covers queue
status. Target symbols: checks (in the health route), c.env.EMAIL_QUEUE,
ServiceStatusSchema, HealthResponseSchema, and HealthChecks.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c1e28e3 and 11ef514.

⛔ Files ignored due to path filters (2)
  • packages/docs/audits/durable-objects-migration-review-2026-02.md is excluded by !packages/docs/audits/**
  • packages/docs/audits/prod-audit-2026-01-19-full.md is excluded by !packages/docs/audits/**
📒 Files selected for processing (32)
  • .cursor/rules/durable-objects.mdc
  • .mcp/memory.db-wal
  • packages/docs/STATUS.md
  • packages/docs/architecture/diagrams/02-system-architecture.md
  • packages/web/src/components/project/overview-tab/OverviewTab.jsx
  • packages/web/src/components/project/reconcile-tab/rob2-reconcile/pages/PreliminaryPage.jsx
  • packages/workers/src/__tests__/health.test.js
  • packages/workers/src/__tests__/helpers.ts
  • packages/workers/src/auth/config.ts
  • packages/workers/src/auth/email.ts
  • packages/workers/src/config/constants.ts
  • packages/workers/src/durable-objects/EmailQueue.ts
  • packages/workers/src/durable-objects/ProjectDoc.ts
  • packages/workers/src/durable-objects/UserSession.ts
  • packages/workers/src/durable-objects/__tests__/EmailQueue.test.js
  • packages/workers/src/index.ts
  • packages/workers/src/lib/email-queue.ts
  • packages/workers/src/lib/send-invitation-email.ts
  • packages/workers/src/routes/__tests__/contact.test.js
  • packages/workers/src/routes/__tests__/email.test.js
  • packages/workers/src/routes/__tests__/project-invitations.test.js
  • packages/workers/src/routes/account-merge.ts
  • packages/workers/src/routes/billing/__tests__/invoiceHandlers.test.ts
  • packages/workers/src/routes/billing/handlers/dunning.ts
  • packages/workers/src/routes/billing/handlers/invoiceHandlers.ts
  • packages/workers/src/routes/contact.ts
  • packages/workers/src/routes/email.ts
  • packages/workers/src/routes/health.ts
  • packages/workers/src/routes/members.ts
  • packages/workers/src/routes/orgs/invitations.ts
  • packages/workers/src/routes/orgs/members.ts
  • packages/workers/wrangler.jsonc
💤 Files with no reviewable changes (8)
  • packages/workers/src/routes/tests/email.test.js
  • .cursor/rules/durable-objects.mdc
  • packages/workers/src/routes/email.ts
  • packages/workers/src/config/constants.ts
  • packages/workers/src/tests/health.test.js
  • packages/workers/src/auth/email.ts
  • packages/workers/src/durable-objects/tests/EmailQueue.test.js
  • packages/workers/src/durable-objects/EmailQueue.ts

Comment on lines +303 to +313
<TooltipTrigger
asChild={item => (
<span
{...item}
class='bg-secondary text-muted-foreground/70 inline-flex cursor-not-allowed items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium'
>
<FiPlus class='h-4 w-4' />
Invite
</span>
)}
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

@ark-ui/solid TooltipTrigger asChild render prop API documentation

💡 Result:

In @ark-ui/solid, the tooltip “trigger” part is Tooltip.Trigger (sometimes referred to as TooltipTrigger in other libs). It supports:

asChild (composition)

  • Prop: asChild?: boolean
  • Meaning: When true, Ark won’t render its default trigger element; instead it merges the trigger props/behavior onto your single child element (your button/link/component). Use exactly one child. [1][2]

Solid example

import { Tooltip } from '@ark-ui/solid/tooltip'

export function Example() {
  return (
    <Tooltip.Root>
      <Tooltip.Trigger asChild>
        <a href="/docs">Docs</a>
      </Tooltip.Trigger>

      <Tooltip.Positioner>
        <Tooltip.Content>Go to docs</Tooltip.Content>
      </Tooltip.Positioner>
    </Tooltip.Root>
  )
}

[1][2]

“Render prop” API (Tooltip state via function child)

Ark UI’s tooltip “render function” pattern is exposed via Tooltip.Context (not Tooltip.Trigger). Tooltip.Context takes a function child that receives the tooltip API (e.g. open). [1]

Solid example

<Tooltip.Positioner>
  <Tooltip.Context>
    {(api) => <Tooltip.Content>Open: {String(api.open)}</Tooltip.Content>}
  </Tooltip.Context>
</Tooltip.Positioner>

[1]

Sources: Ark UI Tooltip (Solid) docs [1], Ark UI Composition / asChild guide [2].

[1] https://ark-ui.com/solid/docs/components/tooltip
[2] https://ark-ui.com/react/docs/guides/composition


Replace asChild render function with boolean pattern.

The asChild prop in @ark-ui/solid's Tooltip.Trigger must be a boolean, not a render function. According to the library documentation, asChild={true} merges trigger behavior onto a single child element. Replace the render function pattern with:

<TooltipTrigger asChild>
  <span
    class='bg-secondary text-muted-foreground/70 inline-flex cursor-not-allowed items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium'
  >
    <FiPlus class='h-4 w-4' />
    Invite
  </span>
</TooltipTrigger>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/components/project/overview-tab/OverviewTab.jsx` around
lines 303 - 313, The TooltipTrigger usage incorrectly passes a render function
to the asChild prop; change TooltipTrigger (from `@ark-ui/solid`) to use the
boolean pattern by setting asChild (e.g., asChild or asChild={true}) and move
the span element inside TooltipTrigger as its single child, removing the
render-function signature and the {...item} spread; keep the existing span
classes and FiPlus element so the trigger behavior is merged onto that span.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
packages/workers/src/index.ts (2)

399-405: Duplicated retry delay calculation.

The exponential backoff formula Math.min(30 * 2 ** msg.attempts, 1800) is repeated in both error paths. Extract to a helper for maintainability.

Proposed fix
+      const getRetryDelay = (attempts: number) => Math.min(30 * 2 ** attempts, 1800);
+
       for (const msg of messages) {
         try {
           // ...
           } else {
             console.error(`[EmailQueue] Send returned error for ${msg.body.to}:`, result.error);
-            const delay = Math.min(30 * 2 ** msg.attempts, 1800);
-            msg.retry({ delaySeconds: delay });
+            msg.retry({ delaySeconds: getRetryDelay(msg.attempts) });
           }
         } catch (error) {
           console.error(`[EmailQueue] Exception sending to ${msg.body.to}:`, error);
-          const delay = Math.min(30 * 2 ** msg.attempts, 1800);
-          msg.retry({ delaySeconds: delay });
+          msg.retry({ delaySeconds: getRetryDelay(msg.attempts) });
         }
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/workers/src/index.ts` around lines 399 - 405, Extract the duplicated
exponential backoff calculation into a single helper function (e.g.,
getRetryDelay or computeRetryDelay) and use it wherever the code calls
Math.min(30 * 2 ** msg.attempts, 1800); specifically replace the inline
expression in the try/catch blocks that call msg.retry({ delaySeconds: ... })
with msg.retry({ delaySeconds: getRetryDelay(msg.attempts) }) and import/define
the helper near the email processing logic (references: msg.retry, msg.attempts,
and the surrounding send/email handling in packages/workers/src/index.ts).

384-408: Add type annotation to batch parameter and import MessageBatch type.

The batch parameter lacks a type annotation. For better type safety and IDE support, explicitly type it as MessageBatch<EmailPayload> from @cloudflare/workers-types.

Proposed fix
-    async queue(batch, env: Env): Promise<void> {
+    async queue(batch: MessageBatch<EmailPayload>, env: Env): Promise<void> {
       const emailService = createEmailService(env);
-      // eslint-disable-next-line no-undef -- Cloudflare Workers global type
-      const messages = batch.messages as Message<EmailPayload>[];
+      const messages = batch.messages;

Add the import at the top of the file:

import type { MessageBatch } from '@cloudflare/workers-types';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/workers/src/index.ts` around lines 384 - 408, The queue function's
batch parameter should be explicitly typed to improve type safety: import the
MessageBatch generic from '@cloudflare/workers-types' and change the queue
signature to accept batch: MessageBatch<EmailPayload> (so references like
messages: Message<EmailPayload>[] still type-check); add the import line for
MessageBatch at the top of the file and ensure EmailPayload is the same type
used elsewhere (e.g., in the messages cast and createEmailService usage).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/workers/src/index.ts`:
- Around line 398-405: The logs currently print plaintext recipient addresses
(msg.body.to) which exposes PII; create a small helper (e.g., maskEmail or
hashEmail) and use it where the code logs errors in the send/exception paths to
replace msg.body.to with a masked or hashed representation, then update both
console.error calls that reference msg.body.to to log the masked value instead
so addresses are never emitted in plaintext (keep masking deterministic if you
need correlation across logs, or truncate/anonymize for simple privacy).
- Around line 391-393: The call site is using a type cast to force EmailPayload
into SendEmailParams; instead either make SendEmailParams match EmailPayload
(make html and text optional) or tighten queueEmail to guarantee both fields and
keep SendEmailParams as-is—pick one and remove the unsafe `as` cast in the
emailService.sendEmail call. Locate types EmailPayload and SendEmailParams and
either relax SendEmailParams to optional html/text or add runtime validation in
queueEmail (and its callers) to ensure both fields are present, update the
function signatures accordingly, and remove the cast so TypeScript enforces
correctness without bypassing checks.

---

Nitpick comments:
In `@packages/workers/src/index.ts`:
- Around line 399-405: Extract the duplicated exponential backoff calculation
into a single helper function (e.g., getRetryDelay or computeRetryDelay) and use
it wherever the code calls Math.min(30 * 2 ** msg.attempts, 1800); specifically
replace the inline expression in the try/catch blocks that call msg.retry({
delaySeconds: ... }) with msg.retry({ delaySeconds: getRetryDelay(msg.attempts)
}) and import/define the helper near the email processing logic (references:
msg.retry, msg.attempts, and the surrounding send/email handling in
packages/workers/src/index.ts).
- Around line 384-408: The queue function's batch parameter should be explicitly
typed to improve type safety: import the MessageBatch generic from
'@cloudflare/workers-types' and change the queue signature to accept batch:
MessageBatch<EmailPayload> (so references like messages: Message<EmailPayload>[]
still type-check); add the import line for MessageBatch at the top of the file
and ensure EmailPayload is the same type used elsewhere (e.g., in the messages
cast and createEmailService usage).

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 11ef514 and a6c7c61.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (8)
  • package.json
  • packages/landing/package.json
  • packages/mcp-memory/package.json
  • packages/mcp/package.json
  • packages/web/package.json
  • packages/workers/package.json
  • packages/workers/src/index.ts
  • packages/workers/wrangler.jsonc
✅ Files skipped from review due to trivial changes (2)
  • packages/landing/package.json
  • package.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/workers/wrangler.jsonc
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: Workers Builds: corates
  • GitHub Check: Workers Builds: corates-workers-prod
  • GitHub Check: prettier
🧰 Additional context used
📓 Path-based instructions (9)
**/*.{js,jsx,ts,tsx}

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

**/*.{js,jsx,ts,tsx}: Use the useProject hook for managing Yjs connections with reference counting instead of creating Y.Doc instances directly
Never create Y.Doc instances directly; always use the connection registry managed by useProject
Access Y.Doc connection state via projectStore.getConnectionState(projectId) instead of checking connection state directly
Read Yjs-synced data from projectStore (using getStudies, getChecklist, etc.) rather than accessing Y.Doc maps/arrays directly
Use operation functions from useProject or projectActionsStore for writing data instead of directly modifying Y.Doc
Don't store Y.Doc references in component state; always retrieve the connection through useProject
Handle connection cleanup via onCleanup() in Solid.js components or equivalent cleanup patterns, calling disconnect() when the component unmounts

Files:

  • packages/workers/src/index.ts
packages/workers/src/**/*.{js,ts,jsx,tsx}

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

ALWAYS use db.batch() for multiple related database operations in Drizzle to ensure atomicity - all operations must succeed or all fail together

Files:

  • packages/workers/src/index.ts
packages/workers/**/*.{js,ts}

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

packages/workers/**/*.{js,ts}: Always use createDomainError from @corates/shared for backend error handling instead of manually creating error objects
Use predefined error constants from @corates/shared (PROJECT_ERRORS, AUTH_ERRORS, VALIDATION_ERRORS, SYSTEM_ERRORS, USER_ERRORS) instead of arbitrary error strings
Wrap database operations in try-catch blocks and return domain errors using createDomainError with SYSTEM_ERRORS.DB_ERROR
Use validation middleware (validateRequest) for request validation instead of manually validating request bodies in routes

Files:

  • packages/workers/src/index.ts
packages/{web,workers}/**/*.{js,ts,jsx,tsx}

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

Never throw string literals; always throw Error objects or return domain errors from API routes

Files:

  • packages/workers/src/index.ts
**/*.{js,ts,jsx,tsx}

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

**/*.{js,ts,jsx,tsx}: Prefer modern ES6+ syntax and features
Comments should explain why something is being done, not what the code is doing
Reserve comments for explaining intent, context, workarounds, assumptions, edge cases, or limitations
Use TODO(agent): prefix for incomplete work or flagged items with brief description and relevant doc references

Files:

  • packages/workers/src/index.ts
packages/workers/src/**/*.{ts,tsx}

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

packages/workers/src/**/*.{ts,tsx}: Use Zod for schema and input validation on backend services
Use Drizzle ORM for all database interactions and migrations
Use Better-Auth for authentication and user management
All project routes are org-scoped and use /api/orgs/:orgId/projects/... pattern in backend
Use requireOrgMembership and requireProjectAccess middleware for authentication in org-scoped routes
Never bypass Drizzle for database access

Never bypass Drizzle for database access - use Drizzle ORM for all database operations

Files:

  • packages/workers/src/index.ts
**/*

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Never use emojis or unicode symbols anywhere in code, comments, documentation, plan files, commit messages, or examples

Files:

  • packages/workers/src/index.ts
  • packages/web/package.json
  • packages/workers/package.json
  • packages/mcp/package.json
  • packages/mcp-memory/package.json
packages/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

packages/**/*.{ts,tsx,js,jsx}: Prefer modern ES6+ syntax and features
Prefer config files over hardcoding values
Keep files small, focused, and modular - extract large files into sub-modules or separate utilities
Each file should handle one coherent responsibility
Comments should explain WHY something is being done or provide context, not repeat what the code is saying
Do NOT narrate what code is doing, don't duplicate function/variable names in comments, and don't leave stale comments that contradict the code
Use the Agent TODO convention // TODO(agent): Brief description for incomplete work, flagging items for future attention, known limitations, and documentation section references

Use TODO(agent) convention with brief description and relevant doc section references for incomplete work or flagged issues

Files:

  • packages/workers/src/index.ts
packages/workers/src/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

packages/workers/src/**/*.ts: Use Zod for schema and input validation in backend code
Use Drizzle ORM for ALL database interactions and migrations
Use Better-Auth for authentication and user management
Never bypass Drizzle for database access

Files:

  • packages/workers/src/index.ts
🧠 Learnings (29)
📚 Learning: 2025-12-27T03:01:35.601Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/durable-objects.mdc:0-0
Timestamp: 2025-12-27T03:01:35.601Z
Learning: Applies to packages/workers/src/durable-objects/**/ProjectDoc.{js,ts} : Use y-websocket protocol with message type 1 for awareness (presence) messages in ProjectDoc

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-17T00:25:35.716Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/corates.mdc:0-0
Timestamp: 2026-01-17T00:25:35.716Z
Learning: Applies to packages/workers/src/**/*.{ts,tsx} : Use Better-Auth for authentication and user management

Applied to files:

  • packages/workers/src/index.ts
  • packages/workers/package.json
📚 Learning: 2026-01-05T01:00:58.113Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/workers-testing.mdc:0-0
Timestamp: 2026-01-05T01:00:58.113Z
Learning: Applies to {packages/workers/src/**/*.test.js,packages/workers/src/**/__tests__/**/*.js} : Only re-mock Postmark and Stripe modules inside a test file if you need behavior different from the global mock setup in packages/workers/src/__tests__/setup.js

Applied to files:

  • packages/workers/src/index.ts
  • packages/workers/package.json
📚 Learning: 2025-12-27T03:01:35.601Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/durable-objects.mdc:0-0
Timestamp: 2025-12-27T03:01:35.601Z
Learning: Applies to packages/workers/src/durable-objects/**/*.{js,ts} : Remove WebSocket sessions from tracking on disconnect and perform cleanup

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2025-12-27T03:01:35.601Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/durable-objects.mdc:0-0
Timestamp: 2025-12-27T03:01:35.601Z
Learning: Applies to packages/workers/src/durable-objects/**/*.{js,ts} : Do not expose internal sync endpoints (/sync, /sync-member, /sync-pdf, /disconnect-all) publicly without X-Internal-Request verification

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-01T23:31:43.756Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/api-routes.mdc:0-0
Timestamp: 2026-01-01T23:31:43.756Z
Learning: Applies to packages/workers/src/routes/**/*.js : Use error constants from `corates/shared` (`PROJECT_ERRORS.*`, `AUTH_ERRORS.*`, `VALIDATION_ERRORS.*`, `SYSTEM_ERRORS.*`, `USER_ERRORS.*`) instead of creating error objects manually

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-01T23:31:43.756Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/api-routes.mdc:0-0
Timestamp: 2026-01-01T23:31:43.756Z
Learning: Applies to packages/workers/src/routes/**/*.js : ALWAYS use `createDomainError` from `corates/shared` for error handling in API routes

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-17T00:25:03.945Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/instructions/api-routes.instructions.md:0-0
Timestamp: 2026-01-17T00:25:03.945Z
Learning: Applies to packages/workers/**/routes/**/*.{js,ts} : Use error helpers from `corates/shared` (`notFound`, `forbidden`, `badRequest`, etc.) for error handling in API routes

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-17T00:25:35.716Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/corates.mdc:0-0
Timestamp: 2026-01-17T00:25:35.716Z
Learning: Applies to packages/workers/src/**/*.{ts,tsx} : Use `requireOrgMembership` and `requireProjectAccess` middleware for authentication in org-scoped routes

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-01T23:31:43.756Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/api-routes.mdc:0-0
Timestamp: 2026-01-01T23:31:43.756Z
Learning: Applies to packages/workers/src/routes/**/*.js : Apply authentication and authorization middleware in correct order: Authentication → Organization membership → Project access → Authorization → Validation → Route handler

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-05T01:00:58.113Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/workers-testing.mdc:0-0
Timestamp: 2026-01-05T01:00:58.113Z
Learning: Applies to {packages/workers/src/**/*.test.js,packages/workers/src/**/__tests__/**/*.js} : Mock src/middleware/auth.js requireAuth function in route tests by setting user/session via x-test-user-id and x-test-user-email headers

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-01T23:32:23.488Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/workers.mdc:0-0
Timestamp: 2026-01-01T23:32:23.488Z
Learning: Applies to packages/workers/src/routes/orgs/**/*.{js,ts,jsx,tsx} : Use `requireOrgMembership` and `requireProjectAccess` middleware instead of manual membership checks for org-scoped routes

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-01T23:31:43.756Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/api-routes.mdc:0-0
Timestamp: 2026-01-01T23:31:43.756Z
Learning: Applies to packages/workers/src/routes/orgs/**/*.js : Use organization-scoped route patterns with path structure `/api/orgs/:orgId/...` and leverage `requireOrgMembership`, `requireProjectAccess`, `getOrgContext`, and `getProjectContext` middleware

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-17T00:25:35.716Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/corates.mdc:0-0
Timestamp: 2026-01-17T00:25:35.716Z
Learning: Applies to packages/workers/src/**/*.{ts,tsx} : All project routes are org-scoped and use `/api/orgs/:orgId/projects/...` pattern in backend

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-01T23:32:23.488Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/workers.mdc:0-0
Timestamp: 2026-01-01T23:32:23.488Z
Learning: Applies to packages/workers/src/routes/orgs/**/*.{js,ts,jsx,tsx} : Use `getOrgContext(c)` to retrieve the validated orgId from context instead of manual checks - orgId is guaranteed valid by middleware

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-17T00:25:35.716Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/corates.mdc:0-0
Timestamp: 2026-01-17T00:25:35.716Z
Learning: Applies to packages/web/src/**/*.{ts,tsx} : Frontend uses orgSlug in URLs (`/orgs/:orgSlug/...`) for readability

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-05T01:00:58.113Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/workers-testing.mdc:0-0
Timestamp: 2026-01-05T01:00:58.113Z
Learning: Applies to {packages/workers/src/**/*.test.js,packages/workers/src/**/__tests__/**/*.js} : For integration tests, fetch against real worker env using app.fetch(req, env, ctx) with env from cloudflare:test, or use fetchApp() helper

Applied to files:

  • packages/workers/src/index.ts
  • packages/workers/package.json
📚 Learning: 2025-12-27T03:02:05.951Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/pdf-handling.mdc:0-0
Timestamp: 2025-12-27T03:02:05.951Z
Learning: Applies to packages/web/src/**/*.{js,jsx,ts,tsx} : Use PDF operations from `useProject` hook instead of bypassing through direct API calls

Applied to files:

  • packages/web/package.json
📚 Learning: 2026-01-17T16:09:36.920Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-17T16:09:36.920Z
Learning: Applies to packages/{web,landing}/src/**/*.{ts,tsx} : Use Ark UI components from `ark-ui/solid` for UI components

Applied to files:

  • packages/web/package.json
📚 Learning: 2026-01-17T00:25:35.716Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/corates.mdc:0-0
Timestamp: 2026-01-17T00:25:35.716Z
Learning: Applies to packages/web/src/**/*.{ts,tsx} : Use Ark UI component library (`ark-ui/solid`) for UI components

Applied to files:

  • packages/web/package.json
📚 Learning: 2026-01-17T16:09:36.920Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-17T16:09:36.920Z
Learning: Applies to packages/{web,landing}/src/**/*.{ts,tsx} : Use `solid-icons` icon library for all icon needs

Applied to files:

  • packages/web/package.json
📚 Learning: 2026-01-17T00:25:35.716Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/corates.mdc:0-0
Timestamp: 2026-01-17T00:25:35.716Z
Learning: Applies to packages/web/src/**/*.{ts,tsx} : Use `solid-icons` library for icons (e.g., `solid-icons/bi`, `solid-icons/fi`)

Applied to files:

  • packages/web/package.json
📚 Learning: 2025-12-27T03:02:05.951Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/pdf-handling.mdc:0-0
Timestamp: 2025-12-27T03:02:05.951Z
Learning: Applies to packages/web/src/**/*.{jsx,tsx} : Use `PdfViewer` component from `@/components/checklist-ui/pdf/PdfViewer.jsx` for displaying PDFs, passing pdfData as ArrayBuffer, fileName, readOnly, and onPageChange

Applied to files:

  • packages/web/package.json
📚 Learning: 2025-12-27T03:02:05.951Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/pdf-handling.mdc:0-0
Timestamp: 2025-12-27T03:02:05.951Z
Learning: Applies to packages/web/src/**/*.{js,jsx,ts,tsx} : Use `cachePdf` and `getCachedPdf` from `primitives/pdfCache.js` for caching PDF data in IndexedDB

Applied to files:

  • packages/web/package.json
📚 Learning: 2026-01-17T16:09:36.920Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-17T16:09:36.920Z
Learning: Applies to packages/workers/src/**/*.ts : Use Better-Auth for authentication and user management

Applied to files:

  • packages/workers/package.json
📚 Learning: 2026-01-05T01:00:58.113Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/workers-testing.mdc:0-0
Timestamp: 2026-01-05T01:00:58.113Z
Learning: Applies to {packages/workers/src/**/*.test.js,packages/workers/src/**/__tests__/**/*.js} : Tests in packages/workers must run under cloudflare/vitest-pool-workers with global setup from packages/workers/src/__tests__/setup.js

Applied to files:

  • packages/workers/package.json
  • packages/mcp/package.json
  • packages/mcp-memory/package.json
📚 Learning: 2026-01-05T01:00:58.113Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/workers-testing.mdc:0-0
Timestamp: 2026-01-05T01:00:58.113Z
Learning: Applies to {packages/workers/src/**/*.test.js,packages/workers/src/**/__tests__/**/*.js} : Reuse provided helpers from packages/workers/src/__tests__/helpers.js (resetTestDatabase, clearProjectDOs, seedUser, seedOrganization, createTestEnv, fetchApp, json, createAuthHeaders) instead of implementing ad-hoc solutions

Applied to files:

  • packages/workers/package.json
  • packages/mcp-memory/package.json
📚 Learning: 2026-01-17T00:25:35.716Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/corates.mdc:0-0
Timestamp: 2026-01-17T00:25:35.716Z
Learning: Applies to packages/workers/src/**/*.{ts,tsx} : Use Zod for schema and input validation on backend services

Applied to files:

  • packages/workers/package.json
  • packages/mcp/package.json
  • packages/mcp-memory/package.json
📚 Learning: 2026-01-17T16:09:36.920Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-17T16:09:36.920Z
Learning: Applies to packages/workers/src/**/*.ts : Use Zod for schema and input validation in backend code

Applied to files:

  • packages/mcp/package.json
🧬 Code graph analysis (1)
packages/workers/src/index.ts (2)
packages/workers/src/auth/email.ts (1)
  • createEmailService (49-148)
packages/workers/src/lib/email-queue.ts (1)
  • EmailPayload (10-15)
🔇 Additional comments (7)
packages/workers/package.json (1)

34-56: Dependency version bumps look reasonable.

These are all minor/patch version bumps that should be backward compatible. The @sentry/cloudflare update aligns with the commit message about fixing Sentry typing.

packages/mcp/package.json (1)

23-23: LGTM — routine dependency bumps.

Both version updates are minor/patch increments following semver conventions. The @types/node dev dependency change is low-risk. Consider verifying the @modelcontextprotocol/sdk version to ensure compatibility with existing code.

Also applies to: 27-27

packages/web/package.json (1)

22-50: Dependency version alignment looks good.

The @embedpdf package set is consistently bumped to ^2.7.0, and the @sentry/solid bump is isolated and clean with no blocking issues in this segment.

packages/mcp-memory/package.json (2)

22-22: Dependency bump for @modelcontextprotocol/sdk is clean.

This is a scoped, version-only update and looks good in this file context.


29-29: @types/node patch update looks safe.

Dev dependency update is straightforward and consistent with the rest of the package manifest.

packages/workers/src/index.ts (2)

19-20: LGTM!

Imports are appropriate for the new queue-based email processing flow.


38-38: LGTM!

Export change correctly reflects the removal of the EmailQueue Durable Object.

Comment thread packages/workers/src/index.ts Outdated
Comment on lines +391 to +393
const result = await emailService.sendEmail(
msg.body as Parameters<typeof emailService.sendEmail>[0],
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's see the context around lines 391-393 in index.ts
sed -n '385,400p' packages/workers/src/index.ts

Repository: InfinityBowman/corates

Length of output: 735


🏁 Script executed:

# Find EmailPayload definition
rg -n "type EmailPayload|interface EmailPayload" --type ts

Repository: InfinityBowman/corates

Length of output: 140


🏁 Script executed:

# Find sendEmail function definition
rg -n "function sendEmail|const sendEmail|export.*sendEmail" packages/workers --type ts

Repository: InfinityBowman/corates

Length of output: 278


🏁 Script executed:

# Check queueEmail calls as suggested in the review
rg -n -A5 "queueEmail\(" --type ts

Repository: InfinityBowman/corates

Length of output: 4160


🏁 Script executed:

# Get the full EmailPayload interface definition
sed -n '10,20p' packages/workers/src/lib/email-queue.ts

Repository: InfinityBowman/corates

Length of output: 399


🏁 Script executed:

# Check SendEmailParams type as well
rg -n -B2 -A5 "type SendEmailParams|interface SendEmailParams" --type ts

Repository: InfinityBowman/corates

Length of output: 502


🏁 Script executed:

# Verify if messages are only queued via queueEmail or could be queued directly
rg -n "batch.publish|\.publish" packages/workers/src/index.ts -A3 -B3

Repository: InfinityBowman/corates

Length of output: 48


Type mismatch between EmailPayload and SendEmailParams requires runtime validation fallback.

EmailPayload defines html and text as optional (either one can be provided), but sendEmail requires both fields. The as cast bypasses TypeScript's type checking. However, queueEmail provides runtime protection by validating that at least one field exists before queuing. To eliminate the type cast and improve type safety, align the interfaces: either update SendEmailParams to match EmailPayload's optional fields, or enforce that queueEmail requires both fields.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/workers/src/index.ts` around lines 391 - 393, The call site is using
a type cast to force EmailPayload into SendEmailParams; instead either make
SendEmailParams match EmailPayload (make html and text optional) or tighten
queueEmail to guarantee both fields and keep SendEmailParams as-is—pick one and
remove the unsafe `as` cast in the emailService.sendEmail call. Locate types
EmailPayload and SendEmailParams and either relax SendEmailParams to optional
html/text or add runtime validation in queueEmail (and its callers) to ensure
both fields are present, update the function signatures accordingly, and remove
the cast so TypeScript enforces correctness without bypassing checks.

Comment thread packages/workers/src/index.ts Outdated
Comment thread packages/workers/src/lib/send-invitation-email.ts
Comment thread packages/workers/src/routes/contact.ts
Comment thread packages/workers/src/auth/config.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

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

⚠️ Outside diff range comments (3)
packages/web/src/primitives/useProject/index.js (1)

265-289: ⚠️ Potential issue | 🟠 Major

After corruption detection, code continues with broken persistence references.

When corrupted state is detected, deleteProjectData(projectId) is called but execution continues to set up the updateHandler (line 280) which captures project.ydoc. Since the project row was just deleted, future updates via Y.applyUpdate(project.ydoc, update, 'dexie-sync') at line 283 will reference a deleted document, potentially throwing errors or silently failing.

For local-only projects this is especially problematic—all data is silently lost, but the persistence handler still tries to write to the deleted document.

Consider returning early after corruption to force re-initialization:

Proposed fix: Return early after corruption to trigger re-initialization
         try {
           const persistedState = Y.encodeStateAsUpdate(project.ydoc);
           Y.applyUpdate(ydoc, persistedState);
         } catch (err) {
           console.error('Corrupted persisted state, clearing local data:', err);
-          deleteProjectData(projectId).catch(() => {});
+          deleteProjectData(projectId).catch(delErr => {
+            console.error('Failed to delete corrupted project data:', delErr);
+          });
+          // Re-create the project row so persistence can restart cleanly
+          db.projects.put({ id: projectId, updatedAt: Date.now() }).catch(() => {});
+          // Exit early - syncFromYDoc will run with empty state, persistence will work with fresh row
+          isLoadingPersistedState = false;
+          connectionEntry.syncManager.syncFromYDoc();
+          if (isLocalProject()) {
+            projectStore.setConnectionState(projectId, {
+              connecting: false,
+              connected: true,
+              synced: true,
+            });
+          }
+          return;
         } finally {
           isLoadingPersistedState = false;
         }

Alternatively, wrap the entire post-load setup in the try block so it only runs on success.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/primitives/useProject/index.js` around lines 265 - 289, When
corrupted persisted state is detected in the whenLoaded handler, stop further
setup so we don't attach persistence handlers that reference a deleted project;
after calling deleteProjectData(projectId) return early (or rethrow) to avoid
running the rest of the post-load setup (the updateHandler closure that calls
Y.applyUpdate(project.ydoc, update, 'dexie-sync'), ydoc.on('update', ...), and
connectionEntry.syncManager.syncFromYDoc), or alternatively move the post-load
setup (creation of updateHandler, ydoc.on('update', ...) and syncFromYDoc call)
inside the try block so they only run when Y.encodeStateAsUpdate/project.ydoc
application succeeds.
packages/workers/src/routes/billing/__tests__/subscriptionHandlers.test.ts (1)

355-387: ⚠️ Potential issue | 🟡 Minor

Assert the resumed period dates, not just the status.

This fixture was updated to the new items.data[0].current_period_* shape, but the test only checks updated.status. Please also verify that periodStart and periodEnd were persisted from those nested fields so the regression this PR is fixing is actually covered.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/workers/src/routes/billing/__tests__/subscriptionHandlers.test.ts`
around lines 355 - 387, The test only checks status; also assert that the
resumed period dates from
stripeSub.items.data[0].current_period_start/current_period_end were persisted
via handleSubscriptionResumed: read the two values from the fixture
(items.data[0].current_period_start and current_period_end), convert them to the
DB-stored format (e.g., JS Date by multiplying by 1000 if DB uses Date), then
add expectations against the fetched updated record's periodStart and periodEnd
fields to ensure they match the converted expectedStart and expectedEnd.
packages/workers/src/routes/billing/handlers/subscriptionHandlers.ts (1)

144-157: ⚠️ Potential issue | 🟠 Major

Preserve existing billing dates when item periods are missing.

On Line 149 and Line 152, a missing firstItem.current_period_* now clears periodStart and periodEnd to null. For an existing subscription row, that silently loses billing state if Stripe sends a partial item payload. Please fall back to the stored values here, and apply the same guard in handleSubscriptionResumed().

Proposed fix
   await db
     .update(subscription)
     .set({
       status: sub.status,
       periodStart:
-        firstItem?.current_period_start ? new Date(firstItem.current_period_start * 1000) : null,
+        firstItem?.current_period_start
+          ? new Date(firstItem.current_period_start * 1000)
+          : existing.periodStart,
       periodEnd:
-        firstItem?.current_period_end ? new Date(firstItem.current_period_end * 1000) : null,
+        firstItem?.current_period_end
+          ? new Date(firstItem.current_period_end * 1000)
+          : existing.periodEnd,
       cancelAtPeriodEnd: sub.cancel_at_period_end,
       cancelAt: sub.cancel_at ? new Date(sub.cancel_at * 1000) : null,
       canceledAt: sub.canceled_at ? new Date(sub.canceled_at * 1000) : null,
-      plan: sub.items.data[0]?.price?.lookup_key || existing.plan,
+      plan: firstItem?.price?.lookup_key || existing.plan,
       updatedAt: new Date(),
     })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/workers/src/routes/billing/handlers/subscriptionHandlers.ts` around
lines 144 - 157, The update is overwriting periodStart/periodEnd with null when
firstItem.current_period_start/current_period_end are missing; change the
db.update(...) .set mapping to fall back to the existing subscription row values
(e.g., use existing.periodStart and existing.periodEnd) instead of null so
missing item periods don’t wipe stored billing dates, and make the same change
inside handleSubscriptionResumed() to guard against partial Stripe item
payloads.
🧹 Nitpick comments (3)
packages/web/src/primitives/useProject/connection.js (1)

86-106: Good defensive enhancement for awareness resilience.

The try/catch wrapper appropriately isolates ephemeral awareness failures from breaking the provider. The array clone before mutation is correct.

Minor hardening: consider accessing the error message defensively in case a non-Error is thrown.

Defensive error message access
       } catch (err) {
-        console.warn('Awareness update skipped:', err.message);
+        console.warn('Awareness update skipped:', err instanceof Error ? err.message : err);
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/primitives/useProject/connection.js` around lines 86 - 106,
The catch block in the AWARENESS_MSG_TYPE wrapper assumes thrown err has a
message property; update the catch to defensively derive a string (e.g., const
msg = (err && err.message) || String(err)) and use that in the console.warn so
non-Error throws are handled; modify the wrapper around originalAwarenessHandler
(provider.messageHandlers[AWARENESS_MSG_TYPE]) to compute a safe message and
optionally include the full err object in the log for debugging.
.cursor/mcp.json (1)

19-22: Consider adding explicit type field for consistency.

The sentry entry in .mcp.json includes "type": "http", but this file omits it. Adding the explicit type would maintain consistency between configuration files.

♻️ Suggested fix
     },
     "sentry": {
+      "type": "http",
       "url": "https://mcp.sentry.dev/mcp"
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.cursor/mcp.json around lines 19 - 22, The sentry configuration is missing
an explicit "type" field; update the "sentry" object to include "type": "http"
alongside the existing "url" property so it matches other config files and keeps
schema consistency (i.e., add the "type" key inside the "sentry" object that
currently contains "url").
packages/workers/src/routes/contact.ts (1)

179-210: Make the returned messageId traceable through the queue pipeline.

The UUID returned here is generated after enqueueing and is not attached to the queued payload, so clients and support cannot correlate it with queue processing or Postmark delivery logs. If you want to expose a receipt ID, generate it before enqueueing and include it in the payload/logging; otherwise the field is misleading.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/workers/src/routes/contact.ts` around lines 179 - 210, Generate the
messageId before calling queueEmail and attach it to the queued payload so the
same UUID can be correlated end-to-end; e.g., create a messageId via
crypto.randomUUID() prior to invoking queueEmail and include that messageId
field in the object passed to queueEmail, and return that same messageId from
the c.json response (the affected symbols are queueEmail and the return call to
c.json in contact.ts).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/web/package.json`:
- Line 55: Update the codebase to be compatible with better-auth 1.5.4: replace
any removed API usages (createAdapter, Adapter) and types (InferUser,
InferSession) with the new equivalents (Adapter replacements and User, Session),
change imports that used the removed barrel `@better-auth/core/utils` to explicit
subpath imports (e.g., `@better-auth/core/utils/id`), replace calls to the removed
/forget-password/email-otp flow with the standard password reset endpoints and
verify magic link/verification handlers, update plugin error code definitions to
use defineErrorCodes(), and audit code that relied on "after" DB hooks running
inside transactions (update logic to account for hooks running after commit);
search for these symbols (createAdapter, Adapter, InferUser, InferSession,
defineErrorCodes, `@better-auth/core/utils`) to locate and update all occurrences.

In
`@packages/web/src/components/project/reconcile-tab/rob2-reconcile/pages/PreliminaryPage.jsx`:
- Line 1: The preliminary NoteEditor edits are updating Y.Text but not
persisting into the reconciled checklist, so restore a bridge that calls
props.onFinalChange (which flows to updateChecklistAnswer) when the editor
content changes: add an explicit onChange (or onFinalChange) prop to
NoteEditor.jsx and invoke it from the editor change handler (in NoteEditor.jsx
around the current Y.Text update logic), then in ROB2Reconciliation.jsx wire
that prop to the existing props.onFinalChange/updateChecklistAnswer path so
props.finalValue is written into reconciledChecklist; ensure the same fix is
applied to both referenced locations (around lines ~323-329 and ~365-377) so
manual edits persist.

In `@packages/workers/src/auth/config.ts`:
- Around line 182-184: The current await queueEmail(env, { to: email, subject,
html, text }).catch(...) swallows enqueue errors; change it to await
queueEmail(env, { to: email, subject, html, text }) without a catch, or catch,
log via console.error('[Auth] Magic link email queue error:', err) and then
rethrow the error (throw err) so the failure propagates to the caller; apply the
same fix to the other two occurrences that use queueEmail in the auth email
hooks (the other blocks that currently log-and-resolve) so auth-critical flows
can surface retryable errors.

In `@packages/workers/src/lib/email-queue.ts`:
- Around line 10-24: Replace the hand-rolled truthiness check in queueEmail with
a reusable Zod schema: define a z.object schema for EmailPayload (fields to,
subject, html?, text?, replyTo?) and add a .refine(...) that enforces at least
one of html or text is present; then validate the incoming payload via
schema.parse or safeParse inside queueEmail and on failure throw
createDomainError(VALIDATION_ERRORS.INVALID_INPUT, ...) with the schema error
details instead of the manual check. Ensure the schema (e.g.,
EmailPayloadSchema) is exported/placed so other callers can reuse it across
email flows.

In `@packages/workers/src/lib/send-invitation-email.ts`:
- Around line 106-108: The code currently logs the full invitation magic link
(capturedMagicLinkUrl) when env.ENVIRONMENT !== 'production', exposing a bearer
token; remove that console.log or replace it with a redacted version that strips
or masks the token/query string (e.g., remove everything after the path or
replace the token value with ****) before logging. Update the block around
capturedMagicLinkUrl (in send-invitation-email.ts) so you never emit the raw URL
to logs, keeping only non-sensitive context such as the host/path or a masked
token.
- Around line 13-35: Add runtime input validation to sendInvitationEmail by
creating a Zod schema that mirrors SendInvitationEmailParams (e.g.,
SendInvitationEmailSchema = z.object({ env: z.any(), email: z.string().email(),
token: z.string(), projectName: z.string(), inviterName: z.string(), role:
z.string() })) and validate the incoming params at the top of the
sendInvitationEmail function using parse or safeParse; use the parsed result for
downstream logic (or throw a clear error/return a predictable failure when
validation fails). Replace direct usage of the TypeScript-only
SendInvitationEmailParams with the validated value (or infer the TS type from
the schema) so malformed request data fails early and predictably. Ensure the
validation is imported from zod and that any error messages include enough
context for callers.

In `@packages/workers/src/routes/billing/handlers/invoiceHandlers.ts`:
- Around line 19-20: Update InvoiceWithSubscription and the three handlers
(handleInvoicePaymentSucceeded, handleInvoicePaymentFailed,
handleInvoiceFinalized) to read the subscription ID from the new invoice
structure at invoice.parent.subscription_details.subscription, falling back to
the legacy invoice.subscription when present for backward compatibility; modify
the type/interface so it reflects optional paths (e.g., subscription?: string
and parent?: { subscription_details?: { subscription?: string } }) and change
the handlers' checks to extract subscriptionId =
invoice.parent?.subscription_details?.subscription || invoice.subscription
before deciding if the invoice is a subscription invoice so both old and new
webhook payloads are handled.

---

Outside diff comments:
In `@packages/web/src/primitives/useProject/index.js`:
- Around line 265-289: When corrupted persisted state is detected in the
whenLoaded handler, stop further setup so we don't attach persistence handlers
that reference a deleted project; after calling deleteProjectData(projectId)
return early (or rethrow) to avoid running the rest of the post-load setup (the
updateHandler closure that calls Y.applyUpdate(project.ydoc, update,
'dexie-sync'), ydoc.on('update', ...), and
connectionEntry.syncManager.syncFromYDoc), or alternatively move the post-load
setup (creation of updateHandler, ydoc.on('update', ...) and syncFromYDoc call)
inside the try block so they only run when Y.encodeStateAsUpdate/project.ydoc
application succeeds.

In `@packages/workers/src/routes/billing/__tests__/subscriptionHandlers.test.ts`:
- Around line 355-387: The test only checks status; also assert that the resumed
period dates from
stripeSub.items.data[0].current_period_start/current_period_end were persisted
via handleSubscriptionResumed: read the two values from the fixture
(items.data[0].current_period_start and current_period_end), convert them to the
DB-stored format (e.g., JS Date by multiplying by 1000 if DB uses Date), then
add expectations against the fetched updated record's periodStart and periodEnd
fields to ensure they match the converted expectedStart and expectedEnd.

In `@packages/workers/src/routes/billing/handlers/subscriptionHandlers.ts`:
- Around line 144-157: The update is overwriting periodStart/periodEnd with null
when firstItem.current_period_start/current_period_end are missing; change the
db.update(...) .set mapping to fall back to the existing subscription row values
(e.g., use existing.periodStart and existing.periodEnd) instead of null so
missing item periods don’t wipe stored billing dates, and make the same change
inside handleSubscriptionResumed() to guard against partial Stripe item
payloads.

---

Nitpick comments:
In @.cursor/mcp.json:
- Around line 19-22: The sentry configuration is missing an explicit "type"
field; update the "sentry" object to include "type": "http" alongside the
existing "url" property so it matches other config files and keeps schema
consistency (i.e., add the "type" key inside the "sentry" object that currently
contains "url").

In `@packages/web/src/primitives/useProject/connection.js`:
- Around line 86-106: The catch block in the AWARENESS_MSG_TYPE wrapper assumes
thrown err has a message property; update the catch to defensively derive a
string (e.g., const msg = (err && err.message) || String(err)) and use that in
the console.warn so non-Error throws are handled; modify the wrapper around
originalAwarenessHandler (provider.messageHandlers[AWARENESS_MSG_TYPE]) to
compute a safe message and optionally include the full err object in the log for
debugging.

In `@packages/workers/src/routes/contact.ts`:
- Around line 179-210: Generate the messageId before calling queueEmail and
attach it to the queued payload so the same UUID can be correlated end-to-end;
e.g., create a messageId via crypto.randomUUID() prior to invoking queueEmail
and include that messageId field in the object passed to queueEmail, and return
that same messageId from the c.json response (the affected symbols are
queueEmail and the return call to c.json in contact.ts).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7a4d04cf-700e-4f33-b1fc-ea7fa7d64608

📥 Commits

Reviewing files that changed from the base of the PR and between a6c7c61 and d65cc1c.

⛔ Files ignored due to path filters (2)
  • .mcp/memory.db is excluded by !**/*.db
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (26)
  • .cursor/mcp.json
  • .mcp.json
  • .vscode/mcp.json
  • package.json
  • packages/landing/package.json
  • packages/mcp-memory/package.json
  • packages/mcp/package.json
  • packages/web/package.json
  • packages/web/src/components/project/reconcile-tab/rob2-reconcile/pages/PreliminaryPage.jsx
  • packages/web/src/primitives/useProject/connection.js
  • packages/web/src/primitives/useProject/index.js
  • packages/workers/package.json
  • packages/workers/scripts/setup-stripe-test.mjs
  • packages/workers/src/auth/config.ts
  • packages/workers/src/auth/email.ts
  • packages/workers/src/durable-objects/ProjectDoc.ts
  • packages/workers/src/lib/email-queue.ts
  • packages/workers/src/lib/send-invitation-email.ts
  • packages/workers/src/lib/stripe.ts
  • packages/workers/src/routes/admin/stripe-tools.ts
  • packages/workers/src/routes/billing/__tests__/invoiceHandlers.test.ts
  • packages/workers/src/routes/billing/__tests__/subscriptionHandlers.test.ts
  • packages/workers/src/routes/billing/handlers/invoiceHandlers.ts
  • packages/workers/src/routes/billing/handlers/subscriptionHandlers.ts
  • packages/workers/src/routes/contact.ts
  • pnpm-workspace.yaml
✅ Files skipped from review due to trivial changes (1)
  • .vscode/mcp.json
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/mcp-memory/package.json
  • packages/mcp/package.json
  • packages/workers/src/routes/billing/tests/invoiceHandlers.test.ts
  • packages/workers/package.json
  • package.json
  • packages/landing/package.json

Comment thread packages/web/package.json
"@tanstack/solid-query": "^5.90.23",
"@tanstack/solid-table": "^8.21.3",
"better-auth": "^1.4.19",
"better-auth": "^1.5.4",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

better-auth 1.5 release notes breaking changes

💡 Result:

Better Auth 1.5 (released Mar 1, 2026): breaking changes

  • Removed endpoint: /forget-password/email-otp was removed (use the standard password reset flow instead). [1]
  • Removed export: better-auth/adapters/test was removed (use the testUtils plugin instead). [1]
  • API key plugin extraction: apiKey moved out of better-auth/plugins into @better-auth/api-key (update your import). [1]
    • Schema: ApiKey.userIdreferenceId; added configId (default "default"). [1]
    • Options: permissions.defaultPermissions first arg changed userIdreferenceId. [1]
    • Client SDK: apiKey.userIdapiKey.referenceId; new apiKey.references (owner type) and apiKey.configId. [1]
  • Previously @deprecated APIs removed (notable examples): createAdaptercreateAdapterFactory, AdapterDBAdapter, TransactionAdapterDBTransactionAdapter, plus several client/helper types and option names. [2]
  • Core utils import change: @better-auth/core/utils barrel export removed; must import from subpaths (e.g. @better-auth/core/utils/id, /json, /error-codes). [2]
  • Plugin error codes typing: plugin $ERROR_CODES now expects Record<string, RawError> (use defineErrorCodes(), which now yields {code, message} objects). [3]
  • Type removals: InferUser / InferSession types removed (use generic User / Session types instead). [3]
  • Hook timing: database “after” hooks now run after the transaction commits (not inside the transaction). [4]
  • Import path change: getMigrations moved to better-auth/db/migration. [4]
  • Secondary storage session shape: session.id removed from sessions in secondary storage. [4]
  • Plugin init context semantics: plugin init() context is now mutable and shared across lifecycle; init() can return extra context keys. [4]

Sources: Better Auth 1.5 release post + changelog entry for v1.5.0. [1][2]


Update better-auth to 1.5.4 and verify auth flow migrations.

Better-auth 1.5.4 includes breaking changes that directly impact this PR's password reset flow:

  • /forget-password/email-otp endpoint was removed; use the standard password reset flow instead
  • createAdapter, Adapter, and other deprecated APIs were removed
  • @better-auth/core/utils barrel export removed; imports must use subpaths (e.g., @better-auth/core/utils/id)
  • Type removals: InferUser and InferSession replaced with User and Session
  • Database "after" hooks now run after transaction commits (not during)
  • Plugin error codes typing requires defineErrorCodes() helper

Verify all email flows (magic links, verification, password resets) still work with these changes and update imports accordingly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/package.json` at line 55, Update the codebase to be compatible
with better-auth 1.5.4: replace any removed API usages (createAdapter, Adapter)
and types (InferUser, InferSession) with the new equivalents (Adapter
replacements and User, Session), change imports that used the removed barrel
`@better-auth/core/utils` to explicit subpath imports (e.g.,
`@better-auth/core/utils/id`), replace calls to the removed
/forget-password/email-otp flow with the standard password reset endpoints and
verify magic link/verification handlers, update plugin error code definitions to
use defineErrorCodes(), and audit code that relied on "after" DB hooks running
inside transactions (update logic to account for hooks running after commit);
search for these symbols (createAdapter, Adapter, InferUser, InferSession,
defineErrorCodes, `@better-auth/core/utils`) to locate and update all occurrences.

Comment thread packages/workers/src/auth/config.ts Outdated
Comment on lines +10 to +24
export interface EmailPayload {
to: string;
subject: string;
html?: string;
text?: string;
replyTo?: string;
}

export async function queueEmail(env: Env, payload: EmailPayload): Promise<void> {
if (!payload?.to || !payload?.subject || (!payload?.html && !payload?.text)) {
throw createDomainError(
VALIDATION_ERRORS.INVALID_INPUT,
{ fields: ['to', 'subject', 'html', 'text'] },
'Email payload requires to, subject, and html or text',
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Use a shared Zod schema for queued email payloads.

This helper is now the backend validation boundary for multiple email flows, but it still relies on a single truthiness check. Converting this to a z.object(...).refine(...) schema keeps the contract reusable across callers and avoids hand-rolled validation drift.

As per coding guidelines, packages/workers/src/**/*.{ts,tsx}: Use Zod for schema and input validation on backend services.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/workers/src/lib/email-queue.ts` around lines 10 - 24, Replace the
hand-rolled truthiness check in queueEmail with a reusable Zod schema: define a
z.object schema for EmailPayload (fields to, subject, html?, text?, replyTo?)
and add a .refine(...) that enforces at least one of html or text is present;
then validate the incoming payload via schema.parse or safeParse inside
queueEmail and on failure throw
createDomainError(VALIDATION_ERRORS.INVALID_INPUT, ...) with the schema error
details instead of the manual check. Ensure the schema (e.g.,
EmailPayloadSchema) is exported/placed so other callers can reuse it across
email flows.

Comment on lines +13 to +35
interface SendInvitationEmailParams {
env: Env;
email: string;
token: string;
projectName: string;
inviterName: string;
role: string;
}

interface SendInvitationEmailResult {
emailQueued: boolean;
}

/**
* Generates a magic link and queues an invitation email.
*
* Throws on critical errors (missing AUTH_SECRET, failed magic link generation).
* Returns { emailQueued: false } if only the queue send fails, since the
* invitation record already exists in the database and can be resent.
*/
export async function sendInvitationEmail(
params: SendInvitationEmailParams,
): Promise<SendInvitationEmailResult> {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Add runtime validation for this shared backend entry point.

SendInvitationEmailParams is only a TypeScript type right now. Since this helper builds auth URLs and email payloads from request-derived strings, please parse params with a Zod schema before using it so bad input fails early and predictably.

As per coding guidelines, "packages/workers/src/**/*.{ts,tsx}: Use Zod for schema and input validation on backend services".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/workers/src/lib/send-invitation-email.ts` around lines 13 - 35, Add
runtime input validation to sendInvitationEmail by creating a Zod schema that
mirrors SendInvitationEmailParams (e.g., SendInvitationEmailSchema = z.object({
env: z.any(), email: z.string().email(), token: z.string(), projectName:
z.string(), inviterName: z.string(), role: z.string() })) and validate the
incoming params at the top of the sendInvitationEmail function using parse or
safeParse; use the parsed result for downstream logic (or throw a clear
error/return a predictable failure when validation fails). Replace direct usage
of the TypeScript-only SendInvitationEmailParams with the validated value (or
infer the TS type from the schema) so malformed request data fails early and
predictably. Ensure the validation is imported from zod and that any error
messages include enough context for callers.

Comment thread packages/workers/src/lib/send-invitation-email.ts
Comment on lines +19 to +20
// The subscription field moved to invoice.parent.subscription_details.subscription in newer
// API versions, but webhook payloads may still include it at the top level
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cat -n packages/workers/src/routes/billing/handlers/invoiceHandlers.ts | head -100

Repository: InfinityBowman/corates

Length of output: 3761


🏁 Script executed:

# Search for all occurrences of invoice.subscription in the file
rg "invoice\.subscription" packages/workers/src/routes/billing/handlers/invoiceHandlers.ts -A 2 -B 2

Repository: InfinityBowman/corates

Length of output: 48


🏁 Script executed:

# Search for references to invoice.parent.subscription_details
rg "invoice\.parent|subscription_details" packages/workers/src/routes/billing/handlers/invoiceHandlers.ts -A 2 -B 2

Repository: InfinityBowman/corates

Length of output: 296


🏁 Script executed:

# Get more lines to see the full handlers
cat -n packages/workers/src/routes/billing/handlers/invoiceHandlers.ts | head -200

Repository: InfinityBowman/corates

Length of output: 7309


🏁 Script executed:

# Search for all handlers in this file
rg "export async function handle" packages/workers/src/routes/billing/handlers/invoiceHandlers.ts

Repository: InfinityBowman/corates

Length of output: 214


🏁 Script executed:

# Check if there are any references to invoice.parent or subscription_details anywhere in the codebase
rg "invoice\.parent|subscription_details" packages/workers/src/routes/billing/ -r

Repository: InfinityBowman/corates

Length of output: 479


🏁 Script executed:

# Check for references to invoice.parent or subscription_details anywhere
rg "invoice\.parent|subscription_details" packages/workers/src/routes/billing/

Repository: InfinityBowman/corates

Length of output: 222


🏁 Script executed:

# Get the rest of the invoiceHandlers file to see all handlers
tail -n +200 packages/workers/src/routes/billing/handlers/invoiceHandlers.ts | head -100

Repository: InfinityBowman/corates

Length of output: 1984


🏁 Script executed:

# Check the git history or diff to see what changed in this file recently
git log --oneline -20 packages/workers/src/routes/billing/handlers/invoiceHandlers.ts

Repository: InfinityBowman/corates

Length of output: 85


🏁 Script executed:

# Check if Stripe API version was recently updated
git log --all --oneline -30 | head -30

Repository: InfinityBowman/corates

Length of output: 1509


🏁 Script executed:

# Search for Stripe API version or stripe client initialization
rg "stripe|Stripe" packages/workers/src/routes/billing/ -i | grep -i "version\|api\|2025"

Repository: InfinityBowman/corates

Length of output: 1031


🏁 Script executed:

# Check what the actual Stripe type looks like to understand the new structure
rg "invoice\.parent" packages/ -r

Repository: InfinityBowman/corates

Length of output: 479


🏁 Script executed:

# Search for Stripe Invoice type definitions or usage in the codebase
fd "stripe" packages/workers/src -type f | head -20

Repository: InfinityBowman/corates

Length of output: 236


🏁 Script executed:

# Check if there are any type definitions or imports from stripe package
rg "from 'stripe'" packages/workers/src -A 2 -B 2

Repository: InfinityBowman/corates

Length of output: 9340


🏁 Script executed:

# Look for any parent field references specifically
rg "\.parent" packages/workers/src/routes/billing/ -B 2 -A 2

Repository: InfinityBowman/corates

Length of output: 616


🏁 Script executed:

# Check the package.json to see the Stripe version
cat packages/workers/package.json | grep -A 2 -B 2 "stripe"

Repository: InfinityBowman/corates

Length of output: 607


🏁 Script executed:

# Check the lib/stripe.ts file for API version configuration
cat -n packages/workers/src/lib/stripe.ts | head -50

Repository: InfinityBowman/corates

Length of output: 1922


🏁 Script executed:

# Check if there's a STRIPE_API_VERSION constant defined
rg "STRIPE_API_VERSION" packages/workers/src

Repository: InfinityBowman/corates

Length of output: 470


Update handlers to read subscription ID from the new invoice structure.

The comment acknowledges that Stripe's API moved subscription from invoice.subscription to invoice.parent.subscription_details.subscription in newer API versions, but the handlers still only check the deprecated top-level field. All three handlers (handleInvoicePaymentSucceeded, handleInvoicePaymentFailed, handleInvoiceFinalized) will fail to recognize subscription invoices after the Stripe API version upgrade to 2026-02-25.clover, incorrectly classifying them as non-subscription invoices and breaking the dunning flow and subscription status updates.

The InvoiceWithSubscription interface and all handler checks must be updated to read from invoice.parent.subscription_details.subscription instead of (or in addition to, if backward compatibility is needed) the deprecated invoice.subscription field.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/workers/src/routes/billing/handlers/invoiceHandlers.ts` around lines
19 - 20, Update InvoiceWithSubscription and the three handlers
(handleInvoicePaymentSucceeded, handleInvoicePaymentFailed,
handleInvoiceFinalized) to read the subscription ID from the new invoice
structure at invoice.parent.subscription_details.subscription, falling back to
the legacy invoice.subscription when present for backward compatibility; modify
the type/interface so it reflects optional paths (e.g., subscription?: string
and parent?: { subscription_details?: { subscription?: string } }) and change
the handlers' checks to extract subscriptionId =
invoice.parent?.subscription_details?.subscription || invoice.subscription
before deciding if the invoice is a subscription invoice so both old and new
webhook payloads are handled.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
packages/workers/src/index.ts (2)

384-390: ⚠️ Potential issue | 🟠 Major

Mask recipient addresses in queue failure logs.

Both failure paths log msg.body.to verbatim. That leaks recipient PII into worker logs; use a masked or hashed form instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/workers/src/index.ts` around lines 384 - 390, The logs currently
print recipient PII via msg.body.to; create and use a small utility (e.g.,
maskEmail or hashRecipient) and call it when logging failures in the EmailQueue
send/exception handlers so the console.error calls use the masked/hashed value
instead of msg.body.to; update both occurrences that build messages for
`[EmailQueue] Send returned error for ${msg.body.to}:` and `[EmailQueue]
Exception sending to ${msg.body.to}:` to use the new helper and leave retry
logic (msg.retry) unchanged.

371-379: ⚠️ Potential issue | 🟠 Major

Still validate the queue payload before casting it into sendEmail().

batch is declared as MessageBatch<any> and msg.body is force-cast into the email service input. That makes the consumer trust whatever is already in the queue, so a stale or malformed message will fail only at delivery time instead of being rejected up front. Cloudflare's queue API keeps message bodies generic unless you type them explicitly. Parse the body with a shared schema before calling Postmark, and ack invalid payloads explicitly. (developers.cloudflare.com)

As per coding guidelines, "Use Zod for schema and input validation on backend services".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/workers/src/index.ts` around lines 371 - 379, The queue handler is
force-casting msg.body into the email service input (EmailPayload) which trusts
arbitrary queued data; update the async queue(batch: MessageBatch<any>, env:
Env) implementation to validate msg.body with a shared Zod schema (e.g.,
EmailPayloadSchema) before calling createEmailService(...).sendEmail:
parse/strict-validate each msg.body, ack and log/handle messages that fail
validation (do not call sendEmail), and only pass the validated/typed object to
emailService.sendEmail; reference the queue method, MessageBatch, msg.body,
EmailPayload/EmailPayloadSchema, createEmailService, and sendEmail when making
the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/workers/src/index.ts`:
- Around line 384-390: The logs currently print recipient PII via msg.body.to;
create and use a small utility (e.g., maskEmail or hashRecipient) and call it
when logging failures in the EmailQueue send/exception handlers so the
console.error calls use the masked/hashed value instead of msg.body.to; update
both occurrences that build messages for `[EmailQueue] Send returned error for
${msg.body.to}:` and `[EmailQueue] Exception sending to ${msg.body.to}:` to use
the new helper and leave retry logic (msg.retry) unchanged.
- Around line 371-379: The queue handler is force-casting msg.body into the
email service input (EmailPayload) which trusts arbitrary queued data; update
the async queue(batch: MessageBatch<any>, env: Env) implementation to validate
msg.body with a shared Zod schema (e.g., EmailPayloadSchema) before calling
createEmailService(...).sendEmail: parse/strict-validate each msg.body, ack and
log/handle messages that fail validation (do not call sendEmail), and only pass
the validated/typed object to emailService.sendEmail; reference the queue
method, MessageBatch, msg.body, EmailPayload/EmailPayloadSchema,
createEmailService, and sendEmail when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: da005ad2-f255-4ed9-a5cc-489b215ab12c

📥 Commits

Reviewing files that changed from the base of the PR and between d65cc1c and d453c3b.

📒 Files selected for processing (3)
  • eslint.config.js
  • packages/web/src/components/project/reconcile-tab/rob2-reconcile/pages/PreliminaryPage.jsx
  • packages/workers/src/index.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/web/src/components/project/reconcile-tab/rob2-reconcile/pages/PreliminaryPage.jsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Workers Builds: corates-workers-prod
  • GitHub Check: Workers Builds: corates
🧰 Additional context used
📓 Path-based instructions (9)
**/*.{js,jsx,ts,tsx}

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

**/*.{js,jsx,ts,tsx}: Use the useProject hook for managing Yjs connections with reference counting instead of creating Y.Doc instances directly
Never create Y.Doc instances directly; always use the connection registry managed by useProject
Access Y.Doc connection state via projectStore.getConnectionState(projectId) instead of checking connection state directly
Read Yjs-synced data from projectStore (using getStudies, getChecklist, etc.) rather than accessing Y.Doc maps/arrays directly
Use operation functions from useProject or projectActionsStore for writing data instead of directly modifying Y.Doc
Don't store Y.Doc references in component state; always retrieve the connection through useProject
Handle connection cleanup via onCleanup() in Solid.js components or equivalent cleanup patterns, calling disconnect() when the component unmounts

Files:

  • packages/workers/src/index.ts
  • eslint.config.js
packages/workers/src/**/*.{js,ts,jsx,tsx}

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

ALWAYS use db.batch() for multiple related database operations in Drizzle to ensure atomicity - all operations must succeed or all fail together

Files:

  • packages/workers/src/index.ts
packages/workers/**/*.{js,ts}

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

packages/workers/**/*.{js,ts}: Always use createDomainError from @corates/shared for backend error handling instead of manually creating error objects
Use predefined error constants from @corates/shared (PROJECT_ERRORS, AUTH_ERRORS, VALIDATION_ERRORS, SYSTEM_ERRORS, USER_ERRORS) instead of arbitrary error strings
Wrap database operations in try-catch blocks and return domain errors using createDomainError with SYSTEM_ERRORS.DB_ERROR
Use validation middleware (validateRequest) for request validation instead of manually validating request bodies in routes

Files:

  • packages/workers/src/index.ts
packages/{web,workers}/**/*.{js,ts,jsx,tsx}

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

Never throw string literals; always throw Error objects or return domain errors from API routes

Files:

  • packages/workers/src/index.ts
**/*.{js,ts,jsx,tsx}

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

**/*.{js,ts,jsx,tsx}: Prefer modern ES6+ syntax and features
Comments should explain why something is being done, not what the code is doing
Reserve comments for explaining intent, context, workarounds, assumptions, edge cases, or limitations
Use TODO(agent): prefix for incomplete work or flagged items with brief description and relevant doc references

Files:

  • packages/workers/src/index.ts
  • eslint.config.js
packages/workers/src/**/*.{ts,tsx}

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

packages/workers/src/**/*.{ts,tsx}: Use Zod for schema and input validation on backend services
Use Drizzle ORM for all database interactions and migrations
Use Better-Auth for authentication and user management
All project routes are org-scoped and use /api/orgs/:orgId/projects/... pattern in backend
Use requireOrgMembership and requireProjectAccess middleware for authentication in org-scoped routes
Never bypass Drizzle for database access

Never bypass Drizzle for database access - use Drizzle ORM for all database operations

Files:

  • packages/workers/src/index.ts
**/*

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Never use emojis or unicode symbols anywhere in code, comments, documentation, plan files, commit messages, or examples

Files:

  • packages/workers/src/index.ts
  • eslint.config.js
packages/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

packages/**/*.{ts,tsx,js,jsx}: Prefer modern ES6+ syntax and features
Prefer config files over hardcoding values
Keep files small, focused, and modular - extract large files into sub-modules or separate utilities
Each file should handle one coherent responsibility
Comments should explain WHY something is being done or provide context, not repeat what the code is saying
Do NOT narrate what code is doing, don't duplicate function/variable names in comments, and don't leave stale comments that contradict the code
Use the Agent TODO convention // TODO(agent): Brief description for incomplete work, flagging items for future attention, known limitations, and documentation section references

Use TODO(agent) convention with brief description and relevant doc section references for incomplete work or flagged issues

Files:

  • packages/workers/src/index.ts
packages/workers/src/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

packages/workers/src/**/*.ts: Use Zod for schema and input validation in backend code
Use Drizzle ORM for ALL database interactions and migrations
Use Better-Auth for authentication and user management
Never bypass Drizzle for database access

Files:

  • packages/workers/src/index.ts
🧠 Learnings (21)
📚 Learning: 2026-01-05T01:00:58.113Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/workers-testing.mdc:0-0
Timestamp: 2026-01-05T01:00:58.113Z
Learning: Applies to {packages/workers/src/**/*.test.js,packages/workers/src/**/__tests__/**/*.js} : Only re-mock Postmark and Stripe modules inside a test file if you need behavior different from the global mock setup in packages/workers/src/__tests__/setup.js

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2025-12-27T03:01:35.601Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/durable-objects.mdc:0-0
Timestamp: 2025-12-27T03:01:35.601Z
Learning: Applies to packages/workers/src/durable-objects/**/ProjectDoc.{js,ts} : Use y-websocket protocol with message type 1 for awareness (presence) messages in ProjectDoc

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-17T00:25:35.716Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/corates.mdc:0-0
Timestamp: 2026-01-17T00:25:35.716Z
Learning: Applies to packages/workers/src/**/*.{ts,tsx} : Use Better-Auth for authentication and user management

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2025-12-27T03:01:35.601Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/durable-objects.mdc:0-0
Timestamp: 2025-12-27T03:01:35.601Z
Learning: Applies to packages/workers/src/durable-objects/**/*.{js,ts} : Do not expose internal sync endpoints (/sync, /sync-member, /sync-pdf, /disconnect-all) publicly without X-Internal-Request verification

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-17T16:09:36.920Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-17T16:09:36.920Z
Learning: Applies to packages/**/*.{ts,tsx,js,jsx} : Use the Agent TODO convention `// TODO(agent): Brief description` for incomplete work, flagging items for future attention, known limitations, and documentation section references

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-01T23:31:43.756Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/api-routes.mdc:0-0
Timestamp: 2026-01-01T23:31:43.756Z
Learning: Applies to packages/workers/src/routes/**/*.js : Use error constants from `corates/shared` (`PROJECT_ERRORS.*`, `AUTH_ERRORS.*`, `VALIDATION_ERRORS.*`, `SYSTEM_ERRORS.*`, `USER_ERRORS.*`) instead of creating error objects manually

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-01T23:31:43.756Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/api-routes.mdc:0-0
Timestamp: 2026-01-01T23:31:43.756Z
Learning: Applies to packages/workers/src/routes/**/*.js : ALWAYS use `createDomainError` from `corates/shared` for error handling in API routes

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-17T00:25:03.945Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/instructions/api-routes.instructions.md:0-0
Timestamp: 2026-01-17T00:25:03.945Z
Learning: Applies to packages/workers/**/routes/**/*.{js,ts} : Use error helpers from `corates/shared` (`notFound`, `forbidden`, `badRequest`, etc.) for error handling in API routes

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-17T00:25:35.716Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/corates.mdc:0-0
Timestamp: 2026-01-17T00:25:35.716Z
Learning: Applies to packages/workers/src/**/*.{ts,tsx} : Use `requireOrgMembership` and `requireProjectAccess` middleware for authentication in org-scoped routes

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-01T23:31:43.756Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/api-routes.mdc:0-0
Timestamp: 2026-01-01T23:31:43.756Z
Learning: Applies to packages/workers/src/routes/**/*.js : Apply authentication and authorization middleware in correct order: Authentication → Organization membership → Project access → Authorization → Validation → Route handler

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-05T01:00:58.113Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/workers-testing.mdc:0-0
Timestamp: 2026-01-05T01:00:58.113Z
Learning: Applies to {packages/workers/src/**/*.test.js,packages/workers/src/**/__tests__/**/*.js} : Mock src/middleware/auth.js requireAuth function in route tests by setting user/session via x-test-user-id and x-test-user-email headers

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-01T23:32:23.488Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/workers.mdc:0-0
Timestamp: 2026-01-01T23:32:23.488Z
Learning: Applies to packages/workers/src/routes/orgs/**/*.{js,ts,jsx,tsx} : Use `requireOrgMembership` and `requireProjectAccess` middleware instead of manual membership checks for org-scoped routes

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-01T23:31:43.756Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/api-routes.mdc:0-0
Timestamp: 2026-01-01T23:31:43.756Z
Learning: Applies to packages/workers/src/routes/orgs/**/*.js : Use organization-scoped route patterns with path structure `/api/orgs/:orgId/...` and leverage `requireOrgMembership`, `requireProjectAccess`, `getOrgContext`, and `getProjectContext` middleware

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-17T00:25:35.716Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/corates.mdc:0-0
Timestamp: 2026-01-17T00:25:35.716Z
Learning: Applies to packages/workers/src/**/*.{ts,tsx} : All project routes are org-scoped and use `/api/orgs/:orgId/projects/...` pattern in backend

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-01T23:32:23.488Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/workers.mdc:0-0
Timestamp: 2026-01-01T23:32:23.488Z
Learning: Applies to packages/workers/src/routes/orgs/**/*.{js,ts,jsx,tsx} : Use `getOrgContext(c)` to retrieve the validated orgId from context instead of manual checks - orgId is guaranteed valid by middleware

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-17T00:25:35.716Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/corates.mdc:0-0
Timestamp: 2026-01-17T00:25:35.716Z
Learning: Applies to packages/web/src/**/*.{ts,tsx} : Frontend uses orgSlug in URLs (`/orgs/:orgSlug/...`) for readability

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-05T01:00:58.113Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/workers-testing.mdc:0-0
Timestamp: 2026-01-05T01:00:58.113Z
Learning: Applies to {packages/workers/src/**/*.test.js,packages/workers/src/**/__tests__/**/*.js} : For integration tests, fetch against real worker env using app.fetch(req, env, ctx) with env from cloudflare:test, or use fetchApp() helper

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-06T23:56:57.354Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/error-handling.mdc:0-0
Timestamp: 2026-01-06T23:56:57.354Z
Learning: Applies to packages/workers/**/*.{js,ts} : Always use `createDomainError` from `corates/shared` for backend error handling instead of manually creating error objects

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2025-12-27T03:01:35.601Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/durable-objects.mdc:0-0
Timestamp: 2025-12-27T03:01:35.601Z
Learning: Applies to packages/workers/src/durable-objects/**/*.{js,ts} : Handle WebSocket connection errors gracefully by closing connections and cleaning up resources

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-06T23:56:57.354Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/error-handling.mdc:0-0
Timestamp: 2026-01-06T23:56:57.354Z
Learning: Applies to packages/workers/**/*.{js,ts} : Wrap database operations in try-catch blocks and return domain errors using createDomainError with SYSTEM_ERRORS.DB_ERROR

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-06T23:56:57.354Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/error-handling.mdc:0-0
Timestamp: 2026-01-06T23:56:57.354Z
Learning: Applies to packages/{web,workers}/**/*.{js,ts,jsx,tsx} : Never throw string literals; always throw Error objects or return domain errors from API routes

Applied to files:

  • packages/workers/src/index.ts
🔇 Additional comments (1)
eslint.config.js (1)

331-332: LGTM!

The addition of Message and MessageBatch globals is correct for Cloudflare Queues support. These types are used in queue consumer handlers (queue(batch: MessageBatch<T>, env, ctx)) and are appropriately scoped to the backend workers files where the queue consumer is implemented.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

*/
async function sendProjectInvitation(
to: string,
projectName: string,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unused email service methods after queue migration

Low Severity

The sendEmailVerification, sendPasswordReset, and sendMagicLink methods in createEmailService are now dead code. After this PR, auth/config.ts generates email HTML/text directly and calls queueEmail(), and the queue consumer only calls emailService.sendEmail(). These three methods (and the template imports they rely on like getVerificationEmailHtml, getPasswordResetEmailHtml, getMagicLinkEmailHtml) are never invoked anywhere in the codebase. They remain exported on the EmailService interface, which is misleading.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
.mcp.md (1)

1-26: Clarify the purpose of embedding MCP configuration in a Markdown HTML comment.

This file embeds MCP server configuration as JSON within an HTML comment block in a Markdown file. While this format might be intentional for documentation or tooling purposes, it's unconventional compared to standard JSON configuration files (like the related .mcp.json and .vscode/mcp.json files mentioned in the summary).

If this is a private/development configuration, consider adding a brief explanation at the top of the file documenting its purpose and relationship to the other MCP configuration files in the repository.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.mcp.md around lines 1 - 26, The embedded MCP JSON (the HTML comment
containing "mcpServers" with entries "corates", "corates-memory", "ark-ui", and
"sentry") is unclear; add a short top-of-file note explaining why the MCP
configuration is stored inside a Markdown HTML comment, whether it is for
documentation/tooling, and how it relates to the canonical .mcp.json and
.vscode/mcp.json files (e.g., source of truth vs. examples or dev-only
overrides), so readers and tools understand its intent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.mcp.md:
- Around line 16-20: The current MCP entry uses npx with the auto-accept flag
(-y) for the "ark-ui" tool which risks silently installing a
malicious/typosquatted package; replace this with a safer approach: either
pre-install `@ark-ui/mcp` as a devDependency and point the config to the local
binary, remove the -y flag to require user confirmation when invoking npx, or
pin a specific safe version using npx `@ark-ui/mcp`@<semver> to avoid implicit
upgrades—also confirm whether the "ark-ui" configuration in this file is
intended to be private/local-only and, if so, mark or move it to a local-only
config to avoid exposing it in public repos.

---

Nitpick comments:
In @.mcp.md:
- Around line 1-26: The embedded MCP JSON (the HTML comment containing
"mcpServers" with entries "corates", "corates-memory", "ark-ui", and "sentry")
is unclear; add a short top-of-file note explaining why the MCP configuration is
stored inside a Markdown HTML comment, whether it is for documentation/tooling,
and how it relates to the canonical .mcp.json and .vscode/mcp.json files (e.g.,
source of truth vs. examples or dev-only overrides), so readers and tools
understand its intent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 05c34c75-5b3c-4678-9998-425a506d2599

📥 Commits

Reviewing files that changed from the base of the PR and between d453c3b and f2a922c.

📒 Files selected for processing (7)
  • .mcp.json
  • .mcp.md
  • packages/web/src/components/project/overview-tab/OverviewTab.jsx
  • packages/workers/src/auth/config.ts
  • packages/workers/src/index.ts
  • packages/workers/src/lib/send-invitation-email.ts
  • packages/workers/src/routes/billing/__tests__/invoiceHandlers.test.ts
💤 Files with no reviewable changes (1)
  • .mcp.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/workers/src/routes/billing/tests/invoiceHandlers.test.ts
  • packages/workers/src/index.ts
  • packages/web/src/components/project/overview-tab/OverviewTab.jsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: Workers Builds: corates
  • GitHub Check: Workers Builds: corates-workers-prod
🧰 Additional context used
📓 Path-based instructions (9)
**/*.{js,jsx,ts,tsx}

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

**/*.{js,jsx,ts,tsx}: Use the useProject hook for managing Yjs connections with reference counting instead of creating Y.Doc instances directly
Never create Y.Doc instances directly; always use the connection registry managed by useProject
Access Y.Doc connection state via projectStore.getConnectionState(projectId) instead of checking connection state directly
Read Yjs-synced data from projectStore (using getStudies, getChecklist, etc.) rather than accessing Y.Doc maps/arrays directly
Use operation functions from useProject or projectActionsStore for writing data instead of directly modifying Y.Doc
Don't store Y.Doc references in component state; always retrieve the connection through useProject
Handle connection cleanup via onCleanup() in Solid.js components or equivalent cleanup patterns, calling disconnect() when the component unmounts

Files:

  • packages/workers/src/auth/config.ts
  • packages/workers/src/lib/send-invitation-email.ts
packages/workers/src/**/*.{js,ts,jsx,tsx}

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

ALWAYS use db.batch() for multiple related database operations in Drizzle to ensure atomicity - all operations must succeed or all fail together

Files:

  • packages/workers/src/auth/config.ts
  • packages/workers/src/lib/send-invitation-email.ts
packages/workers/**/*.{js,ts}

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

packages/workers/**/*.{js,ts}: Always use createDomainError from @corates/shared for backend error handling instead of manually creating error objects
Use predefined error constants from @corates/shared (PROJECT_ERRORS, AUTH_ERRORS, VALIDATION_ERRORS, SYSTEM_ERRORS, USER_ERRORS) instead of arbitrary error strings
Wrap database operations in try-catch blocks and return domain errors using createDomainError with SYSTEM_ERRORS.DB_ERROR
Use validation middleware (validateRequest) for request validation instead of manually validating request bodies in routes

Files:

  • packages/workers/src/auth/config.ts
  • packages/workers/src/lib/send-invitation-email.ts
packages/{web,workers}/**/*.{js,ts,jsx,tsx}

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

Never throw string literals; always throw Error objects or return domain errors from API routes

Files:

  • packages/workers/src/auth/config.ts
  • packages/workers/src/lib/send-invitation-email.ts
**/*.{js,ts,jsx,tsx}

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

**/*.{js,ts,jsx,tsx}: Prefer modern ES6+ syntax and features
Comments should explain why something is being done, not what the code is doing
Reserve comments for explaining intent, context, workarounds, assumptions, edge cases, or limitations
Use TODO(agent): prefix for incomplete work or flagged items with brief description and relevant doc references

Files:

  • packages/workers/src/auth/config.ts
  • packages/workers/src/lib/send-invitation-email.ts
packages/workers/src/**/*.{ts,tsx}

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

packages/workers/src/**/*.{ts,tsx}: Use Zod for schema and input validation on backend services
Use Drizzle ORM for all database interactions and migrations
Use Better-Auth for authentication and user management
All project routes are org-scoped and use /api/orgs/:orgId/projects/... pattern in backend
Use requireOrgMembership and requireProjectAccess middleware for authentication in org-scoped routes
Never bypass Drizzle for database access

Never bypass Drizzle for database access - use Drizzle ORM for all database operations

Files:

  • packages/workers/src/auth/config.ts
  • packages/workers/src/lib/send-invitation-email.ts
**/*

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Never use emojis or unicode symbols anywhere in code, comments, documentation, plan files, commit messages, or examples

Files:

  • packages/workers/src/auth/config.ts
  • packages/workers/src/lib/send-invitation-email.ts
packages/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

packages/**/*.{ts,tsx,js,jsx}: Prefer modern ES6+ syntax and features
Prefer config files over hardcoding values
Keep files small, focused, and modular - extract large files into sub-modules or separate utilities
Each file should handle one coherent responsibility
Comments should explain WHY something is being done or provide context, not repeat what the code is saying
Do NOT narrate what code is doing, don't duplicate function/variable names in comments, and don't leave stale comments that contradict the code
Use the Agent TODO convention // TODO(agent): Brief description for incomplete work, flagging items for future attention, known limitations, and documentation section references

Use TODO(agent) convention with brief description and relevant doc section references for incomplete work or flagged issues

Files:

  • packages/workers/src/auth/config.ts
  • packages/workers/src/lib/send-invitation-email.ts
packages/workers/src/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

packages/workers/src/**/*.ts: Use Zod for schema and input validation in backend code
Use Drizzle ORM for ALL database interactions and migrations
Use Better-Auth for authentication and user management
Never bypass Drizzle for database access

Files:

  • packages/workers/src/auth/config.ts
  • packages/workers/src/lib/send-invitation-email.ts
🧠 Learnings (27)
📚 Learning: 2026-01-17T00:25:35.716Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/corates.mdc:0-0
Timestamp: 2026-01-17T00:25:35.716Z
Learning: Applies to packages/workers/src/**/*.{ts,tsx} : Use Better-Auth for authentication and user management

Applied to files:

  • packages/workers/src/auth/config.ts
  • packages/workers/src/lib/send-invitation-email.ts
📚 Learning: 2026-01-17T16:09:36.920Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-17T16:09:36.920Z
Learning: Applies to packages/workers/src/**/*.ts : Use Better-Auth for authentication and user management

Applied to files:

  • packages/workers/src/auth/config.ts
  • packages/workers/src/lib/send-invitation-email.ts
📚 Learning: 2026-01-05T01:00:58.113Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/workers-testing.mdc:0-0
Timestamp: 2026-01-05T01:00:58.113Z
Learning: Applies to {packages/workers/src/**/*.test.js,packages/workers/src/**/__tests__/**/*.js} : Only re-mock Postmark and Stripe modules inside a test file if you need behavior different from the global mock setup in packages/workers/src/__tests__/setup.js

Applied to files:

  • packages/workers/src/auth/config.ts
📚 Learning: 2026-01-17T16:09:36.920Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-17T16:09:36.920Z
Learning: Applies to packages/workers/src/**/*.ts : Use Drizzle ORM for ALL database interactions and migrations

Applied to files:

  • packages/workers/src/auth/config.ts
📚 Learning: 2026-01-17T00:25:35.716Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/corates.mdc:0-0
Timestamp: 2026-01-17T00:25:35.716Z
Learning: Applies to packages/workers/src/**/*.{ts,tsx} : Use `requireOrgMembership` and `requireProjectAccess` middleware for authentication in org-scoped routes

Applied to files:

  • packages/workers/src/auth/config.ts
📚 Learning: 2026-01-17T00:25:35.716Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/corates.mdc:0-0
Timestamp: 2026-01-17T00:25:35.716Z
Learning: Applies to packages/workers/src/**/*.{ts,tsx} : Use Drizzle ORM for all database interactions and migrations

Applied to files:

  • packages/workers/src/auth/config.ts
📚 Learning: 2026-01-05T01:00:58.113Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/workers-testing.mdc:0-0
Timestamp: 2026-01-05T01:00:58.113Z
Learning: Applies to {packages/workers/src/**/*.test.js,packages/workers/src/**/__tests__/**/*.js} : Reuse provided helpers from packages/workers/src/__tests__/helpers.js (resetTestDatabase, clearProjectDOs, seedUser, seedOrganization, createTestEnv, fetchApp, json, createAuthHeaders) instead of implementing ad-hoc solutions

Applied to files:

  • packages/workers/src/auth/config.ts
  • packages/workers/src/lib/send-invitation-email.ts
📚 Learning: 2026-01-06T23:56:57.354Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/error-handling.mdc:0-0
Timestamp: 2026-01-06T23:56:57.354Z
Learning: Applies to packages/{web,workers}/**/*.{js,ts,jsx,tsx} : Never throw string literals; always throw Error objects or return domain errors from API routes

Applied to files:

  • packages/workers/src/auth/config.ts
📚 Learning: 2026-01-01T23:31:43.756Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/api-routes.mdc:0-0
Timestamp: 2026-01-01T23:31:43.756Z
Learning: Applies to packages/workers/src/routes/**/*.js : Use error constants from `corates/shared` (`PROJECT_ERRORS.*`, `AUTH_ERRORS.*`, `VALIDATION_ERRORS.*`, `SYSTEM_ERRORS.*`, `USER_ERRORS.*`) instead of creating error objects manually

Applied to files:

  • packages/workers/src/auth/config.ts
📚 Learning: 2026-01-17T16:10:07.531Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-17T16:10:07.531Z
Learning: Use required libraries for specific functionality: Zod for validation, Drizzle for database, Better-Auth for authentication, Ark UI for components

Applied to files:

  • packages/workers/src/auth/config.ts
📚 Learning: 2026-01-17T00:25:03.945Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/instructions/api-routes.instructions.md:0-0
Timestamp: 2026-01-17T00:25:03.945Z
Learning: Applies to packages/workers/**/routes/**/*.{js,ts} : Create DB client from environment using `createDb(c.env.DB)` instead of creating DB connections directly

Applied to files:

  • packages/workers/src/auth/config.ts
📚 Learning: 2026-01-01T23:31:43.756Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/api-routes.mdc:0-0
Timestamp: 2026-01-01T23:31:43.756Z
Learning: Applies to packages/workers/src/routes/**/*.js : Always create DB client from environment using `createDb` function in route handlers

Applied to files:

  • packages/workers/src/auth/config.ts
📚 Learning: 2026-01-17T16:09:36.920Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-17T16:09:36.920Z
Learning: Applies to packages/workers/src/**/*.ts : Never bypass Drizzle for database access

Applied to files:

  • packages/workers/src/auth/config.ts
📚 Learning: 2026-01-17T00:25:35.716Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/corates.mdc:0-0
Timestamp: 2026-01-17T00:25:35.716Z
Learning: Applies to packages/workers/src/**/*.{ts,tsx} : Never bypass Drizzle for database access

Applied to files:

  • packages/workers/src/auth/config.ts
📚 Learning: 2026-01-17T16:10:07.531Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-17T16:10:07.531Z
Learning: Applies to packages/workers/src/**/*.{ts,tsx} : Never bypass Drizzle for database access - use Drizzle ORM for all database operations

Applied to files:

  • packages/workers/src/auth/config.ts
📚 Learning: 2026-01-01T23:32:17.698Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/organizations.mdc:0-0
Timestamp: 2026-01-01T23:32:17.698Z
Learning: Applies to packages/workers/src/routes/orgs/** : Ensure project invitation flow creates org membership before project membership, including orgRole and projectRole in invitation data

Applied to files:

  • packages/workers/src/lib/send-invitation-email.ts
📚 Learning: 2026-01-17T16:09:36.920Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-17T16:09:36.920Z
Learning: Applies to packages/workers/src/**/*.ts : Use Zod for schema and input validation in backend code

Applied to files:

  • packages/workers/src/lib/send-invitation-email.ts
📚 Learning: 2026-01-17T00:25:35.716Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/corates.mdc:0-0
Timestamp: 2026-01-17T00:25:35.716Z
Learning: Applies to packages/workers/src/**/*.{ts,tsx} : Use Zod for schema and input validation on backend services

Applied to files:

  • packages/workers/src/lib/send-invitation-email.ts
📚 Learning: 2026-01-01T23:32:23.488Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/workers.mdc:0-0
Timestamp: 2026-01-01T23:32:23.488Z
Learning: Applies to packages/workers/src/config/validation.{js,ts} : Add new Zod validation schemas to `config/validation.js` and reuse `commonFields` when possible to avoid duplication

Applied to files:

  • packages/workers/src/lib/send-invitation-email.ts
📚 Learning: 2026-01-01T23:31:43.756Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/api-routes.mdc:0-0
Timestamp: 2026-01-01T23:31:43.756Z
Learning: Applies to packages/workers/src/config/validation.js : Add new validation schemas to `src/config/validation.js` and reuse `commonFields` when possible

Applied to files:

  • packages/workers/src/lib/send-invitation-email.ts
📚 Learning: 2026-01-01T23:32:23.488Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/workers.mdc:0-0
Timestamp: 2026-01-01T23:32:23.488Z
Learning: Applies to packages/workers/src/routes/**/*.{js,ts,jsx,tsx} : ALWAYS validate request bodies using `validateRequest` middleware from the validation config rather than manual validation

Applied to files:

  • packages/workers/src/lib/send-invitation-email.ts
📚 Learning: 2026-01-06T23:56:57.354Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/error-handling.mdc:0-0
Timestamp: 2026-01-06T23:56:57.354Z
Learning: Applies to packages/workers/**/*.{js,ts} : Use validation middleware (validateRequest) for request validation instead of manually validating request bodies in routes

Applied to files:

  • packages/workers/src/lib/send-invitation-email.ts
📚 Learning: 2026-01-05T01:00:58.113Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/workers-testing.mdc:0-0
Timestamp: 2026-01-05T01:00:58.113Z
Learning: Applies to {packages/workers/src/**/*.test.js,packages/workers/src/**/__tests__/**/*.js} : Seed test data using provided seed* helpers (seedUser, seedOrganization, seedOrgMember, seedProject, etc.) from packages/workers/src/__tests__/helpers.js with Zod schema validation

Applied to files:

  • packages/workers/src/lib/send-invitation-email.ts
📚 Learning: 2026-01-01T23:32:23.488Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/workers.mdc:0-0
Timestamp: 2026-01-01T23:32:23.488Z
Learning: Applies to packages/workers/src/routes/**/*.{js,ts,jsx,tsx} : Use `validateQueryParams` middleware for validating query parameters in routes

Applied to files:

  • packages/workers/src/lib/send-invitation-email.ts
📚 Learning: 2026-01-01T23:31:43.756Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/api-routes.mdc:0-0
Timestamp: 2026-01-01T23:31:43.756Z
Learning: Applies to packages/workers/src/routes/**/*.js : Use `validateQueryParams` middleware for query string validation in API routes

Applied to files:

  • packages/workers/src/lib/send-invitation-email.ts
📚 Learning: 2026-01-17T00:25:03.945Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/instructions/api-routes.instructions.md:0-0
Timestamp: 2026-01-17T00:25:03.945Z
Learning: Applies to packages/workers/**/routes/**/*.{js,ts} : Always use `validateRequest` middleware for request body validation in API routes, imported from '../config/validation.js'

Applied to files:

  • packages/workers/src/lib/send-invitation-email.ts
📚 Learning: 2026-01-17T16:09:36.920Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-17T16:09:36.920Z
Learning: Applies to packages/**/*.{ts,tsx,js,jsx} : Use the Agent TODO convention `// TODO(agent): Brief description` for incomplete work, flagging items for future attention, known limitations, and documentation section references

Applied to files:

  • packages/workers/src/lib/send-invitation-email.ts
🔇 Additional comments (9)
packages/workers/src/lib/send-invitation-email.ts (5)

13-24: Add Zod runtime validation for input parameters.

This helper is called with request-derived strings (email, token, projectName, etc.). A Zod schema should validate these inputs at runtime to fail fast on malformed data.

+import { z } from 'zod';
+
+const SendInvitationEmailSchema = z.object({
+  email: z.string().email(),
+  token: z.string().min(1),
+  projectName: z.string().min(1),
+  inviterName: z.string().min(1),
+  role: z.string().min(1),
+});
+
 interface SendInvitationEmailParams {
   env: Env;
   email: string;
   token: string;
   projectName: string;
   inviterName: string;
   role: string;
 }

Then parse at function entry:

const validated = SendInvitationEmailSchema.parse({ email, token, projectName, inviterName, role });

As per coding guidelines: "Use Zod for schema and input validation on backend services".


49-87: LGTM!

The temporary auth instance setup correctly mirrors the production configuration. The expiresIn value (60 * MAGIC_LINK_EXPIRY_MINUTES) matches the production magicLink plugin configuration, ensuring consistent token validity.


89-104: LGTM!

The magic link generation correctly normalizes the email to lowercase and properly validates that the URL was captured, throwing a domain error with appropriate context if generation fails.


106-109: LGTM!

The security concern from the previous review has been addressed. The magic link URL is now properly redacted (replacing the token value with "REDACTED") before logging in non-production environments.


128-141: LGTM!

The error handling strategy is well-designed. Since queueEmail throws errors rather than swallowing them, the try-catch correctly distinguishes success from failure. Returning { emailQueued: false } on queue failure is appropriate since the invitation record already exists in the database and can be resent later.

.mcp.md (2)

21-24: LGTM!

The Sentry MCP server configuration correctly uses HTTPS for secure communication with the external endpoint.


3-7: The path packages/mcp/dist/server.js is correctly configured as a build artifact. TypeScript is set to compile source files from packages/mcp/src/ to packages/mcp/dist/ via the tsc build command. The package.json bin entry and start script both reference this path, confirming it is the intended configuration. No action required.

packages/workers/src/auth/config.ts (2)

8-8: Centralizing the Stripe API version here is a good cleanup.

Using the shared constant keeps auth on the same Stripe contract as the rest of the worker and avoids version drift.

Also applies to: 225-226


14-23: The queue-based auth email hooks look solid.

Moving these flows onto shared templates plus queueEmail keeps the hooks thin, and awaiting the enqueue means auth does not report success until the message has actually been accepted by the queue.

Also applies to: 177-181, 443-449, 464-470

Comment thread .mcp.md
Comment on lines +16 to +20
"ark-ui": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@ark-ui/mcp"]
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Security concern: npx -y auto-accepts package installation.

The -y flag causes npx to automatically install @ark-ui/mcp without user confirmation. This poses a security risk if:

  • The package is compromised or replaced with malicious code
  • A typosquatting attack targets the package name
  • The package is unexpectedly updated to a malicious version

Consider one of the following approaches:

  1. Pre-install the package as a dev dependency to lock the version
  2. Remove the -y flag to require explicit user confirmation
  3. Use a specific version constraint: npx @ark-ui/mcp@^1.0.0

Additionally, the AI summary mentions that "ark-ui has been removed from the public configuration," but it appears in this file. Clarify whether this configuration file is intended to be private/local-only.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.mcp.md around lines 16 - 20, The current MCP entry uses npx with the
auto-accept flag (-y) for the "ark-ui" tool which risks silently installing a
malicious/typosquatted package; replace this with a safer approach: either
pre-install `@ark-ui/mcp` as a devDependency and point the config to the local
binary, remove the -y flag to require user confirmation when invoking npx, or
pin a specific safe version using npx `@ark-ui/mcp`@<semver> to avoid implicit
upgrades—also confirm whether the "ark-ui" configuration in this file is
intended to be private/local-only and, if so, mark or move it to a local-only
config to avoid exposing it in public repos.

@InfinityBowman InfinityBowman merged commit c915bfe into main Mar 7, 2026
5 checks passed
@InfinityBowman InfinityBowman deleted the switch-to-email-queue branch March 7, 2026 02:41
@coderabbitai coderabbitai Bot mentioned this pull request Apr 7, 2026
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.

2 participants