The HardCap* constants stop "you have 5000 campaigns on this org"; the
throttles in this commit stop "you created 1000 campaigns today on a
fresh unlimited account." Different shape, different abuse, different
mechanism — Redis-backed per-(scope, resource, UTC-day) counters that
reset by key design at midnight UTC, no scheduled job needed.
New service internal/app/dailythrottle:
- CheckAndIncrement(scope, resource, ceiling) atomically bumps the
counter and returns errx.TooManyRequests when the post-increment
value exceeds the ceiling.
- 25h TTL so the key always expires after the day rolls over even
if the process restarts before midnight.
- Fail-open when the cache is absent (jobs/tests) so creation paths
that haven't been wired with a cache still work.
Caps (config.DailyThrottleNew*):
- 20 new campaigns/org/day
- 5 new mailboxes/org/day
- 3 new workspaces/owner/day
Wired into three creation paths:
- campaign.Create — scoped on the orgID when present
- email.OAuthFinish + email.OnboardSMTPIMAP — scoped on the orgID;
fires only at actual create, not OAuthStart, so retrying a failed
OAuth flow doesn't burn the day's budget.
- organization.Create — scoped on the owner uuid (the org doesn't
exist yet)
Adds errx.TooManyRequests (HTTP 429) since no caller had one before.
emailService gains WireThrottle alongside the existing WireWebhooks
pattern so jobs / tests can build the service without a cache. Same
treatment in main.go.