Skip to content

Add Discord approval gate for Luma event drafts#19

Merged
andrelandgraf merged 6 commits intomainfrom
codex/discord-review-approval-flow
Feb 23, 2026
Merged

Add Discord approval gate for Luma event drafts#19
andrelandgraf merged 6 commits intomainfrom
codex/discord-review-approval-flow

Conversation

@andrelandgraf
Copy link
Contributor

@andrelandgraf andrelandgraf commented Feb 23, 2026

Summary

  • create event_review_sessions table to track Discord review state per imported Luma event
  • import new Luma events as drafts (isDraft = true)
  • post each new draft to a Discord review thread
  • poll pending threads and set event live only after explicit Approved reply
  • stop polling approved threads by marking review session status as approved

Verification

  • bun run typecheck
  • bun run build
  • bun run fmt
  • bun run db:generate

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Discord integration for event review workflows
    • Events can now be reviewed through dedicated Discord threads
    • Automatic approval tracking from Discord reviews
    • Events transition from draft to live upon approval
  • Chores

    • Added new database schema for review session management
    • Added Discord configuration support (bot token and channel settings)

@vercel
Copy link

vercel bot commented Feb 23, 2026

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

Project Deployment Actions Updated (UTC)
allthingsweb Ready Ready Preview, Comment Feb 23, 2026 1:19am

Request Review

@coderabbitai
Copy link

coderabbitai bot commented Feb 23, 2026

Warning

Rate limit exceeded

@andrelandgraf has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 24 minutes and 53 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.

📝 Walkthrough

Walkthrough

This PR introduces Discord-based event review workflow integration to Luma Sync. It adds database infrastructure for tracking review sessions, Discord API integration for creating and monitoring review threads, workflow automation for polling approvals, and configuration support for Discord bot authentication.

Changes

Cohort / File(s) Summary
Database Migrations
app/migrations/0012_thin_morlun.sql, app/migrations/meta/0012_snapshot.json, app/migrations/meta/_journal.json
New event_review_sessions table created with id, eventId (unique, FK to events), provider, channel/thread/message IDs, status, and timestamps. Includes cascade delete on event removal and journal entry documenting migration.
Configuration & Secrets
app/src/lib/config.ts, app/src/lib/integrations/config.ts
Added Discord configuration section with botToken and reviewChannelId environment variables exposed through config schema.
Schema Definition
app/src/lib/schema.ts
New eventReviewSessionsTable defined with relationships and constraints; exported types InsertEventReviewSession and SelectEventReviewSession added.
Discord Integration
app/src/workflows/luma-sync/steps/discord.ts
New Discord review workflow module with functions for creating review threads, posting draft messages, polling for approvals, and handling Snowflake ID comparisons; includes Discord API wrapper and approval detection logic.
Event Review Session Management
app/src/workflows/luma-sync/steps/events.ts
New functions for upserting review sessions, listing pending reviews, updating cursor positions, and triggering live-flag updates on explicit approval; extended createEventFromDraft return projection.
Workflow Orchestration
app/src/workflows/luma-sync/index.ts
Enhanced Luma Sync workflow to create Discord review threads after event draft creation, poll pending sessions for approvals, and trigger live-flag workflow on approval; hardcoded isDraft to true and refactored error scoping.
Type System
app/src/workflows/luma-sync/types.ts
Updated LumaSyncCreatedEvent with review-related fields (startDate, endDate, tagline, attendeeLimit, isDraft); restructured LumaSyncError with scope/reference pattern; added approvedCount to LumaSyncResult.

Sequence Diagram(s)

sequenceDiagram
    participant WF as Luma Sync Workflow
    participant Discord as Discord API
    participant DB as Database
    participant Poll as Approval Poller

    WF->>WF: Create event draft
    WF->>Discord: POST root message to review channel
    Discord-->>WF: channelId, rootMessageId
    WF->>Discord: POST thread with draft details
    Discord-->>WF: threadId, lastSeenMessageId
    WF->>DB: Create review session (pending)
    DB-->>WF: sessionId

    Note over Poll: Post-processing loop

    Poll->>DB: List pending sessions (limit 100)
    DB-->>Poll: pendingSessions[]
    
    loop For each pending session
        Poll->>Discord: List thread messages after lastSeenMessageId
        Discord-->>Poll: messages[]
        Poll->>Poll: Scan for explicit approval
        
        alt Approval found
            Poll->>DB: Get event isDraft status
            Poll->>DB: Update isDraft = false
            Poll->>DB: Update session status = approved
            Poll->>WF: Increment approvedCount
        else New messages detected
            Poll->>DB: Update session cursor
        end
    end

    WF-->>WF: Return with approvedCount
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 A Discord thread springs forth with care,
Review sessions bloom in the database's lair,
Messages poll and approvals appear—
Events go live when consensus rings clear! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and accurately summarizes the main objective: adding a Discord approval mechanism for Luma event drafts, which matches the core functionality implemented across the changeset.

✏️ 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 codex/discord-review-approval-flow

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

❤️ Share

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

Copy link

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

🧹 Nitpick comments (6)
app/src/workflows/luma-sync/steps/events.ts (1)

185-186: Naming inconsistency: set_live_after_explicit_approval uses snake_case.

Every other function in this file uses camelCase (createEventFromDraft, resolveUniqueSlug, etc.). The snake_case name with a separate camelCase alias on Line 216 is unnecessary indirection — just name the function setLiveAfterExplicitApproval directly.

♻️ Proposed fix
-export async function set_live_after_explicit_approval({
+export async function setLiveAfterExplicitApproval({
   reviewSessionId,
   eventId,
   approvalMessageId,
...
 }
-
-export const setLiveAfterExplicitApproval = set_live_after_explicit_approval;

Also applies to: 216-216

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

In `@app/src/workflows/luma-sync/steps/events.ts` around lines 185 - 186, The
function is misnamed in snake_case; rename set_live_after_explicit_approval to
camelCase setLiveAfterExplicitApproval and remove the separate camelCase alias
used later (setLiveAfterExplicitApproval) to avoid indirection; update any
exports/uses in this module that reference set_live_after_explicit_approval to
reference setLiveAfterExplicitApproval (check the function declaration and the
alias at or around the existing setLiveAfterExplicitApproval reference) so
naming is consistent with other functions like createEventFromDraft and
resolveUniqueSlug.
app/src/lib/schema.ts (1)

197-214: Consider constraining status (and optionally provider) at the database level.

Both status and provider are plain text columns that accept any string, but the workflow only expects specific values ("pending" / "approved" for status, "discord" for provider). A pgEnum or a CHECK constraint would prevent invalid data from being written at the DB level, consistent with how hackathonStateEnum and profileTypeEnum are used elsewhere in this schema.

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

In `@app/src/lib/schema.ts` around lines 197 - 214, The event_review_sessions
table uses free-form text for provider and status; constrain them at the DB
level by introducing an enum or CHECK constraint and applying it in
eventReviewSessionsTable: create a pgEnum (e.g., eventReviewStatusEnum with
"pending" and "approved") and use that for the status column instead of
text("status"), and optionally create a provider enum (or CHECK) limited to
"discord" and use that for provider; update the references in
eventReviewSessionsTable (status and provider) so the schema enforces allowed
values.
app/src/workflows/luma-sync/steps/discord.ts (3)

254-307: Minor: after sorting ascending, latestSeenMessageId is always messages[messages.length - 1].id.

The loop to track latestSeenMessageId is correct but redundant with the sort. After messages.sort(...), you could just assign latestSeenMessageId = messages[messages.length - 1].id and simplify the loop to only scan for approvals. Not worth a change if you prefer the explicit approach.

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

In `@app/src/workflows/luma-sync/steps/discord.ts` around lines 254 - 307, In
pollDiscordThreadForApproval, after messages.sort((left, right) =>
compareSnowflakeIds(left.id, right.id)) set latestSeenMessageId =
messages[messages.length - 1].id (or null when messages empty) instead of
updating it inside the loop, and then simplify the for (const message of
messages) loop to only check non-bot messages for isExplicitApprovalMessage and
set approvalMessageId; keep the existing return shape (latestSeenMessageId,
approvalMessageId) and preserve error handling around listThreadMessages.

91-93: Redundant new Date() wrapping.

value is already a Date; wrapping it in new Date(value) just clones it. You can call value.toISOString() directly.

♻️ Proposed fix
 function toIsoString(value: Date): string {
-  return new Date(value).toISOString();
+  return value.toISOString();
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/workflows/luma-sync/steps/discord.ts` around lines 91 - 93, The
toIsoString function wraps an existing Date in new Date(value) unnecessarily;
update the toIsoString function to call value.toISOString() directly (keep the
function name toIsoString and parameter value) to avoid cloning the Date object
and return the ISO string.

133-164: No timeout or rate-limit handling on Discord API calls.

Discord enforces per-route rate limits and returns 429 with a Retry-After header. Currently, a 429 is treated as a generic failure. For a polling workflow that runs repeatedly, consider at minimum:

  • Adding an AbortSignal.timeout() (or equivalent) to the fetch call to avoid hanging indefinitely.
  • Detecting 429 responses and either respecting Retry-After or surfacing a distinct error so the caller can back off.

Not blocking, but worth considering for production robustness.

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

In `@app/src/workflows/luma-sync/steps/discord.ts` around lines 133 - 164, In
discordRequest, add request timeout and explicit 429 handling: create an
AbortController (or use AbortSignal.timeout) and pass its signal into fetch to
avoid hangs, and after receiving the response check for status 429; if 429, read
the Retry-After header (or parse response body) and either sleep for that many
seconds before retrying or throw a distinct rate-limit error (e.g.,
DiscordRateLimitError) that includes the retry-after value so callers can back
off; keep the existing error payload parsing for other non-ok statuses.
app/src/workflows/luma-sync/index.ts (1)

240-280: Sequential polling of up to 100 sessions — adequate for now, but note scalability.

Each pending session is polled one-by-one with a Discord API call. At 100 sessions, and assuming ~200-500ms per API call, this loop could take 20-50 seconds. This is fine for a background workflow, but as the number of pending reviews grows:

  1. The hardcoded limit of 100 means sessions beyond that threshold are silently skipped each cycle.
  2. Sequential execution amplifies latency linearly.

For now this is acceptable, but consider batching or parallelizing (with rate-limit awareness) if the volume grows.

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

In `@app/src/workflows/luma-sync/index.ts` around lines 240 - 280, The loop polls
up to 100 sessions sequentially causing latency and silently skipping >100
items; change listPendingDiscordReviewSessions to accept a configurable/pageable
limit or implement pagination, and process sessions in parallel with controlled
concurrency (e.g., p-map or a Promise pool) when calling
pollDiscordThreadForApproval, set_live_after_explicit_approval, and
updateDiscordReviewSessionCursor so you retain per-session error handling and
cursor updates; ensure you throttle/handle Discord rate limits and keep the
errors.push logic intact for failed tasks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/workflows/luma-sync/index.ts`:
- Around line 210-229: When Discord thread/session creation fails in the
try/catch around createDiscordReviewThreadForEvent and
createDiscordReviewSession for createdEvent, mark the created event so it won't
be silently orphaned: update the event record (or call a helper like
enqueueReviewRetry) to set a "needsReviewRetry" or "reviewRetryAt" field and
persist it, and/or push the event ID into a retry queue/topic for the next sync;
also record the failure detail in errors as you already do. Ensure changes touch
the same code paths that persist events (referencing createdEvent,
createDiscordReviewThreadForEvent, createDiscordReviewSession) so subsequent
sync runs can detect and retry creating the review session.

In `@app/src/workflows/luma-sync/steps/discord.ts`:
- Around line 67-76: The compareSnowflakeIds function currently falls back to
localeCompare on BigInt parse failure which can mis-order numeric strings;
update the catch to do a numeric-aware fallback: if both inputs match /^\d+$/
then compare by length (shorter = smaller) and only use localeCompare if lengths
equal, otherwise throw an error (include the invalid inputs) so callers are
alerted to truly non-numeric snowflake values; reference compareSnowflakeIds to
locate and change the catch behavior.
- Around line 147-149: The code parses Discord's response with
JSON.parse(responseText) which can throw if the body is non-empty but not valid
JSON; wrap the parse in a try/catch (or use a guarded parse helper) around
JSON.parse so that on parse failure you set responseData to null (or include the
raw responseText for diagnostics) and do not let a SyntaxError mask the HTTP
status; update the logic around responseText/responseData in this module
(discord.ts) so any thrown parse errors are caught and the code continues to
handle non-2xx statuses using response.status and the safe responseData value.

In `@app/src/workflows/luma-sync/steps/events.ts`:
- Around line 185-214: Replace the two independent updates in
set_live_after_explicit_approval with a single db.batch call that sends both
UPDATE queries together: build the eventsTable update (setting isDraft=false and
returning id for eventId) and the eventReviewSessionsTable update (setting
status="approved", approvalMessageId and lastSeenMessageId) and pass both
queries into db.batch([...]) so they are sent in one request; then extract the
first batch result to determine updatedEvent truthiness to return, using the
existing symbols eventId, reviewSessionId, approvalMessageId, eventsTable,
eventReviewSessionsTable and db.batch instead of two separate await
db.update(...) calls.

---

Nitpick comments:
In `@app/src/lib/schema.ts`:
- Around line 197-214: The event_review_sessions table uses free-form text for
provider and status; constrain them at the DB level by introducing an enum or
CHECK constraint and applying it in eventReviewSessionsTable: create a pgEnum
(e.g., eventReviewStatusEnum with "pending" and "approved") and use that for the
status column instead of text("status"), and optionally create a provider enum
(or CHECK) limited to "discord" and use that for provider; update the references
in eventReviewSessionsTable (status and provider) so the schema enforces allowed
values.

In `@app/src/workflows/luma-sync/index.ts`:
- Around line 240-280: The loop polls up to 100 sessions sequentially causing
latency and silently skipping >100 items; change
listPendingDiscordReviewSessions to accept a configurable/pageable limit or
implement pagination, and process sessions in parallel with controlled
concurrency (e.g., p-map or a Promise pool) when calling
pollDiscordThreadForApproval, set_live_after_explicit_approval, and
updateDiscordReviewSessionCursor so you retain per-session error handling and
cursor updates; ensure you throttle/handle Discord rate limits and keep the
errors.push logic intact for failed tasks.

In `@app/src/workflows/luma-sync/steps/discord.ts`:
- Around line 254-307: In pollDiscordThreadForApproval, after
messages.sort((left, right) => compareSnowflakeIds(left.id, right.id)) set
latestSeenMessageId = messages[messages.length - 1].id (or null when messages
empty) instead of updating it inside the loop, and then simplify the for (const
message of messages) loop to only check non-bot messages for
isExplicitApprovalMessage and set approvalMessageId; keep the existing return
shape (latestSeenMessageId, approvalMessageId) and preserve error handling
around listThreadMessages.
- Around line 91-93: The toIsoString function wraps an existing Date in new
Date(value) unnecessarily; update the toIsoString function to call
value.toISOString() directly (keep the function name toIsoString and parameter
value) to avoid cloning the Date object and return the ISO string.
- Around line 133-164: In discordRequest, add request timeout and explicit 429
handling: create an AbortController (or use AbortSignal.timeout) and pass its
signal into fetch to avoid hangs, and after receiving the response check for
status 429; if 429, read the Retry-After header (or parse response body) and
either sleep for that many seconds before retrying or throw a distinct
rate-limit error (e.g., DiscordRateLimitError) that includes the retry-after
value so callers can back off; keep the existing error payload parsing for other
non-ok statuses.

In `@app/src/workflows/luma-sync/steps/events.ts`:
- Around line 185-186: The function is misnamed in snake_case; rename
set_live_after_explicit_approval to camelCase setLiveAfterExplicitApproval and
remove the separate camelCase alias used later (setLiveAfterExplicitApproval) to
avoid indirection; update any exports/uses in this module that reference
set_live_after_explicit_approval to reference setLiveAfterExplicitApproval
(check the function declaration and the alias at or around the existing
setLiveAfterExplicitApproval reference) so naming is consistent with other
functions like createEventFromDraft and resolveUniqueSlug.

Comment on lines +210 to +229

try {
const reviewThread =
await createDiscordReviewThreadForEvent(createdEvent);
await createDiscordReviewSession({
eventId: createdEvent.id,
channelId: reviewThread.channelId,
rootMessageId: reviewThread.rootMessageId,
threadId: reviewThread.threadId,
lastSeenMessageId: reviewThread.lastSeenMessageId,
});
} catch (reviewSetupError) {
errors.push({
scope: "review",
reference: createdEvent.id,
error: `Failed to create Discord review thread: ${toErrorMessage(
reviewSetupError,
)}`,
});
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Discord review setup failure is non-blocking — good resilience.

If the Discord thread or session creation fails, the event is still created as a draft and the error is logged. The event won't have a review session though, which means it'll stay as a draft indefinitely with no way to approve it through Discord polling.

Consider whether an orphaned draft (no review session) should be surfaced more prominently or retried on next sync, so it doesn't silently remain in draft forever.

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

In `@app/src/workflows/luma-sync/index.ts` around lines 210 - 229, When Discord
thread/session creation fails in the try/catch around
createDiscordReviewThreadForEvent and createDiscordReviewSession for
createdEvent, mark the created event so it won't be silently orphaned: update
the event record (or call a helper like enqueueReviewRetry) to set a
"needsReviewRetry" or "reviewRetryAt" field and persist it, and/or push the
event ID into a retry queue/topic for the next sync; also record the failure
detail in errors as you already do. Ensure changes touch the same code paths
that persist events (referencing createdEvent,
createDiscordReviewThreadForEvent, createDiscordReviewSession) so subsequent
sync runs can detect and retry creating the review session.

Comment on lines +67 to +76
function compareSnowflakeIds(a: string, b: string): number {
try {
const aValue = BigInt(a);
const bValue = BigInt(b);
if (aValue === bValue) return 0;
return aValue > bValue ? 1 : -1;
} catch {
return a.localeCompare(b);
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

localeCompare fallback can mis-order numeric snowflake strings.

If BigInt() parsing fails (e.g., non-numeric input), localeCompare does lexicographic comparison which gives wrong results for numeric strings of differing lengths (e.g., "9" > "10" lexicographically). Consider throwing or padding instead, though in practice Discord snowflake IDs should always be valid numeric strings, so the risk is low.

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

In `@app/src/workflows/luma-sync/steps/discord.ts` around lines 67 - 76, The
compareSnowflakeIds function currently falls back to localeCompare on BigInt
parse failure which can mis-order numeric strings; update the catch to do a
numeric-aware fallback: if both inputs match /^\d+$/ then compare by length
(shorter = smaller) and only use localeCompare if lengths equal, otherwise throw
an error (include the invalid inputs) so callers are alerted to truly
non-numeric snowflake values; reference compareSnowflakeIds to locate and change
the catch behavior.

Comment on lines +147 to +149
const responseText = await response.text();
const responseData =
responseText.length > 0 ? JSON.parse(responseText) : null;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

JSON.parse can throw on non-empty, non-JSON response bodies.

If Discord returns a non-empty body that isn't valid JSON (e.g., an HTML error page from a proxy/CDN), JSON.parse(responseText) will throw an unhandled SyntaxError, masking the actual HTTP error status.

🛡️ Proposed fix
   const responseText = await response.text();
-  const responseData =
-    responseText.length > 0 ? JSON.parse(responseText) : null;
+  let responseData: unknown = null;
+  if (responseText.length > 0) {
+    try {
+      responseData = JSON.parse(responseText);
+    } catch {
+      if (!response.ok) {
+        throw new Error(
+          `Discord API request failed (${response.status}): ${responseText.slice(0, 200)}`,
+        );
+      }
+    }
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/workflows/luma-sync/steps/discord.ts` around lines 147 - 149, The
code parses Discord's response with JSON.parse(responseText) which can throw if
the body is non-empty but not valid JSON; wrap the parse in a try/catch (or use
a guarded parse helper) around JSON.parse so that on parse failure you set
responseData to null (or include the raw responseText for diagnostics) and do
not let a SyntaxError mask the HTTP status; update the logic around
responseText/responseData in this module (discord.ts) so any thrown parse errors
are caught and the code continues to handle non-2xx statuses using
response.status and the safe responseData value.

Comment on lines +185 to +214
export async function set_live_after_explicit_approval({
reviewSessionId,
eventId,
approvalMessageId,
}: {
reviewSessionId: string;
eventId: string;
approvalMessageId: string;
}): Promise<boolean> {
"use step";

const [updatedEvent] = await db
.update(eventsTable)
.set({
isDraft: false,
})
.where(and(eq(eventsTable.id, eventId), eq(eventsTable.isDraft, true)))
.returning({ id: eventsTable.id });

await db
.update(eventReviewSessionsTable)
.set({
status: "approved",
approvalMessageId,
lastSeenMessageId: approvalMessageId,
})
.where(eq(eventReviewSessionsTable.id, reviewSessionId));

return Boolean(updatedEvent);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd 'db.ts' --type f

Repository: allthingsweb-dev/allthingsweb

Length of output: 90


🏁 Script executed:

cat -n app/src/lib/db.ts

Repository: allthingsweb-dev/allthingsweb

Length of output: 351


🏁 Script executed:

rg 'db\.transaction' --type ts --type tsx

Repository: allthingsweb-dev/allthingsweb

Length of output: 100


🏁 Script executed:

rg 'db\.transaction' --type-list | head -20

Repository: allthingsweb-dev/allthingsweb

Length of output: 462


🏁 Script executed:

rg 'db\.transaction'

Repository: allthingsweb-dev/allthingsweb

Length of output: 55


🏁 Script executed:

rg 'await db\.' --max-count=20

Repository: allthingsweb-dev/allthingsweb

Length of output: 3242


🏁 Script executed:

cat app/package.json | grep -A 5 '"drizzle'

Repository: allthingsweb-dev/allthingsweb

Length of output: 652


🏁 Script executed:

cat -n app/src/lib/tx-utils.ts

Repository: allthingsweb-dev/allthingsweb

Length of output: 1238


🏁 Script executed:

rg 'try.*catch|BEGIN|ROLLBACK' app/src/workflows/luma-sync/steps/events.ts

Repository: allthingsweb-dev/allthingsweb

Length of output: 55


🏁 Script executed:

cat -n app/src/workflows/luma-sync/steps/events.ts | head -220

Repository: allthingsweb-dev/allthingsweb

Length of output: 7104


🌐 Web query:

drizzle-orm neon-http transaction support db.transaction

💡 Result:

drizzle-orm/neon-http does not support interactive transactions, so calling db.transaction(...) will always fail with:

Drizzle’s Neon docs explicitly note that HTTP is best for single / non-interactive work, and if you need session/interactive transaction support you should use a WebSocket/pg-compatible approach instead. (orm.drizzle.team)

What to do instead

1) If you need real db.transaction(...)

Use the WebSocket/pg-compatible Neon serverless driver via Drizzle’s neon-serverless adapter (typically with Pool / Client):

import { Pool } from "@neondatabase/serverless";
import { drizzle } from "drizzle-orm/neon-serverless";

const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export const db = drizzle({ client: pool });

(Example pattern shown in practice here.) (stackoverflow.com)

2) If you only need “non-interactive transactions” (send a batch as one unit)

Neon’s HTTP driver supports non-interactive transactions via sql.transaction([...]) (array of statements executed together). (planetscale.com)

In Drizzle, the closest equivalent is db.batch([...]), which Drizzle documents as supported for Neon. (orm.drizzle.team)

Note: db.batch(...) is not the same API/semantics as db.transaction(async (tx) => ...)—it’s a “send multiple statements together” style, not an interactive transaction with a transactional tx client. (orm.drizzle.team)

Citations:


🏁 Script executed:

rg 'db\.batch' || echo "No db.batch usage found"

Repository: allthingsweb-dev/allthingsweb

Length of output: 96


Non-atomic dual update — event and review session can diverge on partial failure.

set_live_after_explicit_approval performs two independent UPDATE statements. If the first succeeds (setting isDraft = false) but the second fails (marking the session "approved"), the event goes live while the session remains "pending", causing the poller to keep trying to approve an already-live event.

The project uses drizzle-orm/neon-http, which does not support db.transaction(). Use db.batch() to send both updates together as a batch:

 export async function set_live_after_explicit_approval({
   reviewSessionId,
   eventId,
   approvalMessageId,
 }: {
   reviewSessionId: string;
   eventId: string;
   approvalMessageId: string;
 }): Promise<boolean> {
   "use step";
 
-  const [updatedEvent] = await db
-    .update(eventsTable)
-    .set({
-      isDraft: false,
-    })
-    .where(and(eq(eventsTable.id, eventId), eq(eventsTable.isDraft, true)))
-    .returning({ id: eventsTable.id });
-
-  await db
-    .update(eventReviewSessionsTable)
-    .set({
-      status: "approved",
-      approvalMessageId,
-      lastSeenMessageId: approvalMessageId,
-    })
-    .where(eq(eventReviewSessionsTable.id, reviewSessionId));
-
-  return Boolean(updatedEvent);
+  const results = await db.batch([
+    db
+      .update(eventsTable)
+      .set({
+        isDraft: false,
+      })
+      .where(and(eq(eventsTable.id, eventId), eq(eventsTable.isDraft, true)))
+      .returning({ id: eventsTable.id }),
+    db
+      .update(eventReviewSessionsTable)
+      .set({
+        status: "approved",
+        approvalMessageId,
+        lastSeenMessageId: approvalMessageId,
+      })
+      .where(eq(eventReviewSessionsTable.id, reviewSessionId)),
+  ]);
+
+  const [updatedEvent] = results[0] as Array<{ id: string }>;
+  return Boolean(updatedEvent);
 }

Note: db.batch() sends all queries together in a single request, reducing the window for divergence, but does not provide full ACID transaction guarantees on HTTP. For true transactional safety, consider switching to neon-serverless driver.

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

In `@app/src/workflows/luma-sync/steps/events.ts` around lines 185 - 214, Replace
the two independent updates in set_live_after_explicit_approval with a single
db.batch call that sends both UPDATE queries together: build the eventsTable
update (setting isDraft=false and returning id for eventId) and the
eventReviewSessionsTable update (setting status="approved", approvalMessageId
and lastSeenMessageId) and pass both queries into db.batch([...]) so they are
sent in one request; then extract the first batch result to determine
updatedEvent truthiness to return, using the existing symbols eventId,
reviewSessionId, approvalMessageId, eventsTable, eventReviewSessionsTable and
db.batch instead of two separate await db.update(...) calls.

@andrelandgraf andrelandgraf merged commit 2e05073 into main Feb 23, 2026
3 of 4 checks passed
@andrelandgraf andrelandgraf deleted the codex/discord-review-approval-flow branch February 23, 2026 01:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant