Skip to content

feat: limit free users to 1 phone API key#894

Merged
AchoArnold merged 1 commit into
mainfrom
feat/phone-api-key-entitlement-limit
May 17, 2026
Merged

feat: limit free users to 1 phone API key#894
AchoArnold merged 1 commit into
mainfrom
feat/phone-api-key-entitlement-limit

Conversation

@AchoArnold
Copy link
Copy Markdown
Member

Summary

Add entitlement check to the phone API key creation endpoint so free users can only create 1 phone API key. Paid users remain unlimited. Self-hosted setups are unaffected (controlled by \ENTITLEMENT_ENABLED\ env var).

Changes

Reuses the existing \EntitlementService\ pattern from send schedules:

  • *\�ntitlement_service.go* — Add \PhoneAPIKey\ to \�ntityLimits\ map (free: 1)
  • *\phone_api_key_repository.go* — Add \CountByUser\ to interface
  • *\gorm_phone_api_key_repository.go* — Implement \CountByUser\ (GORM count query)
  • *\phone_api_key_service.go* — Add \CountByUser\ pass-through method
  • *\phone_api_key_handler.go* — Inject \EntitlementService, check entitlement before creating, add 402 Swagger annotation
  • *\container.go* — Wire \EntitlementService\ into \PhoneAPIKeyHandler\

Behavior

User Plan Limit Over Limit Response
Free 1 HTTP 402 with upgrade message
Paid Unlimited N/A
Self-hosted (\ENTITLEMENT_ENABLED=false) Unlimited N/A

No frontend changes needed — the existing error toast already displays the API response message.

Add entitlement check to the phone API key creation endpoint so free
users can only create 1 phone API key. Paid users remain unlimited.
Self-hosted setups are unaffected (controlled by ENTITLEMENT_ENABLED).

Reuses the existing EntitlementService pattern from send schedules:
- Add PhoneAPIKey to entityLimits map (free: 1)
- Add CountByUser to PhoneAPIKeyRepository and service
- Inject EntitlementService into PhoneAPIKeyHandler
- Check entitlement before creating, return 402 if exceeded

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@codacy-production
Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 0 complexity · 3 duplication

Metric Results
Complexity 0
Duplication 3

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 17, 2026

Greptile Summary

This PR gates phone API key creation behind the existing EntitlementService, limiting free users to one key while paid users and self-hosted instances remain unrestricted. The implementation correctly follows the MessageSendSchedule entitlement pattern end-to-end (repository → service → handler → DI wire-up).

  • Adds CountByUser to the phone API key repository, service, and interface, then injects EntitlementService into PhoneAPIKeyHandler with a pre-creation check that returns HTTP 402 when the limit is exceeded.
  • Introduces \"PhoneAPIKey\" as the entity name key in entityLimits, but formatEntityName splits on every uppercase character individually — consecutive letters A, P, I become three separate single-character words, so the user-facing error message reads \"phone a p i keys\" instead of \"phone api keys\"; renaming the key to \"PhoneApiKey\" in both files resolves this.

Confidence Score: 3/5

The entitlement limit is wired correctly end-to-end, but the user-facing error message will be garbled for all free users who hit the cap, a visible regression that surfaces immediately in production.

The core limit enforcement logic is sound, but the entity name "PhoneAPIKey" interacts badly with formatEntityName's character-by-character uppercase split, producing a clearly broken error string for every user who exceeds the free-tier cap. The fix is small (rename the key to "PhoneApiKey" in two places), but without it the feature ships with broken UX on its most visible user-facing output.

api/pkg/services/entitlement_service.go and api/pkg/handlers/phone_api_key_handler.go both need the entity name corrected to "PhoneApiKey".

Important Files Changed

Filename Overview
api/pkg/services/entitlement_service.go Adds "PhoneAPIKey" to entityLimits with a free-tier cap of 1; the consecutive uppercase letters in the key cause formatEntityName to produce a broken error message ("phone a p i keys") — should be "PhoneApiKey".
api/pkg/handlers/phone_api_key_handler.go Injects EntitlementService and adds a pre-creation entitlement check; uses the same "PhoneAPIKey" key that produces a malformed user-facing error message.
api/pkg/repositories/gorm_phone_api_key_repository.go Adds CountByUser with a standard GORM count query filtered by user_id; correct and consistent with the existing repository pattern.
api/pkg/repositories/phone_api_key_repository.go Adds CountByUser to the interface definition; straightforward and consistent.
api/pkg/services/phone_api_key_service.go Adds a thin CountByUser pass-through to the repository with proper tracing; no issues.
api/pkg/di/container.go Wires EntitlementService into PhoneAPIKeyHandler; minimal and correct.

Reviews (1): Last reviewed commit: "feat: limit free users to 1 phone API ke..." | Re-trigger Greptile

Comment on lines +22 to +24
"PhoneAPIKey": {
entities.SubscriptionNameFree: 1,
},
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.

P1 The entity name "PhoneAPIKey" contains consecutive uppercase letters, which breaks formatEntityName. The function splits on every uppercase character boundary individually, so "PhoneAPIKey" becomes the words ["phone", "a", "p", "i", "key"], producing the error message: "Upgrade to a paid plan to create more than [1] phone a p i keys." — which is clearly unreadable. Using "PhoneApiKey" splits correctly into ["phone", "api", "key"]"phone api keys".

Suggested change
"PhoneAPIKey": {
entities.SubscriptionNameFree: 1,
},
"PhoneApiKey": {
entities.SubscriptionNameFree: 1,
},


userID := h.userIDFomContext(c)

result, err := h.entitlementService.Check(ctx, userID, "PhoneAPIKey", func() (int, error) {
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.

P1 The entity name must match the key in entityLimits. If the key is renamed to "PhoneApiKey" in entitlement_service.go (to fix the malformed error message), this lookup must be updated too — otherwise the entitlement check always returns Allowed: true because the key isn't found in the map.

Suggested change
result, err := h.entitlementService.Check(ctx, userID, "PhoneAPIKey", func() (int, error) {
result, err := h.entitlementService.Check(ctx, userID, "PhoneApiKey", func() (int, error) {

@AchoArnold AchoArnold merged commit 16d7d46 into main May 17, 2026
10 checks passed
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