Skip to content

Add broadcast limits per competition with entitlements#372

Open
zacjones93 wants to merge 1 commit intomainfrom
claude/organizer-broadcast-limits-hxnPx
Open

Add broadcast limits per competition with entitlements#372
zacjones93 wants to merge 1 commit intomainfrom
claude/organizer-broadcast-limits-hxnPx

Conversation

@zacjones93
Copy link
Copy Markdown
Contributor

@zacjones93 zacjones93 commented Apr 3, 2026

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 sendBroadcastFn to 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:

    • Current broadcast count vs. limit (e.g., "2 of 5 used")
    • Warning banner when limit is reached
    • Disabled "New Broadcast" button when at capacity
  • Entitlements integration:

    • Added BROADCASTS_PER_COMPETITION limit key to the limits config
    • Integrated getTeamLimit() function to fetch team's broadcast allowance
    • Seeded default limits for different plan tiers (free, pro, etc.)
  • List broadcasts enhancement: listBroadcastsFn now returns:

    • broadcastLimit: The team's limit for this competition
    • broadcastCount: Number of sent broadcasts
    • broadcastsRemaining: 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

  • Limits of -1 represent unlimited broadcasts (no restriction)
  • Only "sent" broadcasts count toward the limit, not drafts
  • The limit check happens before recipient selection to fail fast
  • UI gracefully handles both limited and unlimited scenarios

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

    • Enforce broadcasts_per_competition in sendBroadcastFn before creating a broadcast; only sent broadcasts count; -1 means unlimited.
    • Expose broadcastLimit, broadcastCount, and broadcastsRemaining from listBroadcastsFn; the organizer page shows usage, warns at limit, and disables “New Broadcast.”
    • Add limit key and seed defaults (Free: 5, Pro: 25, Enterprise: unlimited) plus team entitlement seeds.
    • Add unit tests for limit enforcement and usage reporting; update docs.
  • Migration

    • Run seeders to add the new limit and plan values: 02-billing.ts and 06-team-entitlements.ts.
    • Deploy with entitlements enabled so getTeamLimit returns broadcasts_per_competition.

Written for commit 3f30831. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Broadcast limits now enforced per competition by plan tier (Free: 5, Pro: 25, Enterprise: unlimited).
    • Organizer broadcasts page displays remaining broadcast quota.
    • "New Broadcast" button disables when quota reached with upgrade prompt.
  • Documentation

    • Updated broadcast documentation to reflect per-competition limit enforcement.

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
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 3, 2026

Walkthrough

Introduces a per-competition broadcast limit feature across plan tiers. Adds the broadcasts_per_competition limit to seed data with tier-specific values (Free: 5, Pro: 25, Enterprise: unlimited), updates configuration, enforces limits in server functions, adds UI status feedback and quota warnings, and includes comprehensive tests and documentation updates.

Changes

Cohort / File(s) Summary
Seed Data
apps/wodsmith-start/scripts/seed/seeders/02-billing.ts, apps/wodsmith-start/scripts/seed/seeders/06-team-entitlements.ts
Added broadcasts_per_competition limit definition with tier-specific values (5/25/-1) and team-level entitlement overrides.
Broadcast Limit Configuration
apps/wodsmith-start/src/config/limits.ts
Added BROADCASTS_PER_COMPETITION constant to LIMITS object, extending the LimitKey type.
Broadcast Server Functions
apps/wodsmith-start/src/server-fns/broadcast-fns.ts
Extended listBroadcastsFn to return limit metadata (broadcastLimit, broadcastCount, broadcastsRemaining); added quota enforcement in sendBroadcastFn to reject broadcasts when limit is reached.
Broadcast Tests
apps/wodsmith-start/test/server-fns/broadcast-fns.test.ts
New comprehensive test suite validating limit enforcement in sendBroadcastFn (rejection at quota) and response shapes in listBroadcastsFn (finite vs. unlimited limit handling).
Organizer Broadcasts UI
apps/wodsmith-start/src/routes/compete/organizer/$competitionId/broadcasts.tsx
Extended loader to return limit metadata; updated UI to display usage, disable "New Broadcast" action at quota, and show limit-reached warning card.
Documentation
lat.md/domain.md, lat.md/organizer-dashboard.md
Updated broadcast domain and organizer dashboard documentation to describe per-competition limit enforcement and tier-based quota display.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐰 A limit now guards each broadcast cheer,
Per-plan quotas keep the counts quite clear,
Five for the free, twenty-five for the pro,
Endless for enterprise—watch them flow! ✨
With tests to verify and UI so bright,
The rabbit hops cheering this feature's just right!

🚥 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 concisely summarizes the main change: adding broadcast limits per competition with entitlements. The title directly reflects the primary objective of the changeset.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/organizer-broadcast-limits-hxnPx

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.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 3, 2026

🚀 Preview Deployed

URL: https://wodsmith-app-pr-372.zacjones93.workers.dev

Detail Value
Commit cfffdc9
Stage pr-372
Deployed 2026-04-03T01:25:53.670Z

This comment is automatically updated on each push to this PR.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
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 in lat.md/organizer-dashboard.md#Broadcasts or 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

📥 Commits

Reviewing files that changed from the base of the PR and between 9b68173 and 3f30831.

📒 Files selected for processing (8)
  • apps/wodsmith-start/scripts/seed/seeders/02-billing.ts
  • apps/wodsmith-start/scripts/seed/seeders/06-team-entitlements.ts
  • apps/wodsmith-start/src/config/limits.ts
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/broadcasts.tsx
  • apps/wodsmith-start/src/server-fns/broadcast-fns.ts
  • apps/wodsmith-start/test/server-fns/broadcast-fns.test.ts
  • lat.md/domain.md
  • lat.md/organizer-dashboard.md

Comment on lines +211 to +233
// 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.`,
)
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

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) {
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 3, 2026

Choose a reason for hiding this comment

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

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>
Fix with Cubic

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants