Add broadcast limits per competition with entitlements#372
Add broadcast limits per competition with entitlements#372zacjones93 wants to merge 1 commit intomainfrom
Conversation
Adds a `broadcasts_per_competition` limit to the entitlement system to control how many broadcasts organizers can send per competition. Free: 5, Pro: 25, Enterprise: unlimited (-1). - Add BROADCASTS_PER_COMPETITION constant to config/limits.ts - Add limit and plan_limits rows to seed data - Enforce limit in sendBroadcastFn before creating broadcast - Expose broadcastLimit/broadcastCount/broadcastsRemaining from listBroadcastsFn - Show usage counter and disable compose at limit in organizer UI - Add 6 unit tests for limit enforcement and usage reporting - Update lat.md documentation https://claude.ai/code/session_01Jay68PSA5VranJWQkbQZ6r
WalkthroughIntroduces a per-competition broadcast limit feature across plan tiers. Adds the Changes
Sequence DiagramsequenceDiagram
participant Client as Client / UI
participant Route as Broadcasts Route
participant ServerFn as Server Functions
participant DB as Database
participant Limit as Limit Service
Client->>Route: Load broadcasts page
Route->>ServerFn: listBroadcastsFn()
ServerFn->>DB: Query broadcasts for competition
ServerFn->>Limit: getTeamLimit("broadcasts_per_competition")
Limit-->>ServerFn: broadcastLimit (5/25/-1)
ServerFn->>DB: Count SENT broadcasts
ServerFn-->>Route: {broadcasts, broadcastLimit, broadcastCount, broadcastsRemaining}
Route-->>Client: Render page with quota status
Client->>Route: Submit new broadcast
Route->>ServerFn: sendBroadcastFn()
ServerFn->>Limit: getTeamLimit("broadcasts_per_competition")
Limit-->>ServerFn: broadcastLimit
ServerFn->>DB: Count current SENT broadcasts
alt At Quota
ServerFn-->>Client: Error: "Broadcast limit reached"
Client-->>Client: Disable compose, show warning
else Under Quota
ServerFn->>DB: Create broadcast record
ServerFn-->>Client: Success
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
🚀 Preview DeployedURL: https://wodsmith-app-pr-372.zacjones93.workers.dev
This comment is automatically updated on each push to this PR. |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
apps/wodsmith-start/test/server-fns/broadcast-fns.test.ts (1)
90-99: Missing@lat:reference comment for test specification.Per coding guidelines, test files should include
//@lat: [[section-id]]comments to tie tests to their specifications. Consider adding a reference to the relevant section inlat.md/organizer-dashboard.md#Broadcastsor a test spec section.♻️ Suggested addition
describe("Broadcast Limit Enforcement", () => { + // `@lat`: [[organizer-dashboard#Broadcasts]] beforeEach(() => { vi.clearAllMocks()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/wodsmith-start/test/server-fns/broadcast-fns.test.ts` around lines 90 - 99, Add the required `// `@lat`: [[section-id]]` reference comment to this test file to link it to the spec; place the comment near the top of the "Broadcast Limit Enforcement" describe block (or at file header) so it clearly references the relevant spec section (e.g., lat.md/organizer-dashboard.md#Broadcasts) and ensure it appears before the describe("Broadcast Limit Enforcement", ...) declaration so test-suite tooling can pick up the mapping.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/wodsmith-start/src/server-fns/broadcast-fns.ts`:
- Around line 211-233: The current broadcast quota check using getTeamLimit and
counting rows from competitionBroadcastsTable with BROADCAST_STATUS.SENT has a
TOCTOU race: the count is read (db.select(...).from(...).where(...)) well before
the new broadcast insert, so concurrent requests can both pass and exceed the
limit; fix by performing the count check and the insert within a single database
transaction (use your DB client's transaction block) and lock the relevant
competition row or rows (e.g., SELECT ... FOR UPDATE on the
competition/organizing team or the competitionBroadcastsTable aggregate) before
re-checking the count, or alternatively enforce the limit at the database level
with a constraint/unique/index so the insert will fail if the limit would be
exceeded; update the code around getTeamLimit, the count query, and the
broadcast insert to use the transactional/locking approach or handle a DB
constraint error and surface a clear quota-exceeded error.
---
Nitpick comments:
In `@apps/wodsmith-start/test/server-fns/broadcast-fns.test.ts`:
- Around line 90-99: Add the required `// `@lat`: [[section-id]]` reference
comment to this test file to link it to the spec; place the comment near the top
of the "Broadcast Limit Enforcement" describe block (or at file header) so it
clearly references the relevant spec section (e.g.,
lat.md/organizer-dashboard.md#Broadcasts) and ensure it appears before the
describe("Broadcast Limit Enforcement", ...) declaration so test-suite tooling
can pick up the mapping.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: e18eca20-4002-46e6-8cf5-04bdf4821b02
📒 Files selected for processing (8)
apps/wodsmith-start/scripts/seed/seeders/02-billing.tsapps/wodsmith-start/scripts/seed/seeders/06-team-entitlements.tsapps/wodsmith-start/src/config/limits.tsapps/wodsmith-start/src/routes/compete/organizer/$competitionId/broadcasts.tsxapps/wodsmith-start/src/server-fns/broadcast-fns.tsapps/wodsmith-start/test/server-fns/broadcast-fns.test.tslat.md/domain.mdlat.md/organizer-dashboard.md
| // Check broadcast limit for this competition | ||
| const broadcastLimit = await getTeamLimit( | ||
| competition.organizingTeamId, | ||
| LIMITS.BROADCASTS_PER_COMPETITION, | ||
| ) | ||
|
|
||
| if (broadcastLimit !== -1) { | ||
| const [{ count: currentCount }] = await db | ||
| .select({ count: count() }) | ||
| .from(competitionBroadcastsTable) | ||
| .where( | ||
| and( | ||
| eq(competitionBroadcastsTable.competitionId, data.competitionId), | ||
| eq(competitionBroadcastsTable.status, BROADCAST_STATUS.SENT), | ||
| ), | ||
| ) | ||
|
|
||
| if (Number(currentCount) >= broadcastLimit) { | ||
| throw new Error( | ||
| `Broadcast limit reached (${broadcastLimit} per competition). Upgrade your plan to send more broadcasts.`, | ||
| ) | ||
| } | ||
| } |
There was a problem hiding this comment.
Potential race condition in limit enforcement.
There's a TOCTOU (time-of-check-time-of-use) gap between checking the broadcast count (Lines 218-226) and inserting the new broadcast (Line 362). Two concurrent requests could both pass the limit check and exceed the quota.
Given this is a low-volume organizer action, the risk is minimal—worst case is exceeding the limit by 1-2 broadcasts. If stricter enforcement is needed in the future, consider moving the count check and insert into a single transaction with a SELECT ... FOR UPDATE or a database-level constraint.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/wodsmith-start/src/server-fns/broadcast-fns.ts` around lines 211 - 233,
The current broadcast quota check using getTeamLimit and counting rows from
competitionBroadcastsTable with BROADCAST_STATUS.SENT has a TOCTOU race: the
count is read (db.select(...).from(...).where(...)) well before the new
broadcast insert, so concurrent requests can both pass and exceed the limit; fix
by performing the count check and the insert within a single database
transaction (use your DB client's transaction block) and lock the relevant
competition row or rows (e.g., SELECT ... FOR UPDATE on the
competition/organizing team or the competitionBroadcastsTable aggregate) before
re-checking the count, or alternatively enforce the limit at the database level
with a constraint/unique/index so the insert will fail if the limit would be
exceeded; update the code around getTeamLimit, the count query, and the
broadcast insert to use the transactional/locking approach or handle a DB
constraint error and surface a clear quota-exceeded error.
There was a problem hiding this comment.
1 issue found across 8 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/wodsmith-start/src/server-fns/broadcast-fns.ts">
<violation number="1" location="apps/wodsmith-start/src/server-fns/broadcast-fns.ts:217">
P1: The broadcast-limit enforcement is race-prone: concurrent sends can bypass the cap because count-check and insert are not atomic.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| LIMITS.BROADCASTS_PER_COMPETITION, | ||
| ) | ||
|
|
||
| if (broadcastLimit !== -1) { |
There was a problem hiding this comment.
P1: The broadcast-limit enforcement is race-prone: concurrent sends can bypass the cap because count-check and insert are not atomic.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/wodsmith-start/src/server-fns/broadcast-fns.ts, line 217:
<comment>The broadcast-limit enforcement is race-prone: concurrent sends can bypass the cap because count-check and insert are not atomic.</comment>
<file context>
@@ -6,808 +6,758 @@
+ LIMITS.BROADCASTS_PER_COMPETITION,
+ )
+
+ if (broadcastLimit !== -1) {
+ const [{ count: currentCount }] = await db
+ .select({ count: count() })
</file context>
Summary
Implements broadcast rate limiting per competition based on team entitlements. Organizers can now send a configurable number of broadcasts per competition, with the ability to upgrade for unlimited broadcasts.
Key Changes
Broadcast limit enforcement: Added validation in
sendBroadcastFnto check against team entitlements before allowing a broadcast to be sent. Throws an error if the limit is reached.Limit tracking in UI: Updated the broadcasts page to display:
Entitlements integration:
BROADCASTS_PER_COMPETITIONlimit key to the limits configgetTeamLimit()function to fetch team's broadcast allowanceList broadcasts enhancement:
listBroadcastsFnnow returns:broadcastLimit: The team's limit for this competitionbroadcastCount: Number of sent broadcastsbroadcastsRemaining: Calculated remaining broadcasts (null if unlimited)Code formatting: Standardized indentation and import ordering throughout the broadcast functions and page component for consistency.
Test coverage: Added comprehensive test suite for broadcast limit enforcement scenarios.
Implementation Details
-1represent unlimited broadcasts (no restriction)https://claude.ai/code/session_01Jay68PSA5VranJWQkbQZ6r
Summary by cubic
Adds per-competition broadcast limits tied to team entitlements. Organizers see usage in the UI and cannot send once the limit is reached; plans can upgrade for more or unlimited sends.
New Features
broadcasts_per_competitioninsendBroadcastFnbefore creating a broadcast; only sent broadcasts count;-1means unlimited.broadcastLimit,broadcastCount, andbroadcastsRemainingfromlistBroadcastsFn; the organizer page shows usage, warns at limit, and disables “New Broadcast.”Migration
02-billing.tsand06-team-entitlements.ts.getTeamLimitreturnsbroadcasts_per_competition.Written for commit 3f30831. Summary will update on new commits.
Summary by CodeRabbit
New Features
Documentation