Skip to content

✨ server: add queue on user creation#619

Open
aguxez wants to merge 7 commits intomainfrom
feature/webhook-queue
Open

✨ server: add queue on user creation#619
aguxez wants to merge 7 commits intomainfrom
feature/webhook-queue

Conversation

@aguxez
Copy link
Contributor

@aguxez aguxez commented Jan 5, 2026

closes #177

Summary by CodeRabbit

  • New Features

    • Background job queue for async Alchemy webhook subscriber updates and worker lifecycle management.
    • Webhook readiness check that surfaces a retriable error when webhook isn’t initialized.
  • Bug Fixes

    • Credential creation now returns a retriable 503 when webhook setup is pending and logs debug info on webhook init.
  • Tests

    • Added unit tests and mocks covering queue processing, credential creation, and auth/registration flows.

Open with Devin

This is part 1 of 2 in a stack made with GitButler:

Greptile Summary

This PR migrates the Alchemy webhook subscriber registration from a synchronous inline fetch call to an async BullMQ job queue, and introduces a WebhookNotReadyError (503) guard so that credential creation fails gracefully when the webhook hasn't been initialized yet. Redis is also split into two connections — one with maxRetriesPerRequest: null for BullMQ compatibility and one with maxRetriesPerRequest: 3 for regular request handling.

Key changes:

  • New server/queues/alchemyQueue.ts: defines the BullMQ queue, worker, processor, and lifecycle helpers (initializeWorker, close).
  • createCredential.ts: guards on webhookId before DB insert, removes the direct updateWebhookAddresses call, and fire-and-forgets a BullMQ ADD_SUBSCRIBER job after the credential is committed.
  • authentication.ts and registration.ts: catch WebhookNotReadyError and return 503 with a retriable tag.
  • redis.ts: splits into a BullMQ-safe default connection (maxRetriesPerRequest: null) and a request-scoped requestRedis (maxRetriesPerRequest: 3).
  • The close() function in alchemyQueue.ts uses Promise.all rather than Promise.allSettled, meaning if either the worker or queue close call rejects, the alchemyWorker = undefined / _alchemyQueue = undefined cleanup never runs and leaves stale module-level references.
  • No retry or backoff strategy is configured on the queue, so failed jobs are permanently marked failed after a single attempt (also noted in a previous review thread).

Confidence Score: 3/5

  • Mostly safe to merge, but two open issues from prior review (shutdown race condition and no retry/backoff) plus a new cleanup bug in close() should be addressed before this reaches production.
  • The core logic is sound and the 503 guard on webhook readiness is correctly implemented across both auth paths. However, the close() function in alchemyQueue.ts uses Promise.all which means cleanup state is never reset if either close call fails — this can cause subtle bugs on repeated close calls or in tests. Combined with the previously noted shutdown race condition (queue and Redis closing concurrently) and lack of retry/backoff on failed jobs, the queue infrastructure has three reliability gaps that could affect production.
  • server/queues/alchemyQueue.tsclose() cleanup bug, missing retry config, and inconsistent Sentry imports. server/index.ts — shutdown ordering of queue vs Redis.

Important Files Changed

Filename Overview
server/queues/alchemyQueue.ts New BullMQ queue and worker infrastructure for async Alchemy webhook subscription. close() uses Promise.all which skips cleanup if either close call fails; should use Promise.allSettled. Mixing @sentry/core and @sentry/node imports is inconsistent. No retry/backoff configured on the queue (noted in prior review). Both Queue and Worker share the same redis connection (BullMQ will duplicate it internally, so this is safe).
server/utils/createCredential.ts Correctly gates on webhookId before inserting credentials (returns 503 via WebhookNotReadyError if not ready), then enqueues a fire-and-forget BullMQ job after DB commit. The fire-and-forget pattern is intentional but means a silent Redis failure at enqueue time leaves the user unsubscribed from the webhook with only a Sentry event as signal.
server/utils/redis.ts Correctly splits Redis connections: default export uses maxRetriesPerRequest: null for BullMQ compatibility; requestRedis uses maxRetriesPerRequest: 3 for API request handlers. closeRedis closes both connections.
server/index.ts Adds worker initialization and BullMQ/Redis close calls. Both closeAlchemyQueue() and closeRedis() run concurrently in Promise.allSettled, creating a shutdown race condition (noted in prior review).
server/api/auth/authentication.ts Adds WebhookNotReadyError handler in the siwe credential creation path (mirrors the registration.ts approach) and switches to requestRedis. Addresses the gap noted in prior review.
server/api/auth/registration.ts Adds WebhookNotReadyError 503 handling and switches to requestRedis. Straightforward and correct.
server/test/queues/alchemyQueue.test.ts New tests covering the queue processor: happy path, unknown job name, and Alchemy API failure. Does not cover initializeWorker or the event handler callbacks, but core processing logic is tested.
server/test/utils/createCredential.test.ts New tests covering queue job enqueue on credential creation, WebhookNotReadyError when webhookId is absent, and exception capture on queue failure. All three cases are well-structured and use proper hoisted mocks.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Auth as Auth Endpoint
    participant CC as createCredential
    participant DB as Database
    participant AQ as AlchemyQueue (BullMQ)
    participant Redis
    participant W as AlchemyWorker
    participant Alchemy as Alchemy API

    Client->>Auth: POST register or SIWE
    Auth->>CC: createCredential(ctx, credentialId)
    CC->>CC: check webhookId is set
    alt webhookId not initialized
        CC-->>Auth: throw WebhookNotReadyError
        Auth-->>Client: 503 retriable
    else webhookId ready
        CC->>DB: INSERT credential row
        CC->>CC: setSignedCookie plus sardine customer
        CC-)AQ: queue.add(ADD_SUBSCRIBER, data) fire-and-forget
        CC-->>Auth: return credential info
        Auth-->>Client: 200 OK
        Note over AQ,W: Background processing
        AQ->>Redis: persist job
        W->>Redis: dequeue job
        W->>Alchemy: PATCH update-webhook-addresses
        alt success
            Alchemy-->>W: 200
        else failure
            Alchemy-->>W: error
            W-->>W: mark failed, no retry configured
        end
    end
Loading

Last reviewed commit: 65ebdb9

@changeset-bot
Copy link

changeset-bot bot commented Jan 5, 2026

🦋 Changeset detected

Latest commit: f520abb

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@exactly/server Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link

coderabbitai bot commented Jan 5, 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

Adds an asynchronous BullMQ queue and worker for Alchemy webhook subscriber updates, routes webhook subscription work through the queue from credential creation, introduces WebhookNotReadyError with 503 handling in auth routes, expands tests and mocks, and integrates worker lifecycle into server startup/shutdown.

Changes

Cohort / File(s) Summary
Queue Implementation
server/queues/alchemyQueue.ts, server/test/queues/alchemyQueue.test.ts
New BullMQ-backed queue + worker for Alchemy jobs (type AlchemyJobData, AlchemyJob, processor, initialize/close). Tests cover ADD_SUBSCRIBER request shape, unknown job handling, and API failure paths.
Credential Creation & Readiness
server/utils/createCredential.ts, server/test/utils/createCredential.test.ts
createCredential now throws/export WebhookNotReadyError when webhookId missing; enqueues AlchemyJob.ADD_SUBSCRIBER via getAlchemyQueue() instead of calling update directly. Tests assert job enqueueing and error capture behavior.
Auth Routes Error Handling
server/api/auth/authentication.ts, server/api/auth/registration.ts
Wrap createCredential in try/catch to return 503 when WebhookNotReadyError is thrown; other errors rethrown.
Server Lifecycle
server/index.ts
Start initializeWorker() on non-VITEST startup and include queue close in shutdown sequence (closeAlchemyQueue()).
Removed synchronous Alchemy helper
server/utils/alchemy.ts, server/test/mocks/alchemy.ts
Removed updateWebhookAddresses function and its mock counterpart (synchronous webhook update removed in favor of queue).
Tests & Mocks updates
server/test/mocks/redis.ts, server/test/mocks/sentry.ts, server/test/api/auth.test.ts, server/test/api/registration.test.ts
Adjusted Redis mock export shape; replaced simple Sentry mock with comprehensive mocked API; tests mock getAlchemyQueue() to stub queue .add() behavior.
Activity logging & changelog
server/hooks/activity.ts, .changeset/short-cars-return.md
Added debug logs around webhook initialization and added a changeset declaring a patch release for @exactly/server.

Sequence Diagram

sequenceDiagram
    actor User
    participant Auth as Auth Handler
    participant Cred as createCredential
    participant Queue as Alchemy Queue
    participant Worker as Queue Worker
    participant Alchemy as Alchemy API

    User->>Auth: register/login request
    Auth->>Cred: createCredential()

    alt webhookId available
        Cred->>Queue: enqueue ADD_SUBSCRIBER (account, webhookId)
        Queue-->>Cred: job queued
        Cred-->>Auth: success
        Auth-->>User: 200 OK

        Note over Queue,Worker: background processing
        Queue->>Worker: process ADD_SUBSCRIBER
        Worker->>Alchemy: PATCH /addresses (headers + payload)
        alt Alchemy 2xx
            Alchemy-->>Worker: 200 OK
            Worker-->>Queue: job completed
        else non-2xx
            Alchemy-->>Worker: error
            Worker-->>Sentry: log error
            Worker-->>Queue: job failed
        end
    else webhookId missing
        Cred-->>Cred: throw WebhookNotReadyError
        Auth->>Auth: catch WebhookNotReadyError
        Auth-->>User: 503 Service Unavailable (retriable)
    end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested Reviewers

  • cruzdanilo
  • nfmelendez
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: implementing a queue system to handle user creation, which directly addresses the linked issue about handling Alchemy webhook subscription failures.
Linked Issues check ✅ Passed The PR implements a queue-based approach to handle Alchemy webhook subscription failures, addressing issue #177's requirement to better manage webhook subscription errors during user creation.
Out of Scope Changes check ✅ Passed All changes are within scope: the new queue infrastructure, webhook subscription handling, error recovery mechanism, and necessary test coverage directly support the primary objective of handling Alchemy webhook failures.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/webhook-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.

@gemini-code-assist
Copy link

Summary of Changes

Hello @aguxez, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a robust queueing system for handling Alchemy webhook subscription updates. By integrating BullMQ, the application now processes these updates asynchronously, moving the task from the main request-response cycle to a background worker. This change enhances the system's reliability and performance by preventing blocking operations and providing built-in retry capabilities for external API calls, particularly when a new user credential is created.

Highlights

  • Introduction of Alchemy Queue: A new BullMQ queue (alchemyQueue) and its worker are implemented to manage Alchemy-related background tasks, specifically for updating webhook subscriptions.
  • Asynchronous Webhook Updates: The process of adding a subscriber to an Alchemy webhook is now offloaded from the main thread to the alchemyQueue, improving responsiveness and enabling retry mechanisms.
  • Refactoring createCredential: The createCredential utility function no longer directly calls the Alchemy API; instead, it enqueues an ADD_SUBSCRIBER job to the alchemyQueue.
  • New Queue Constants: Dedicated constants (QueueName and AlchemyJob) are introduced for better organization and type safety of queue names and job types.
  • Testing for Queue Functionality: New test files (alchemyQueue.test.ts and createCredential.test.ts) have been added to ensure the correct behavior of the Alchemy queue processor and its integration with credential creation.
  • Environment Configuration Update: The openapi.ts script now explicitly sets REDIS_URL and DISABLE_WORKERS to manage worker initialization during schema generation.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

gemini-code-assist[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@aguxez aguxez force-pushed the feature/webhook-queue branch 2 times, most recently from 32c8a33 to 0dc7771 Compare January 6, 2026 12:00
coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@aguxez aguxez force-pushed the feature/webhook-queue branch from 0dc7771 to 63b777f Compare January 6, 2026 12:28
@sentry
Copy link

sentry bot commented Jan 6, 2026

Codecov Report

❌ Patch coverage is 84.61538% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 71.24%. Comparing base (3b75acd) to head (f520abb).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
server/utils/createCredential.ts 85.71% 3 Missing and 2 partials ⚠️
server/api/auth/registration.ts 50.00% 2 Missing ⚠️
server/hooks/activity.ts 75.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #619      +/-   ##
==========================================
+ Coverage   71.13%   71.24%   +0.11%     
==========================================
  Files         211      211              
  Lines        8349     8427      +78     
  Branches     2727     2750      +23     
==========================================
+ Hits         5939     6004      +65     
- Misses       2132     2140       +8     
- Partials      278      283       +5     
Flag Coverage Δ
e2e 66.84% <50.00%> (-4.29%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

coderabbitai[bot]

This comment was marked as resolved.

@cruzdanilo
Copy link
Member

@cursor review

@cursor
Copy link

cursor bot commented Jan 6, 2026

PR Summary

Cursor Bugbot is generating a summary for commit 63b777f. Configure here.

@cursor
Copy link

cursor bot commented Jan 6, 2026

PR Summary

Introduces background processing for Alchemy webhook address updates and wires it into credential creation.

  • Adds bullmq queue/worker (alchemyQueue, processor) with Sentry tracing and Redis config; supports disabling workers via DISABLE_WORKERS
  • createCredential now enqueues AlchemyJob.ADD_SUBSCRIBER instead of calling Alchemy directly
  • Adds tests for queue processor and createCredential queuing behavior; updates openapi script env to set REDIS_URL and DISABLE_WORKERS

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

cursor[bot]

This comment was marked as resolved.

@aguxez aguxez force-pushed the feature/webhook-queue branch from 63b777f to 03cbfe4 Compare January 7, 2026 11:05
sentry[bot]

This comment was marked as resolved.

@aguxez aguxez force-pushed the feature/webhook-queue branch from 03cbfe4 to 54b79be Compare January 7, 2026 11:12
Copy link

@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: 6

🤖 Fix all issues with AI agents
In @server/queues/alchemyQueue.ts:
- Line 9: The file directly reads process.env.DISABLE_WORKERS into the const
disableWorkers which violates the rule to not access process.env in application
code; change the module to accept configuration via dependency injection (e.g.,
export a factory like createAlchemyQueue(config) or accept a Config parameter)
and replace direct usage of disableWorkers with config.disableWorkers (or
equivalent) throughout the file so configuration is loaded once at startup and
passed in rather than read from process.env inside this module.
- Around line 21-26: The code directly reads process.env to build the Redis
`connection` object and uses Number() for port parsing; change this by
introducing a factory function (e.g., export function
createRedisConnection(redisConfig: { url?: string; host?: string; port?: string
| number })) that takes the preloaded config instead of accessing process.env
inside this module, and inside that function build the connection: if
redisConfig.url use new Redis(redisConfig.url, { maxRetriesPerRequest: null }),
otherwise use { host: redisConfig.host ?? "localhost", port:
Number.parseInt(String(redisConfig.port ?? "6379"), 10) }; update call sites to
pass the startup-loaded config into createRedisConnection instead of relying on
environment access here.
- Around line 41-68: The span.setStatus calls in processor (inside the
AlchemyJob.ADD_SUBSCRIBER case and the default case) use the magic number 2;
replace that literal with a named constant or the Sentry enum (e.g., import
SpanStatusCode from "@sentry/node" and use SpanStatusCode.Error) or define a
local constant like SPAN_STATUS_ERROR and use it in both span.setStatus calls to
improve readability and consistency.
- Around line 72-100: The Worker instance created for QueueName.ALCHEMY is not
stored, preventing graceful shutdown; assign it to a module-scoped variable
(e.g., let alchemyWorker: Worker | undefined = new Worker(...) or create it then
set alchemyWorker = worker) so you can call alchemyWorker?.close() during
application termination/shutdown handling; ensure the variable is exported or
accessible by your existing shutdown routine and keep existing event handlers
(.on("failed"/"completed"/"active"/"error")) attached to the stored worker.

In @server/test/queues/alchemyQueue.test.ts:
- Line 54: The test assertion includes an unnecessary type assertion on the
headers matcher; remove the trailing "as Record<string, string>" from the
headers line so the matcher uses expect.objectContaining({ "X-Alchemy-Token":
"mock-token" }) directly (update the headers property in the assertion where
expect.objectContaining is used).

In @server/test/utils/createCredential.test.ts:
- Line 67: Remove the redundant type assertion on the account matcher: locate
the object assigning account: expect.stringMatching(/^0x/) as string and simply
use account: expect.stringMatching(/^0x/); the expect.stringMatching call
already returns the correct matcher type so drop the "as string" cast to clean
up the test.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 63b777f and 54b79be.

📒 Files selected for processing (7)
  • .changeset/short-cars-return.md
  • server/queues/alchemyQueue.ts
  • server/queues/constants.ts
  • server/script/openapi.ts
  • server/test/queues/alchemyQueue.test.ts
  • server/test/utils/createCredential.test.ts
  • server/utils/createCredential.ts
🧰 Additional context used
📓 Path-based instructions (7)
server/**/*.ts

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

server/**/*.ts: Use c.var object to pass strongly-typed data between Hono middleware and route handlers; do not use c.set
All request validation (headers, body, params) must be handled by @hono/valibot-validator middleware; do not perform manual validation inside route handlers
Use Hono's built-in error handling by throwing new HTTPException() for expected errors; unhandled errors will be caught and logged automatically
Enforce Node.js best practices using ESLint plugin:n/recommended configuration
Enforce Drizzle ORM best practices using ESLint plugin:drizzle/all configuration, including requiring where clauses for update and delete operations
Use Drizzle ORM query builder for all database interactions; do not write raw SQL queries unless absolutely unavoidable
All authentication and authorization logic must be implemented in Hono middleware
Do not access process.env directly in application code; load all configuration and secrets once at startup and pass them through dependency injection or context
Avoid long-running, synchronous operations; use async/await correctly and be mindful of CPU-intensive tasks to prevent blocking the event loop

Files:

  • server/test/utils/createCredential.test.ts
  • server/script/openapi.ts
  • server/queues/constants.ts
  • server/test/queues/alchemyQueue.test.ts
  • server/queues/alchemyQueue.ts
  • server/utils/createCredential.ts
**/*.{js,ts,tsx,jsx,sol}

📄 CodeRabbit inference engine (AGENTS.md)

Follow linter/formatter (eslint, prettier, solhint) strictly with high strictness level. No any type.

Files:

  • server/test/utils/createCredential.test.ts
  • server/script/openapi.ts
  • server/queues/constants.ts
  • server/test/queues/alchemyQueue.test.ts
  • server/queues/alchemyQueue.ts
  • server/utils/createCredential.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Omit redundant type names in variable declarations - let the type system explain itself

**/*.{ts,tsx}: Use PascalCase for TypeScript types and interfaces
Use valibot for all runtime validation of API inputs, environment variables, and other data; define schemas once and reuse them
Infer TypeScript types from valibot schemas using type User = v.Input<typeof UserSchema> instead of manually defining interfaces

Files:

  • server/test/utils/createCredential.test.ts
  • server/script/openapi.ts
  • server/queues/constants.ts
  • server/test/queues/alchemyQueue.test.ts
  • server/queues/alchemyQueue.ts
  • server/utils/createCredential.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Omit contextual names - don't repeat class/module names in members
Omit meaningless words like 'data', 'state', 'manager', 'engine', 'value' from variable and function names unless they add disambiguation

**/*.{ts,tsx,js,jsx}: Prefer function declarations for all multi-line functions; use function expressions or arrow functions only for single-line implementations
Prefer const for all variable declarations by default; only use let if the variable's value will be reassigned
Declare each variable on its own line with its own const or let keyword, not multiple declarations on one line
Use camelCase for TypeScript variables and functions
Always use import type { ... } for type imports
Use relative paths for all imports within the project; avoid tsconfig path aliases
Follow eslint-plugin-import order: react, external libraries, then relative paths
Use object and array destructuring to access and use properties
Use object method shorthand syntax when a function is a property of an object
Prefer optional chaining (?.), nullish coalescing (??), object and array spreading (...), and for...of loops over traditional syntax
Do not use abbreviations or cryptic names; write out full words like error, parameters, request instead of err, params, req
Use Number.parseInt() instead of the global parseInt() function when parsing numbers
All classes called with new must use PascalCase
Use Buffer.from(), Buffer.alloc(), or Buffer.allocUnsafe() instead of the deprecated new Buffer()
Use @ts-expect-error instead of @ts-ignore; follow it immediately with a single-line lowercase comment explaining why the error is expected, without separators like - or :
Do not include the type in a variable's name; let the static type system do its job (e.g., use const user: User not const userObject: User)
Do not repeat the name of a class or module within its members; omit contextual names (e.g., use `class User { getProfil...

Files:

  • server/test/utils/createCredential.test.ts
  • server/script/openapi.ts
  • server/queues/constants.ts
  • server/test/queues/alchemyQueue.test.ts
  • server/queues/alchemyQueue.ts
  • server/utils/createCredential.ts
server/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

server/**/*.{ts,tsx}: Server API: implement schema-first approach using OpenAPI via hono with validation via valibot middleware
Server database: drizzle schema is source of truth. Migrations required. No direct database access in handlers - use c.var.db

Files:

  • server/test/utils/createCredential.test.ts
  • server/script/openapi.ts
  • server/queues/constants.ts
  • server/test/queues/alchemyQueue.test.ts
  • server/queues/alchemyQueue.ts
  • server/utils/createCredential.ts
**/*.ts

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

For files with a single default export, name the file identically to the export; for files with multiple exports, use camelCase with a strong preference for a single word

Files:

  • server/test/utils/createCredential.test.ts
  • server/script/openapi.ts
  • server/queues/constants.ts
  • server/test/queues/alchemyQueue.test.ts
  • server/queues/alchemyQueue.ts
  • server/utils/createCredential.ts
**/.changeset/*.md

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

Use a lowercase sentence in the imperative present tense for changeset summaries

Files:

  • .changeset/short-cars-return.md
🧠 Learnings (6)
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Zero config local dev environment: no `.env` files, mock all external services

Applied to files:

  • server/script/openapi.ts
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Server stack: use hono, node.js, drizzle orm, postgres

Applied to files:

  • server/script/openapi.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/**/*.ts : Do not access `process.env` directly in application code; load all configuration and secrets once at startup and pass them through dependency injection or context

Applied to files:

  • server/script/openapi.ts
  • server/queues/alchemyQueue.ts
📚 Learning: 2025-12-31T00:23:55.034Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 610
File: .changeset/ready-experts-fly.md:1-2
Timestamp: 2025-12-31T00:23:55.034Z
Learning: In the exactly/exa repository, allow and require empty changeset files (containing only --- separators) when changes are not user-facing and do not warrant a version bump. This is needed because CI runs changeset status --since origin/main and requires a changeset file to exist. Ensure such empty changesets are used only for non-user-facing changes and document the rationale in the commit or changelog notes.

Applied to files:

  • .changeset/short-cars-return.md
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/**/*.ts : Avoid long-running, synchronous operations; use `async/await` correctly and be mindful of CPU-intensive tasks to prevent blocking the event loop

Applied to files:

  • server/queues/alchemyQueue.ts
📚 Learning: 2025-12-30T15:03:28.449Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/style.mdc:0-0
Timestamp: 2025-12-30T15:03:28.449Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use `ts-expect-error` instead of `ts-ignore`; follow it immediately with a single-line lowercase comment explaining why the error is expected, without separators like `-` or `:`

Applied to files:

  • server/utils/createCredential.ts
🧬 Code graph analysis (3)
server/test/queues/alchemyQueue.test.ts (1)
server/queues/alchemyQueue.ts (2)
  • AlchemyJobData (14-19)
  • processor (41-68)
server/queues/alchemyQueue.ts (1)
server/queues/constants.ts (2)
  • QueueName (1-3)
  • AlchemyJob (7-9)
server/utils/createCredential.ts (2)
server/queues/alchemyQueue.ts (1)
  • alchemyQueue (33-33)
server/queues/constants.ts (1)
  • AlchemyJob (7-9)
🔇 Additional comments (12)
server/queues/constants.ts (1)

1-11: LGTM! Clean constants definition.

The use of as const with derived enum types is idiomatic TypeScript, providing strong type safety for queue and job names.

server/script/openapi.ts (1)

25-26: LGTM! Appropriate environment configuration for OpenAPI generation.

The explicit Redis URL and DISABLE_WORKERS flag correctly prevent worker initialization during spec generation while maintaining the zero-config local dev environment.

.changeset/short-cars-return.md (1)

1-5: LGTM! Changeset follows the required format.

The summary correctly uses lowercase imperative present tense as per coding guidelines.

server/test/utils/createCredential.test.ts (1)

1-58: LGTM! Comprehensive test coverage with proper mocking.

The test suite correctly validates both the happy path (job enqueuing) and error handling (exception capture), with well-isolated mocks.

Also applies to: 72-82

server/test/queues/alchemyQueue.test.ts (1)

1-53: LGTM! Comprehensive test coverage for queue processor.

The test suite validates all critical paths: successful webhook updates, unknown job handling, and API failure scenarios. The tests correctly expect the processor to throw errors for edge cases.

Also applies to: 55-90

server/utils/createCredential.ts (4)

6-7: LGTM!

The Sentry import changes appropriately separate setUser from @sentry/core and captureException from @sentry/node, following the modular structure of the Sentry SDK.


14-14: LGTM!

The database import addition is necessary for the database operations and follows the project's import conventions.


21-22: LGTM!

The queue-related imports are correctly added to support the new asynchronous job enqueueing approach, replacing the previous synchronous HTTP call.


54-54: Consider consistent error capture options.

The captureException call at line 54 doesn't include a level option, while the similar sardine error capture at line 56 specifies { level: "error" }. For consistency, consider adding the same error level to both non-critical async operations.

🔎 Suggested consistency improvement
-    alchemyQueue.add(AlchemyJob.ADD_SUBSCRIBER, { account, webhookId }).catch(captureException),
+    alchemyQueue.add(AlchemyJob.ADD_SUBSCRIBER, { account, webhookId }).catch((error: unknown) =>
+      captureException(error, { level: "error" }),
+    ),

Likely an incorrect or invalid review comment.

server/queues/alchemyQueue.ts (3)

1-6: LGTM!

The imports are correctly structured with proper type imports and necessary dependencies for the queue implementation.


11-19: LGTM!

The AlchemyJobData interface is well-documented with clear JSDoc comments explaining each field's purpose.


28-33: LGTM!

The queue is correctly instantiated with proper documentation explaining its purpose for offloading webhook subscription updates and enabling retries.

@aguxez aguxez force-pushed the feature/webhook-queue branch from 54b79be to 61cb4bb Compare January 7, 2026 11:32
sentry[bot]

This comment was marked as resolved.

Copy link

@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

🤖 Fix all issues with AI agents
In @server/test/queues/alchemyQueue.test.ts:
- Line 54: The test contains an unnecessary type assertion on the headers
matcher: remove the trailing "as Record<string, string>" from the
expect.objectContaining call (the matcher returned by expect.objectContaining({
"X-Alchemy-Token": "mock-token" }) is already correctly typed); update the line
using the headers matcher (the expression starting with
expect.objectContaining({ "X-Alchemy-Token": "mock-token" })) to omit the cast
and run tests to confirm no type errors remain.

In @server/test/utils/createCredential.test.ts:
- Line 67: Remove the redundant type assertion on the account matcher: replace
the property value "expect.stringMatching(/^0x/) as string" with just
"expect.stringMatching(/^0x/)" in the createCredential.test.ts test (the object
property named account), since expect.stringMatching already returns the correct
matcher type.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 54b79be and 61cb4bb.

📒 Files selected for processing (7)
  • .changeset/short-cars-return.md
  • server/queues/alchemyQueue.ts
  • server/queues/constants.ts
  • server/script/openapi.ts
  • server/test/queues/alchemyQueue.test.ts
  • server/test/utils/createCredential.test.ts
  • server/utils/createCredential.ts
🧰 Additional context used
📓 Path-based instructions (7)
**/.changeset/*.md

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

Use a lowercase sentence in the imperative present tense for changeset summaries

Files:

  • .changeset/short-cars-return.md
server/**/*.ts

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

server/**/*.ts: Use c.var object to pass strongly-typed data between Hono middleware and route handlers; do not use c.set
All request validation (headers, body, params) must be handled by @hono/valibot-validator middleware; do not perform manual validation inside route handlers
Use Hono's built-in error handling by throwing new HTTPException() for expected errors; unhandled errors will be caught and logged automatically
Enforce Node.js best practices using ESLint plugin:n/recommended configuration
Enforce Drizzle ORM best practices using ESLint plugin:drizzle/all configuration, including requiring where clauses for update and delete operations
Use Drizzle ORM query builder for all database interactions; do not write raw SQL queries unless absolutely unavoidable
All authentication and authorization logic must be implemented in Hono middleware
Do not access process.env directly in application code; load all configuration and secrets once at startup and pass them through dependency injection or context
Avoid long-running, synchronous operations; use async/await correctly and be mindful of CPU-intensive tasks to prevent blocking the event loop

Files:

  • server/queues/alchemyQueue.ts
  • server/script/openapi.ts
  • server/utils/createCredential.ts
  • server/test/queues/alchemyQueue.test.ts
  • server/queues/constants.ts
  • server/test/utils/createCredential.test.ts
**/*.{js,ts,tsx,jsx,sol}

📄 CodeRabbit inference engine (AGENTS.md)

Follow linter/formatter (eslint, prettier, solhint) strictly with high strictness level. No any type.

Files:

  • server/queues/alchemyQueue.ts
  • server/script/openapi.ts
  • server/utils/createCredential.ts
  • server/test/queues/alchemyQueue.test.ts
  • server/queues/constants.ts
  • server/test/utils/createCredential.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Omit redundant type names in variable declarations - let the type system explain itself

**/*.{ts,tsx}: Use PascalCase for TypeScript types and interfaces
Use valibot for all runtime validation of API inputs, environment variables, and other data; define schemas once and reuse them
Infer TypeScript types from valibot schemas using type User = v.Input<typeof UserSchema> instead of manually defining interfaces

Files:

  • server/queues/alchemyQueue.ts
  • server/script/openapi.ts
  • server/utils/createCredential.ts
  • server/test/queues/alchemyQueue.test.ts
  • server/queues/constants.ts
  • server/test/utils/createCredential.test.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Omit contextual names - don't repeat class/module names in members
Omit meaningless words like 'data', 'state', 'manager', 'engine', 'value' from variable and function names unless they add disambiguation

**/*.{ts,tsx,js,jsx}: Prefer function declarations for all multi-line functions; use function expressions or arrow functions only for single-line implementations
Prefer const for all variable declarations by default; only use let if the variable's value will be reassigned
Declare each variable on its own line with its own const or let keyword, not multiple declarations on one line
Use camelCase for TypeScript variables and functions
Always use import type { ... } for type imports
Use relative paths for all imports within the project; avoid tsconfig path aliases
Follow eslint-plugin-import order: react, external libraries, then relative paths
Use object and array destructuring to access and use properties
Use object method shorthand syntax when a function is a property of an object
Prefer optional chaining (?.), nullish coalescing (??), object and array spreading (...), and for...of loops over traditional syntax
Do not use abbreviations or cryptic names; write out full words like error, parameters, request instead of err, params, req
Use Number.parseInt() instead of the global parseInt() function when parsing numbers
All classes called with new must use PascalCase
Use Buffer.from(), Buffer.alloc(), or Buffer.allocUnsafe() instead of the deprecated new Buffer()
Use @ts-expect-error instead of @ts-ignore; follow it immediately with a single-line lowercase comment explaining why the error is expected, without separators like - or :
Do not include the type in a variable's name; let the static type system do its job (e.g., use const user: User not const userObject: User)
Do not repeat the name of a class or module within its members; omit contextual names (e.g., use `class User { getProfil...

Files:

  • server/queues/alchemyQueue.ts
  • server/script/openapi.ts
  • server/utils/createCredential.ts
  • server/test/queues/alchemyQueue.test.ts
  • server/queues/constants.ts
  • server/test/utils/createCredential.test.ts
server/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

server/**/*.{ts,tsx}: Server API: implement schema-first approach using OpenAPI via hono with validation via valibot middleware
Server database: drizzle schema is source of truth. Migrations required. No direct database access in handlers - use c.var.db

Files:

  • server/queues/alchemyQueue.ts
  • server/script/openapi.ts
  • server/utils/createCredential.ts
  • server/test/queues/alchemyQueue.test.ts
  • server/queues/constants.ts
  • server/test/utils/createCredential.test.ts
**/*.ts

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

For files with a single default export, name the file identically to the export; for files with multiple exports, use camelCase with a strong preference for a single word

Files:

  • server/queues/alchemyQueue.ts
  • server/script/openapi.ts
  • server/utils/createCredential.ts
  • server/test/queues/alchemyQueue.test.ts
  • server/queues/constants.ts
  • server/test/utils/createCredential.test.ts
🧠 Learnings (6)
📚 Learning: 2025-12-31T00:23:55.034Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 610
File: .changeset/ready-experts-fly.md:1-2
Timestamp: 2025-12-31T00:23:55.034Z
Learning: In the exactly/exa repository, allow and require empty changeset files (containing only --- separators) when changes are not user-facing and do not warrant a version bump. This is needed because CI runs changeset status --since origin/main and requires a changeset file to exist. Ensure such empty changesets are used only for non-user-facing changes and document the rationale in the commit or changelog notes.

Applied to files:

  • .changeset/short-cars-return.md
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/**/*.ts : Do not access `process.env` directly in application code; load all configuration and secrets once at startup and pass them through dependency injection or context

Applied to files:

  • server/queues/alchemyQueue.ts
  • server/script/openapi.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/**/*.ts : Avoid long-running, synchronous operations; use `async/await` correctly and be mindful of CPU-intensive tasks to prevent blocking the event loop

Applied to files:

  • server/queues/alchemyQueue.ts
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Zero config local dev environment: no `.env` files, mock all external services

Applied to files:

  • server/script/openapi.ts
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Server stack: use hono, node.js, drizzle orm, postgres

Applied to files:

  • server/script/openapi.ts
📚 Learning: 2025-12-30T15:03:28.449Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/style.mdc:0-0
Timestamp: 2025-12-30T15:03:28.449Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use `ts-expect-error` instead of `ts-ignore`; follow it immediately with a single-line lowercase comment explaining why the error is expected, without separators like `-` or `:`

Applied to files:

  • server/utils/createCredential.ts
🧬 Code graph analysis (4)
server/queues/alchemyQueue.ts (1)
server/queues/constants.ts (2)
  • QueueName (1-3)
  • AlchemyJob (7-9)
server/utils/createCredential.ts (2)
server/queues/alchemyQueue.ts (1)
  • alchemyQueue (33-33)
server/queues/constants.ts (1)
  • AlchemyJob (7-9)
server/test/queues/alchemyQueue.test.ts (3)
.maestro/maestro.d.ts (1)
  • Response (19-19)
server/queues/constants.ts (1)
  • AlchemyJob (7-9)
server/queues/alchemyQueue.ts (2)
  • AlchemyJobData (14-19)
  • processor (41-68)
server/test/utils/createCredential.test.ts (3)
server/utils/createCredential.ts (1)
  • createCredential (24-61)
server/queues/alchemyQueue.ts (1)
  • alchemyQueue (33-33)
server/queues/constants.ts (1)
  • AlchemyJob (7-9)
🔇 Additional comments (9)
server/script/openapi.ts (1)

25-26: LGTM! Environment configuration aligns with new queue infrastructure.

The Redis URL now provides a proper connection string for local development, and the DISABLE_WORKERS flag enables control over worker initialization during spec generation.

.changeset/short-cars-return.md (1)

1-5: LGTM! Changeset follows guidelines.

The description uses lowercase imperative present tense as required by the coding guidelines.

server/test/utils/createCredential.test.ts (1)

1-82: LGTM! Test coverage is comprehensive.

The tests appropriately cover both the success path (job enqueuing) and error path (exception capture), with thorough mocking of dependencies.

server/queues/constants.ts (1)

1-11: LGTM! Clean constants definition.

The constants follow TypeScript best practices with as const assertions and proper type derivation. Naming conventions are correct throughout.

server/test/queues/alchemyQueue.test.ts (1)

1-90: LGTM! Comprehensive test coverage for the queue processor.

The tests cover all critical paths: successful processing, unknown job handling, and API failure scenarios. The mocking strategy is thorough and appropriate.

server/utils/createCredential.ts (1)

46-59: Queue-based approach for webhook subscription looks good.

The switch from synchronous HTTP PATCH to asynchronous queue processing improves reliability by enabling retries on API failures and avoids blocking the main request flow. The Promise.all correctly handles the concurrent operations with appropriate error capture.

server/queues/alchemyQueue.ts (3)

60-64: Unknown job handling now correctly throws - good fix.

The default case properly sets the span status and throws an error, ensuring unknown jobs fail fast rather than silently succeeding. This addresses the previous feedback about silent failures.


98-99: Error handler addition prevents process crashes - good fix.

The .on("error") handler correctly captures Redis connection failures and internal errors, preventing uncaught exceptions from crashing the process. This follows the pattern used elsewhere in the codebase (e.g., analytics.on("error", ...) in segment.ts).


41-68: Processor implementation is well-structured.

The processor correctly:

  • Uses Sentry tracing with appropriate span attributes
  • Handles the ADD_SUBSCRIBER job type with proper API call
  • Throws on non-ok responses with status details
  • Throws on unknown job names for fail-fast behavior

@aguxez aguxez force-pushed the feature/webhook-queue branch 3 times, most recently from f9b7be5 to 5e59af2 Compare January 7, 2026 13:53
Copy link

@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: 3

🤖 Fix all issues with AI agents
In @.changeset/short-cars-return.md:
- Line 5: Update the changelog summary in .changeset/short-cars-return.md by
removing the redundant "server:" prefix so the summary reads "add queue on user
creation" (i.e., replace "server: add queue on user creation" with "add queue on
user creation").

In @server/test/queues/alchemyQueue.test.ts:
- Line 54: Remove the redundant type assertion by deleting "as Record<string,
string>" from the headers matcher so it reads headers: expect.objectContaining({
"X-Alchemy-Token": "mock-token" }); leave the expect.objectContaining() usage
intact (in the test that sets headers) since Vitest will infer the matcher types
correctly.

In @server/test/utils/createCredential.test.ts:
- Line 67: The test includes a redundant type assertion on the account property:
remove the unnecessary "as string" cast and rely on expect.stringMatching(/^0x/)
directly (i.e., replace "account: expect.stringMatching(/^0x/) as string" with
"account: expect.stringMatching(/^0x/)" in the test using
expect.stringMatching).
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 61cb4bb and 5e59af2.

📒 Files selected for processing (7)
  • .changeset/short-cars-return.md
  • server/queues/alchemyQueue.ts
  • server/queues/constants.ts
  • server/script/openapi.ts
  • server/test/queues/alchemyQueue.test.ts
  • server/test/utils/createCredential.test.ts
  • server/utils/createCredential.ts
🧰 Additional context used
📓 Path-based instructions (7)
server/**/*.ts

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

server/**/*.ts: Use c.var object to pass strongly-typed data between Hono middleware and route handlers; do not use c.set
All request validation (headers, body, params) must be handled by @hono/valibot-validator middleware; do not perform manual validation inside route handlers
Use Hono's built-in error handling by throwing new HTTPException() for expected errors; unhandled errors will be caught and logged automatically
Enforce Node.js best practices using ESLint plugin:n/recommended configuration
Enforce Drizzle ORM best practices using ESLint plugin:drizzle/all configuration, including requiring where clauses for update and delete operations
Use Drizzle ORM query builder for all database interactions; do not write raw SQL queries unless absolutely unavoidable
All authentication and authorization logic must be implemented in Hono middleware
Do not access process.env directly in application code; load all configuration and secrets once at startup and pass them through dependency injection or context
Avoid long-running, synchronous operations; use async/await correctly and be mindful of CPU-intensive tasks to prevent blocking the event loop

Files:

  • server/script/openapi.ts
  • server/queues/constants.ts
  • server/test/queues/alchemyQueue.test.ts
  • server/queues/alchemyQueue.ts
  • server/utils/createCredential.ts
  • server/test/utils/createCredential.test.ts
**/*.{js,ts,tsx,jsx,sol}

📄 CodeRabbit inference engine (AGENTS.md)

Follow linter/formatter (eslint, prettier, solhint) strictly with high strictness level. No any type.

Files:

  • server/script/openapi.ts
  • server/queues/constants.ts
  • server/test/queues/alchemyQueue.test.ts
  • server/queues/alchemyQueue.ts
  • server/utils/createCredential.ts
  • server/test/utils/createCredential.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Omit redundant type names in variable declarations - let the type system explain itself

**/*.{ts,tsx}: Use PascalCase for TypeScript types and interfaces
Use valibot for all runtime validation of API inputs, environment variables, and other data; define schemas once and reuse them
Infer TypeScript types from valibot schemas using type User = v.Input<typeof UserSchema> instead of manually defining interfaces

Files:

  • server/script/openapi.ts
  • server/queues/constants.ts
  • server/test/queues/alchemyQueue.test.ts
  • server/queues/alchemyQueue.ts
  • server/utils/createCredential.ts
  • server/test/utils/createCredential.test.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Omit contextual names - don't repeat class/module names in members
Omit meaningless words like 'data', 'state', 'manager', 'engine', 'value' from variable and function names unless they add disambiguation

**/*.{ts,tsx,js,jsx}: Prefer function declarations for all multi-line functions; use function expressions or arrow functions only for single-line implementations
Prefer const for all variable declarations by default; only use let if the variable's value will be reassigned
Declare each variable on its own line with its own const or let keyword, not multiple declarations on one line
Use camelCase for TypeScript variables and functions
Always use import type { ... } for type imports
Use relative paths for all imports within the project; avoid tsconfig path aliases
Follow eslint-plugin-import order: react, external libraries, then relative paths
Use object and array destructuring to access and use properties
Use object method shorthand syntax when a function is a property of an object
Prefer optional chaining (?.), nullish coalescing (??), object and array spreading (...), and for...of loops over traditional syntax
Do not use abbreviations or cryptic names; write out full words like error, parameters, request instead of err, params, req
Use Number.parseInt() instead of the global parseInt() function when parsing numbers
All classes called with new must use PascalCase
Use Buffer.from(), Buffer.alloc(), or Buffer.allocUnsafe() instead of the deprecated new Buffer()
Use @ts-expect-error instead of @ts-ignore; follow it immediately with a single-line lowercase comment explaining why the error is expected, without separators like - or :
Do not include the type in a variable's name; let the static type system do its job (e.g., use const user: User not const userObject: User)
Do not repeat the name of a class or module within its members; omit contextual names (e.g., use `class User { getProfil...

Files:

  • server/script/openapi.ts
  • server/queues/constants.ts
  • server/test/queues/alchemyQueue.test.ts
  • server/queues/alchemyQueue.ts
  • server/utils/createCredential.ts
  • server/test/utils/createCredential.test.ts
server/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

server/**/*.{ts,tsx}: Server API: implement schema-first approach using OpenAPI via hono with validation via valibot middleware
Server database: drizzle schema is source of truth. Migrations required. No direct database access in handlers - use c.var.db

Files:

  • server/script/openapi.ts
  • server/queues/constants.ts
  • server/test/queues/alchemyQueue.test.ts
  • server/queues/alchemyQueue.ts
  • server/utils/createCredential.ts
  • server/test/utils/createCredential.test.ts
**/*.ts

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

For files with a single default export, name the file identically to the export; for files with multiple exports, use camelCase with a strong preference for a single word

Files:

  • server/script/openapi.ts
  • server/queues/constants.ts
  • server/test/queues/alchemyQueue.test.ts
  • server/queues/alchemyQueue.ts
  • server/utils/createCredential.ts
  • server/test/utils/createCredential.test.ts
**/.changeset/*.md

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

Use a lowercase sentence in the imperative present tense for changeset summaries

Files:

  • .changeset/short-cars-return.md
🧠 Learnings (6)
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Zero config local dev environment: no `.env` files, mock all external services

Applied to files:

  • server/script/openapi.ts
📚 Learning: 2025-12-23T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Server stack: use hono, node.js, drizzle orm, postgres

Applied to files:

  • server/script/openapi.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/**/*.ts : Do not access `process.env` directly in application code; load all configuration and secrets once at startup and pass them through dependency injection or context

Applied to files:

  • server/script/openapi.ts
  • server/queues/alchemyQueue.ts
📚 Learning: 2025-12-23T19:57:35.503Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/server.mdc:0-0
Timestamp: 2025-12-23T19:57:35.503Z
Learning: Applies to server/**/*.ts : Avoid long-running, synchronous operations; use `async/await` correctly and be mindful of CPU-intensive tasks to prevent blocking the event loop

Applied to files:

  • server/queues/alchemyQueue.ts
📚 Learning: 2025-12-31T00:23:55.034Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 610
File: .changeset/ready-experts-fly.md:1-2
Timestamp: 2025-12-31T00:23:55.034Z
Learning: In the exactly/exa repository, allow and require empty changeset files (containing only --- separators) when changes are not user-facing and do not warrant a version bump. This is needed because CI runs changeset status --since origin/main and requires a changeset file to exist. Ensure such empty changesets are used only for non-user-facing changes and document the rationale in the commit or changelog notes.

Applied to files:

  • .changeset/short-cars-return.md
📚 Learning: 2025-12-30T15:03:28.449Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/style.mdc:0-0
Timestamp: 2025-12-30T15:03:28.449Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use `ts-expect-error` instead of `ts-ignore`; follow it immediately with a single-line lowercase comment explaining why the error is expected, without separators like `-` or `:`

Applied to files:

  • server/utils/createCredential.ts
🧬 Code graph analysis (4)
server/test/queues/alchemyQueue.test.ts (3)
.maestro/maestro.d.ts (1)
  • Response (19-19)
server/queues/constants.ts (1)
  • AlchemyJob (7-9)
server/queues/alchemyQueue.ts (2)
  • AlchemyJobData (15-20)
  • processor (42-69)
server/queues/alchemyQueue.ts (1)
server/queues/constants.ts (2)
  • QueueName (1-3)
  • AlchemyJob (7-9)
server/utils/createCredential.ts (2)
server/queues/alchemyQueue.ts (1)
  • alchemyQueue (34-34)
server/queues/constants.ts (1)
  • AlchemyJob (7-9)
server/test/utils/createCredential.test.ts (4)
.maestro/maestro.d.ts (1)
  • Response (19-19)
server/utils/createCredential.ts (1)
  • createCredential (24-61)
server/queues/alchemyQueue.ts (1)
  • alchemyQueue (34-34)
server/queues/constants.ts (1)
  • AlchemyJob (7-9)
🔇 Additional comments (5)
server/script/openapi.ts (1)

25-26: LGTM!

The REDIS_URL change to a full URL format is more explicit, and adding DISABLE_WORKERS prevents the queue worker from starting during OpenAPI spec generation, which is appropriate for this tooling script.

server/test/utils/createCredential.test.ts (1)

73-81: LGTM!

The exception handling test correctly verifies that queue failures are captured via Sentry's captureException, ensuring observability for background job enqueue failures.

server/queues/constants.ts (1)

1-11: LGTM!

Clean constants definition following TypeScript best practices. The use of as const for literal types and indexed access types for enums ensures type safety throughout the queue system.

server/test/queues/alchemyQueue.test.ts (2)

68-71: LGTM!

The test correctly validates that the processor throws an error for unknown job names, aligning with the processor's default case behavior.


73-89: LGTM!

Excellent addition of the API failure test case. This ensures the processor correctly handles and reports non-ok responses from the Alchemy API, improving resilience and observability.

@aguxez aguxez force-pushed the feature/webhook-queue branch from 5e59af2 to fdc90ad Compare January 7, 2026 18:15
chatgpt-codex-connector[bot]

This comment was marked as resolved.

@greptile-apps
Copy link

greptile-apps bot commented Mar 3, 2026

Additional Comments (3)

server/utils/createCredential.ts, line 56
Credential persisted before webhook readiness is checked

database.insert(credentials) runs at line 44 and commits the row to the database. Only then, at line 56, WebhookNotReadyError is thrown, which causes the caller to return HTTP 503 and invite the client to retry.

On retry the client calls the same endpoint again, which tries to insert the same credential ID a second time. Because the row is already in the database that second insert will hit a unique-constraint violation, returning a 500 instead of the expected success — the user is permanently stuck.

The fix is to validate webhookId before the database insert so the operation is either fully committed or not attempted at all:

  if (!webhookId) throw new WebhookNotReadyError();

  await database.insert(credentials).values([
    {
      account,
      id: credentialId,
      publicKey,
      factory: exaAccountFactoryAddress,
      transports: options?.webauthn?.transports,
      counter: options?.webauthn?.counter,
      source: options?.source,
    },
  ]);

server/queues/alchemyQueue.ts, line 66
Worker initialization silently swallowed — queue can be enqueued but never processed

If new Worker(...) throws (e.g., initial Redis handshake fails), the catch block sets alchemyWorker = undefined and returns early. However, _alchemyQueue is never cleaned up here: subsequent calls to getAlchemyQueue() will happily enqueue ADD_SUBSCRIBER jobs into the queue, but since alchemyWorker is undefined, those jobs will sit in Redis forever and never be processed.

Consider either:

  1. Propagating the error (re-throwing) so the process fails fast on startup instead of silently accumulating dead jobs.
  2. Resetting _alchemyQueue as well in the catch block so any later call to getAlchemyQueue() also fails.
  } catch (error) {
    captureException(error, { level: "error", tags: { queue: QUEUE_NAME, phase: "initialization" } });
    await _alchemyQueue?.close();
    _alchemyQueue = undefined;
    return;
  }

server/queues/alchemyQueue.ts, line 6
Both queue and worker share the same IORedis connection

BullMQ internally requires one connection for pub/sub (blocking commands) and another for regular commands, and it duplicates the provided connection to achieve this. When both the Queue and the Worker are created with the same redis instance, BullMQ ends up calling .duplicate() on it twice, producing two separate connections — that part is fine. However, sharing the same base instance means that when closeRedis() calls redis.quit() it tears down the parent connection, which can race with or interfere with BullMQ's own duplicate-connection lifecycle.

The safest approach (as also mentioned in BullMQ docs) is to pass an IORedis connection options object or a new IORedis instance directly to the Queue / Worker so BullMQ fully owns its connections:

import { Redis as IORedis } from "ioredis";

if (!process.env.REDIS_URL) throw new Error("missing redis url");
const bullmqRedis = new IORedis(process.env.REDIS_URL, { maxRetriesPerRequest: null });
// pass bullmqRedis to Queue / Worker, never call .quit() on it from outside BullMQ

@greptile-apps
Copy link

greptile-apps bot commented Mar 3, 2026

Additional Comments (2)

server/utils/createCredential.ts, line 56
Credential inserted before webhook readiness check

The database.insert(credentials).values([...]) call on line 44 persists the credential record unconditionally, but if (!webhookId) throw new WebhookNotReadyError() isn't checked until line 56. This ordering creates a broken retry loop:

  1. Webhook not ready → credential row is written to DB → WebhookNotReadyError thrown → caller returns 503.
  2. Client retries (per the retriable tag) → database.insert throws a unique-constraint violation because the credential ID already exists → that error is not WebhookNotReadyError → falls through to the generic 500 handler.

The user can never successfully retry because the second attempt always 500s, defeating the entire purpose of the 503 retriable pattern.

The fix is to move the webhook guard above the database insert:

  if (!webhookId) throw new WebhookNotReadyError();

  await database.insert(credentials).values([
    {
      account,
      id: credentialId,
      publicKey,
      factory: exaAccountFactoryAddress,
      transports: options?.webauthn?.transports,
      counter: options?.webauthn?.counter,
      source: options?.source,
    },
  ]);

server/test/utils/createCredential.test.ts, line 91
Test doesn't assert the DB insert is skipped on 503

The test correctly asserts that WebhookNotReadyError is thrown when webhookId is undefined, but it never checks that database.insert was not called. Because of this gap, the existing logic bug (insert runs before the guard) is invisible to the test suite — the mock insert just silently succeeds and the error is thrown afterward.

Adding an assertion like the one below would have caught the ordering issue:

it("should throw WebhookNotReadyError when webhookId is undefined", async () => {
  mocks.webhookId.value = undefined;
  const credentialId = "0x1234567890123456789012345678901234567890";

  await expect(createCredential(mockContext, credentialId)).rejects.toThrow(WebhookNotReadyError);

  // DB must NOT have been written — a real retry would hit a unique-constraint error otherwise
  expect(vi.mocked(database.insert)).not.toHaveBeenCalled();
});

@greptile-apps
Copy link

greptile-apps bot commented Mar 3, 2026

Additional Comments (2)

server/utils/createCredential.ts, line 56
Credential inserted into DB before webhook readiness check

The database.insert(credentials) call (line 44) persists the new credential unconditionally, and only then checks whether webhookId is set (line 56). If the webhook isn't ready, WebhookNotReadyError is thrown, the caller returns a 503, and the client is told to retry — but the credential row already exists in the database.

On retry, the same credentialId is inserted again, which will hit a primary-key / unique-constraint violation and cause the request to fail with an unhandled 500 rather than the expected 503, making the retry impossible. The user is permanently stuck.

The fix is to check webhookId before the DB insert (and before any side-effects):

  // guard before any side effects
  if (!webhookId) throw new WebhookNotReadyError();

  await database.insert(credentials).values([
    {
      account,
      id: credentialId,
      ...
    },
  ]);

server/test/utils/createCredential.test.ts, line 110
Potential flaky assertion on fire-and-forget queue add

In createCredential.ts, the getAlchemyQueue().add(...).catch(...) call is not awaited — it is intentionally fire-and-forget. This means when await createCredential(mockContext, credentialId) returns on line 98, the .catch() microtask for the rejected add may not have executed yet, so captureException might not have been called when the expect on line 100 runs.

In practice this may pass due to V8's microtask ordering, but it is fragile. Consider wrapping the assertion in vi.waitFor to make it deterministic:

await createCredential(mockContext, credentialId);

await vi.waitFor(() => {
  expect(vi.mocked(captureException)).toHaveBeenCalledWith(
    error,
    expect.objectContaining({ ... }),
  );
});

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

await database.insert(credentials).values([

P1 Badge Check webhook readiness before inserting credentials

createCredential persists the credential row before verifying webhookId, so when webhook initialization is still pending the function throws WebhookNotReadyError after writing to credentials. In the registration handlers this returns 503 on the first attempt, but a retry with the same credential now hits a unique-key insert error and falls through the generic 500 path instead of the intended retriable response. Move the readiness guard ahead of the insert (or make the insert idempotent) so retries remain valid while the webhook is unavailable.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@greptile-apps
Copy link

greptile-apps bot commented Mar 4, 2026

Additional Comments (2)

server/test/api/auth.test.ts, line 45
Missing webhookId mock — tests silently depend on ALCHEMY_ACTIVITY_ID

The queue is mocked here, but createCredential now has an early guard:

if (!webhookId) throw new WebhookNotReadyError();

where webhookId comes from ../hooks/activity as process.env.ALCHEMY_ACTIVITY_ID. If that env-var is absent in the test environment, every test that reaches createCredential (i.e. any registration or SIWE flow that creates a new credential) will now receive a 503 instead of the expected 200, silently breaking those tests.

createCredential.test.ts handles this correctly by mocking the module:

vi.mock("../../hooks/activity", () => ({
  get webhookId() { return mocks.webhookId.value; },
}));

The same pattern should be applied here so these tests don't depend on an environment variable being present:

vi.mock("../../queues/alchemyQueue", async (importOriginal) => {
  const actual = await importOriginal<typeof AlchemyQueue>();
  return { ...actual, getAlchemyQueue: vi.fn(() => ({ add: vi.fn().mockResolvedValue({}) })) };
});

vi.mock("../../hooks/activity", () => ({ webhookId: "mock-webhook-id" }));

server/utils/createCredential.ts, line 82
Fire-and-forget enqueue — silent loss of webhook subscriptions on Redis unavailability

The .add() call is intentionally not awaited (async background processing), and the .catch() handler captures to Sentry. However, if Redis is temporarily unavailable when a new credential is created, the add() rejection is silently swallowed and the user's address is never enqueued — meaning their account will never receive Alchemy activity notifications. There is no compensating mechanism (e.g. a dead-letter queue, a retry for the enqueue itself, or a deferred sweep).

This is a design trade-off, and the choice to not block credential creation is reasonable. It's worth being explicit in a comment that this is intentional and that ops must monitor the Sentry extra.job = "add-subscriber" alerts to detect missed enqueues:

// Fire-and-forget: credential creation is not blocked on queue availability.
// Monitor Sentry for `extra.job = "add-subscriber"` errors — a failure here
// means the user's address was never subscribed to the Alchemy webhook.
getAlchemyQueue()
  .add(AlchemyJob.ADD_SUBSCRIBER, { account, webhookId })
  .catch((error: unknown) =>
    captureException(error, {
      level: "error",
      extra: { job: AlchemyJob.ADD_SUBSCRIBER, account, webhookId, credentialId },
    }),
  );

chatgpt-codex-connector[bot]

This comment was marked as resolved.

@greptile-apps
Copy link

greptile-apps bot commented Mar 4, 2026

Additional Comments (2)

server/queues/alchemyQueue.ts, line 97
Promise.all in close() skips cleanup on partial failure

If either alchemyWorker?.close() or _alchemyQueue?.close() rejects, Promise.all short-circuits and the alchemyWorker = undefined; _alchemyQueue = undefined; cleanup lines never execute. This leaves stale references pointing to already-closing (or failed) instances, meaning any subsequent call to close() (or initializeWorker()) will observe the old references and behave incorrectly.

Using Promise.allSettled ensures cleanup always runs regardless of individual failures:

export async function close() {
  await Promise.allSettled([alchemyWorker?.close() ?? Promise.resolve(), _alchemyQueue?.close() ?? Promise.resolve()]);
  alchemyWorker = undefined;
  _alchemyQueue = undefined;
}

server/queues/alchemyQueue.ts, line 2
Inconsistent Sentry import sources

SPAN_STATUS_ERROR is imported from @sentry/core while all other Sentry utilities (addBreadcrumb, captureException, startSpan, Span) are imported from @sentry/node. Since @sentry/node re-exports everything from @sentry/core, importing SPAN_STATUS_ERROR from @sentry/node would keep all Sentry imports consistent:

import { addBreadcrumb, captureException, SPAN_STATUS_ERROR, startSpan, type Span } from "@sentry/node";

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

chatgpt-codex-connector[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 19 additional findings in Devin Review.

Open in Devin Review

Comment on lines +75 to +82
getAlchemyQueue()
.add(AlchemyJob.ADD_SUBSCRIBER, { account, webhookId })
.catch((error: unknown) =>
captureException(error, {
level: "error",
extra: { job: AlchemyJob.ADD_SUBSCRIBER, account, webhookId, credentialId },
}),
);
Copy link

@devin-ai-integration devin-ai-integration bot Mar 6, 2026

Choose a reason for hiding this comment

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

🚩 Credential is inserted into DB before webhook subscription is queued

The credential is persisted to the database at line 109 before the webhook subscription job is enqueued at line 138. If the server crashes between these two operations, the credential exists in the DB but no webhook subscription job was ever queued. This means the account won't receive activity notifications until manually remediated. The old code had the same gap (fire-and-forget .catch()), so this isn't a regression, but with a job queue it could be improved by making the job enqueue part of the main flow rather than fire-and-forget.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

sentry[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5344ea539d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

credentialId: C,
options?: { source?: string; webauthn?: WebAuthnCredential },
) {
if (!webhookId) throw new WebhookNotReadyError();

Choose a reason for hiding this comment

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

P1 Badge Add recovery path when webhook id is missing

This unconditional WebhookNotReadyError turns a transient startup failure into a persistent outage for new credential creation: webhookId is initialized once in hooks/activity via a single findWebhook(...).then(...).catch(...) chain, and the catch path only logs/captures without retrying or re-initializing. If that initial Alchemy call fails once (e.g., brief network error), webhookId remains undefined for the life of the process, so every new registration/SIWE-first-login keeps returning 503 until a restart.

Useful? React with 👍 / 👎.

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 16 additional findings in Devin Review.

Open in Devin Review

Comment on lines +325 to 329
.then(async (results) => {
await closeRedis();
if (error) reject(error);
else if (results.some((result) => result.status === "rejected")) reject(new Error("closing services failed"));
else resolve(null);

Choose a reason for hiding this comment

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

🟡 closeRedis() failure in shutdown masks other close errors and server error

In the close() function, await closeRedis() at line 326 is called inside the .then() callback before the error-checking logic at lines 327-329. If closeRedis() throws, the rejection propagates to .catch(reject), completely bypassing the checks for the server.close error and allSettled results. This means: (1) if the server had a close error, it is swallowed; (2) if any service in allSettled (Sentry, Segment, DB, credential queue) failed, those failures are silently masked by the Redis error. closeRedis() should be wrapped in try/catch or included in allSettled to avoid masking other shutdown failures.

(Refers to lines 325-330)

Prompt for agents
In server/index.ts, lines 325-330, the `await closeRedis()` call should be wrapped in a try/catch so that its failure does not bypass the checks for the server.close error and allSettled results. One approach:

  .then(async (results) => {
    const redisResult = await closeRedis().then(() => ({ status: 'fulfilled' as const, value: null })).catch((e) => ({ status: 'rejected' as const, reason: e }));
    if (error) reject(error);
    else if (results.some((result) => result.status === 'rejected') || redisResult.status === 'rejected') reject(new Error('closing services failed'));
    else resolve(null);
  })

Alternatively, restructure so that closeRedis() is part of a second allSettled round after closeCredentialQueue completes, or wrap it in try/catch and merge the result with the allSettled results.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 633264c817

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

credentialId: C,
options?: { source?: string; webauthn?: WebAuthnCredential },
) {
if (!webhookId) throw new WebhookNotReadyError();

Choose a reason for hiding this comment

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

P1 Badge Retry webhook initialization before rejecting credential creation

createCredential now hard-fails with WebhookNotReadyError whenever webhookId is unset, but webhookId is only assigned by the one-time startup promise in hooks/activity.ts (which just logs/captures on failure and does not retry). If Alchemy is temporarily unavailable during boot, this guard makes all new SIWE/WebAuthn credential creations return 503 for the lifetime of the process, so the advertised “please retry” path never recovers without a restart.

Useful? React with 👍 / 👎.

});

export async function closeQueue() {
await Promise.all([worker.close(), queue.close()]);

Choose a reason for hiding this comment

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

P2 Badge Wait for both queue resources to close on shutdown

Using Promise.all here short-circuits on the first rejection, so if either worker.close() or queue.close() fails, closeQueue() rejects before awaiting the other close path. In server/index.ts, shutdown then proceeds to closeRedis(), which can race with the still-closing BullMQ resource and produce flaky shutdown errors/leftover handles when one close operation is already failing.

Useful? React with 👍 / 👎.

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 15 additional findings in Devin Review.

Open in Devin Review

Comment on lines +29 to +92
export const CredentialJob = { CREATE: "create" } as const;

export type CredentialJobData = { account: string; webhookId: string };

const queue = new Queue(queueName, { connection: redisConnection });

export async function processor(job: Job<CredentialJobData>) {
return startSpan(
{ name: "credential.processor", op: "queue.process", attributes: { job: job.name, ...job.data } },
async (span: Span) => {
switch (job.name) {
case CredentialJob.CREATE: {
const { account, webhookId: webhook } = job.data;
const response = await fetch("https://dashboard.alchemy.com/api/update-webhook-addresses", {
method: "PATCH",
headers,
body: JSON.stringify({ webhook_id: webhook, addresses_to_add: [account], addresses_to_remove: [] }),
});
if (!response.ok) {
const text = await response.text();
span.setStatus({ code: SPAN_STATUS_ERROR, message: text });
throw new Error(`${response.status} ${text}`);
}
break;
}
default: {
const message = `Unknown job name: ${job.name}`;
span.setStatus({ code: SPAN_STATUS_ERROR, message });
throw new Error(message);
}
}
},
);
}

const worker = new Worker(queueName, processor, {
connection: redisConnection,
limiter: { max: 10, duration: 1000 },
});

worker
.on("failed", (job: Job<CredentialJobData> | undefined, error: Error) => {
captureException(error, { level: "error", extra: { job: job?.data } });
})
.on("completed", (job: Job<CredentialJobData>) => {
addBreadcrumb({ category: "queue", message: `Job ${job.id} completed`, level: "info", data: { job: job.data } });
})
.on("active", (job: Job<CredentialJobData>) => {
addBreadcrumb({ category: "queue", message: `Job ${job.id} active`, level: "info", data: { job: job.data } });
})
.on("error", (error: Error) => {
captureException(error, { level: "error", tags: { queue: queueName } });
});

export async function closeQueue() {
await Promise.all([worker.close(), queue.close()]);
}

export class WebhookNotReadyError extends Error {
constructor() {
super("alchemy webhook not initialized yet, retry credential creation");
this.name = "WebhookNotReadyError";
}
}

Choose a reason for hiding this comment

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

🔴 Default export placed after ~60 lines of supporting code, violating AGENTS.md file ordering rule

AGENTS.md mandates: "the default export — the thing the file exists for — goes first. standalone function declarations only exist because they were extracted for reuse or genuine complexity — they are supporting details and belong at the bottom alongside internal constants and types." In server/utils/createCredential.ts, the default export createCredential is at line 94, but ~60 lines of queue infrastructure (queueName, CredentialJob, CredentialJobData, queue, processor, worker event handlers, closeQueue, WebhookNotReadyError) are placed before it (lines 27–92). Since createCredential is a function declaration (hoisted), it can safely be placed first — its body only executes at call time, when all module-level const/class bindings are initialized.

Prompt for agents
In server/utils/createCredential.ts, move the default export function `createCredential` (currently at lines 94–147) to immediately after the imports (around line 27). Then place all the supporting declarations below it in order of relevance: `queueName` const, `CredentialJob` const, `CredentialJobData` type, `queue` const, `processor` function, `worker` const with event handlers, `closeQueue` function, and `WebhookNotReadyError` class. This follows the AGENTS.md rule that the default export goes first and supporting details go at the bottom.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f520abb1fe

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

credentialId: C,
options?: { source?: string; webauthn?: WebAuthnCredential },
) {
if (!webhookId) throw new WebhookNotReadyError();

Choose a reason for hiding this comment

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

P1 Badge Add retry path before rejecting credential creation

createCredential now hard-fails with WebhookNotReadyError whenever webhookId is unset, but webhookId is initialized by a one-shot async call in hooks/activity.ts that only logs on failure and does not retry. If Alchemy webhook discovery/creation fails once during startup (for example, transient network/API failure), webhookId remains undefined for the life of the process and every new registration/SIWE-first-login returns 503 until a restart, effectively causing a prolonged signup outage from a transient boot error.

Useful? React with 👍 / 👎.

}).catch((error: unknown) => captureException(error, { level: "error" })),
]);

queue.add(CredentialJob.CREATE, { account, webhookId }).catch((error: unknown) =>

Choose a reason for hiding this comment

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

P2 Badge Configure queue job cleanup to avoid Redis growth

Jobs are added without any cleanup options (removeOnComplete / removeOnFail) and the queue has no default job options, so completed and failed credential jobs are retained in Redis indefinitely. Since this path runs on each new credential, Redis keyspace and memory usage will grow monotonically in production, eventually hurting queue performance and operational stability.

Useful? React with 👍 / 👎.

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.

server: handle the case where subscribing to an Alchemy Webhook for an address update fails

3 participants