Skip to content

feat(subscriptions): bring subscriptions to the free tier#59580

Closed
vdekrijger wants to merge 5 commits into
PostHog:masterfrom
vdekrijger:worktree-subscriptions-freemium
Closed

feat(subscriptions): bring subscriptions to the free tier#59580
vdekrijger wants to merge 5 commits into
PostHog:masterfrom
vdekrijger:worktree-subscriptions-freemium

Conversation

@vdekrijger
Copy link
Copy Markdown
Contributor

Problem

Insight & dashboard subscriptions were a hard paid feature — free-tier orgs were blocked entirely (a PremiumFeaturePermission gate on the viewsets and a full-page PayGateMini on the UI). That's a missed adoption/stickiness opportunity: free users can't experience the feature before deciding to upgrade.

Changes

Make subscriptions freemium, mirroring how alerts already work: free orgs get a small allowance, then hit the existing paywall.

  • Free tier gets 5 subscriptions (team-wide, non-deleted), the limit read live from the alert free-tier constant (AlertConfiguration.ALERTS_ALLOWED_ON_FREE_TIER) so the two never drift.
  • Paid tier is unlimited (the SUBSCRIPTIONS entitlement carries no numeric limit → unlimited), unchanged.
  • Viewing / editing / disabling / deleting existing subscriptions is never gated — even at/over the limit. The paywall appears only when a free user at the limit tries to add an additional subscription.

Backend:

  • Subscription.check_subscription_limit() — a near-exact mirror of AlertConfiguration.check_alert_limit (reads the billing entitlement limit, falls back to the free-tier constant). Counts deleted=False rows.
  • Enforced in SubscriptionSerializer.validate() on POST only (edits via PATCH are never blocked).
  • Removed PremiumFeaturePermission from SubscriptionViewSet and SubscriptionDeliveryViewSet so free orgs can list/read.

Frontend:

  • subscriptionCountLogic — lazily loads the team-wide count via the generated API (limit=1, reads count).
  • The paywall collapses to a single chokepoint: EditSubscription create mode (id === 'new'). A FreeTierCreateGate shows the form while under the limit and the existing PayGateMini upsell at/over it (isFreeTierCreateAtLimit). Fail-open while the count is loading — the backend is the hard guarantee.
  • Removed the three wholesale PayGateMini wrappers (list scene, single scene, subscribe modal) and unhid the insight side-panel "Subscription" menu item for free users.

No DB migration, no schema/serializer-field changes (so no generated-type churn).

Screenshots (Storybook, real components)

Free, under limit Free, at limit
New Subscription form renders PayGateMini upsell ("Insight & dashboard subscriptions — Upgrade to use this feature", View plans / Learn more)

(Editing an existing subscription and managing the list render normally for free orgs — no wholesale paywall.)

How did you test this code?

This PR was authored by an agent (see Agent context). No manual click-testing is claimed — below are the automated tests actually run, all green:

  • Backend integration (ee/api/test/test_subscription.py, posthog/models/test/test_subscription_model.py) — real requests through the full Django stack against Postgres: free org creates 5 → 6th returns 400 under the subscription key; PATCH at the limit returns 200; soft-delete frees a slot; paid org unlimited; free orgs can list/read; check_subscription_limit parameterized over free/paid/numeric-limit/zero/soft-deleted. 152 passed.
  • Frontend unit (jest, src/lib/components/Subscriptions, src/scenes/subscriptions) — isFreeTierCreateAtLimit boundary cases, count-loader (count field + limit=1 invariant + fallback), gate wiring. 63 passed.
  • Live browser e2e (playwright/e2e/subscriptions-freemium.spec.ts) — drove the real frontend against a running stack:
Running 2 tests using 1 worker
  ✓ subscriptions freemium gate › free org under the limit can open the create form
  ✓ subscriptions freemium gate › free org at the limit sees the upgrade paywall instead of the create form
  2 passed (29.0s)

The e2e mocks the org entitlement + team subscription count (the gate is a pure frontend decision: !hasSubscriptionsFeature && count >= FREE_LIMIT); the hard backend limit is covered by the backend tests.

Publish to changelog?

Yes — "Subscriptions are now available on the free tier (up to 5, then upgrade for unlimited)."

Docs update

Subscriptions docs may want a note that the free tier includes up to 5 subscriptions.

🤖 Agent context

Authored with Claude Code via a proof-driven-dev workflow (spec → testable criteria matrix → plan → TDD task-by-task with per-task spec + code-quality review → verification report + rerunnable scripts/verify-subscriptions-free-tier.sh). Hardened with a local multi-reviewer pass; the one substantive finding (a tautological frontend constant test) was fixed by extracting the gate decision into the tested isFreeTierCreateAtLimit predicate.

Key decisions:

  • Mirror alerts rather than invent a new mechanism. Alerts already implement "N free, then paywall"; subscriptions now reuse that exact shape (constant fallback + entitlement limit), including reading the alert free-tier constant live so the two stay in lockstep. No billing-service change.
  • Gate the create entry, not the whole UI. Edits/views must stay open even at the limit, so the paywall moved from the wholesale wrappers to EditSubscription create mode only — which also covers every create entry point in one place.
  • Fail-open on the frontend. If the count is still loading, the form shows; the backend POST check is the authoritative limit.

Agent-authored; requires human review. Verified via the automated tests listed above (including a live browser e2e run against a local stack).

@vdekrijger vdekrijger force-pushed the worktree-subscriptions-freemium branch 3 times, most recently from 47287b3 to 0c25637 Compare May 22, 2026 09:30
…synced to alerts)

Free orgs get 5 subscriptions (synced live to the alert free-tier limit),
then the existing paywall on the next create; viewing/editing/deleting
existing subscriptions is never gated; paid orgs stay unlimited.

- Subscription.check_subscription_limit mirrors AlertConfiguration.check_alert_limit
- POST-only enforcement in SubscriptionSerializer.validate
- remove PremiumFeaturePermission from both subscription viewsets
- frontend: subscriptionCountLogic + FreeTierCreateGate in EditSubscription
- remove wholesale PayGateMini wrappers; unhide insight menu item
…te gate

The free-tier create gate rendered the generic PayGateMini 'Upgrade to use
this feature', implying subscriptions are paid-only. They are freemium with a
5-subscription cap, so swap in UsageLimitPaywall with 'Subscription limit
reached' framing showing the limit and current usage. Update the e2e to anchor
on the new title instead of the PayGateMini learn-more testid.
AI summary generation called the LLM with no budget check. Adopt the chat
assistant's pattern (ee/api/conversation.py): is_team_limited(AI_CREDITS) via
the billing quota-limiting cache. Enable-time gate in the serializer raises
QuotaLimitExceeded when toggling a summary on while over budget; generation-time
gate in the snapshot activity skips the summary and delivers the rest (graceful
degradation) so an org over budget stops consuming credits mid-month.
@vdekrijger vdekrijger force-pushed the worktree-subscriptions-freemium branch from 0c25637 to 835003b Compare May 22, 2026 13:26
@vdekrijger
Copy link
Copy Markdown
Contributor Author

Superseded by a 2-PR Graphite stack on the main repo: #59624 (freemium subscriptions) and #59625 (AI-summary credit gate, stacked). Moved off the fork to PostHog/posthog.

@vdekrijger vdekrijger closed this May 22, 2026
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