Skip to content

Add BullMQ background jobs and Swagger OpenAPI docs for backend#54

Merged
fuzziecoder merged 2 commits intomainfrom
codex/implement-api-documentation-with-swagger
Feb 25, 2026
Merged

Add BullMQ background jobs and Swagger OpenAPI docs for backend#54
fuzziecoder merged 2 commits intomainfrom
codex/implement-api-documentation-with-swagger

Conversation

@fuzziecoder
Copy link
Owner

@fuzziecoder fuzziecoder commented Feb 25, 2026

Motivation

  • Provide background processing for emails, scheduled reminders, and periodic cleanup using BullMQ/Redis to support event automation and maintenance.
  • Expose API documentation so the backend surface is discoverable and testable via Swagger UI/OpenAPI.
  • Add DB helpers to support job-driven features (reminder window lookup and expired-spot cleanup) and make env handling resilient in environments without dev deps.

Description

  • Added a job system backend/jobs.js that scaffolds BullMQ queues/workers for email notifications, spot reminders, and recurring cleanup, and which falls back to a no-op implementation when BullMQ/ioredis are not available.
  • Integrated the job system into backend/server.js to initialize jobs on startup, enqueue an email notification after order creation, expose admin endpoints to trigger reminders/cleanup (POST /api/jobs/reminders/run and POST /api/jobs/cleanup/run), include job status in GET /api/health, and add graceful shutdown hooks.
  • Added OpenAPI builder and Swagger HTML in backend/openapi.js and exposed docs endpoints at GET /api/docs/openapi.json and GET /api/docs.
  • Extended backend/db.js with getSpotsBetween for reminder-window lookups and cleanupExpiredSpots which removes expired spots and related orders/order items, and updated backend/env.js and .env.example to include Redis/job and other environment settings.

Testing

  • Ran syntax checks for modified modules with node --check backend/server.js backend/jobs.js backend/openapi.js backend/db.js backend/env.js, which passed.
  • Started the server and validated endpoints with curl: GET /api/health returned a health payload including jobs.enabled, GET /api/docs/openapi.json returned the OpenAPI JSON, and GET /api/docs served the Swagger HTML; these checks succeeded when run against a live instance.
  • Attempted to install runtime packages (npm install bullmq ioredis swagger-ui-dist dotenv zod) but the install was blocked by the environment (registry returned 403 Forbidden), so the job system ran in degraded mode with jobs.enabled: false as expected.
  • All changes were committed; automated checks and the runtime validations described above completed successfully (job behavior is intentionally no-op when deps are missing).

Codex Task


Open with Devin

@vercel
Copy link

vercel bot commented Feb 25, 2026

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

Project Deployment Actions Updated (UTC)
brocode-spot-update-app Ready Ready Preview, Comment Feb 25, 2026 2:07pm

@fuzziecoder fuzziecoder self-assigned this Feb 25, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 25, 2026

Warning

Rate limit exceeded

@fuzziecoder has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 7 minutes and 7 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 3eea30a and d64b1d9.

📒 Files selected for processing (7)
  • .env.example
  • backend/README.md
  • backend/db.js
  • backend/env.js
  • backend/jobs.js
  • backend/openapi.js
  • backend/server.js
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/implement-api-documentation-with-swagger

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

@fuzziecoder fuzziecoder merged commit f5a9e41 into main Feb 25, 2026
3 of 4 checks passed
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 4 potential issues.

View 9 additional findings in Devin Review.

Open in Devin Review

}

if (method === 'POST' && path === '/api/jobs/reminders/run') {
const authedUser = getUserFromAuthHeader(req.headers.authorization);

Choose a reason for hiding this comment

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

🔴 Missing await on getUserFromAuthHeader makes job endpoints always return 403 Forbidden

At lines 403 and 417, getUserFromAuthHeader (an async function defined at backend/server.js:93) is called without await. This means authedUser is assigned a Promise object, which is always truthy. Since a Promise has no .role property, authedUser.role evaluates to undefined, so the check authedUser.role !== 'admin' is always true, and both endpoints unconditionally return 403 Forbidden.

Root Cause and Impact

Every other auth-protected endpoint in the file uses await getUserFromAuthHeader(...) (e.g., backend/server.js:269, backend/server.js:292, backend/server.js:317). These two new job endpoints omit the await.

Because a Promise is truthy and Promise.role === undefined !== 'admin', the guard condition:

if (!authedUser || authedUser.role !== 'admin') {
  sendJson(res, 403, { error: 'Forbidden' });
  return;
}

always triggers, making POST /api/jobs/reminders/run and POST /api/jobs/cleanup/run completely inaccessible — even for legitimate admin users.

Suggested change
const authedUser = getUserFromAuthHeader(req.headers.authorization);
const authedUser = await getUserFromAuthHeader(req.headers.authorization);
Open in Devin Review

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

}

if (method === 'POST' && path === '/api/jobs/cleanup/run') {
const authedUser = getUserFromAuthHeader(req.headers.authorization);

Choose a reason for hiding this comment

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

🔴 Missing await on getUserFromAuthHeader in cleanup endpoint always returns 403

Same issue as the reminders endpoint: getUserFromAuthHeader is called without await at line 417, so the cleanup endpoint is also permanently inaccessible.

Root Cause

getUserFromAuthHeader is async (backend/server.js:93), so calling it without await returns a Promise. The auth guard always rejects because Promise.role is undefined.

Suggested change
const authedUser = getUserFromAuthHeader(req.headers.authorization);
const authedUser = await getUserFromAuthHeader(req.headers.authorization);
Open in Devin Review

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

}

await jobSystem.enqueueExpiredSpotCleanup();
sendJson(res, 202, { accepted: true, jobsEnabled: jobSystem.enabled });

Choose a reason for hiding this comment

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

🔴 Missing return and closing } in cleanup handler causes subsequent routes to be unreachable

After sendJson(res, 202, ...) at line 424, the /api/jobs/cleanup/run handler is missing both a return; statement and the closing } brace for its if block. The next if statement (for /api/presence/heartbeat at line 425) is nested inside the cleanup block instead of being a sibling.

Detailed Explanation

The code at lines 416-425 reads:

if (method === 'POST' && path === '/api/jobs/cleanup/run') {
    ...
    sendJson(res, 202, { accepted: true, jobsEnabled: jobSystem.enabled });
  if (method === 'POST' && path === '/api/presence/heartbeat') {

The if block for cleanup is never closed. This means:

  1. For POST /api/jobs/cleanup/run requests: After sending the 202 response, execution falls through into the presence/heartbeat code, which will attempt to send a second response on the same res object, causing a "headers already sent" error.
  2. For all other requests: The presence heartbeat, presence active, events state PUT/POST, and events state GET routes (lines 425-493) are nested inside the cleanup if block. Since the outer condition method === 'POST' && path === '/api/jobs/cleanup/run' is false for those requests, all those routes become completely unreachable — they will all fall through to the 404 handler.

Impact: The presence and event-state endpoints are broken for all users.

Suggested change
sendJson(res, 202, { accepted: true, jobsEnabled: jobSystem.enabled });
sendJson(res, 202, { accepted: true, jobsEnabled: jobSystem.enabled });
return;
}
Open in Devin Review

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

process.on('SIGTERM', async () => {
await jobSystem.shutdown();
process.exit(0);
console.log(`Cache mode: ${cache.mode}`);

Choose a reason for hiding this comment

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

🟡 console.log for cache mode placed after process.exit(0) in SIGTERM handler — unreachable and misplaced

At line 512, console.log(\Cache mode: ${cache.mode}`)appears afterprocess.exit(0)` at line 511 inside the SIGTERM handler. This line is unreachable.

Root Cause

This line was originally part of the server.listen callback (visible in the LEFT side of the diff at line 442). During the PR changes, it was accidentally moved into the SIGTERM handler after process.exit(0). As a result:

  1. The cache mode is never logged at startup (it was removed from the server.listen callback at lines 498-502).
  2. The line is dead code in the SIGTERM handler since process.exit(0) terminates the process immediately.
Prompt for agents
In backend/server.js, line 512 (`console.log(\`Cache mode: ${cache.mode}\`);`) is unreachable because it comes after `process.exit(0)` at line 511 inside the SIGTERM handler. This log statement was originally in the `server.listen` callback and should be moved back there. Remove line 512 from the SIGTERM handler and add it to the `server.listen` callback at line 501 (after the Swagger docs log line), so it reads:

server.listen(port, () => {
  console.log(`Backend API running on http://localhost:${port}`);
  console.log(`Using local database at: ${dbPath}`);
  console.log(`Swagger docs available at http://localhost:${port}/api/docs`);
  console.log(`Cache mode: ${cache.mode}`);
});
Open in Devin Review

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

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant