Monetization foundations
The plumbing for differentiating "free" vs "pro" features. No payment flow is active in v0.15.x — every feature is enabled, every quota is generous. This document describes the system so you know what's there and how to extend it when the time comes.
Where it lives
src/config/plans.ts — single source of truth.
The plans
type PlanId = 'free' | 'pro'- Free (default for everyone in v0.15.x) — every feature enabled, generous quotas.
- Pro (forward-looking placeholder) — same features today; intended to host higher quotas + future "pro-only" features.
getEffectivePlan() returns the active plan — currently always 'free'. When a payment integration ships, this is the single function to update.
Feature flags
41 flags covering every shipped capability. Sample:
'tool.pencil': { enabled: true, plan: 'free' },
'gamedev.autotile-47': { enabled: true, plan: 'free' },
'export.upscale-xbr': { enabled: true, plan: 'free' },
// ... etcRead via:
import { useFeature } from '@/config/plans'
const { enabled, requiresUpgrade } = useFeature('gamedev.autotile-47')
if (!enabled) return <UpgradePrompt />requiresUpgrade is true when the user's plan is below the feature's required plan. Always false in v0.15.x because the effective plan check passes for everything.
Quotas
Numeric limits that scale with plan tier:
| Quota | Free | Pro |
|---|---|---|
maxCanvasSize | 512 px | 4096 px |
maxFrames | 200 | 4096 |
maxLayersPerFrame | 32 | 256 |
maxBackupsPerProject | 8 | 64 |
maxCustomBrushes | 64 | 512 |
maxColorCycles | 32 | 256 |
maxFrameTags | 64 | 512 |
maxSlices | 64 | 1024 |
maxReferenceImages | 16 | 128 |
Read via:
import { getQuota } from '@/config/plans'
const max = getQuota('maxFrames')The first call site already wired through is MAX_BACKUPS_PER_PROJECT in src/features/persistence/backups.ts — when more numeric limits get added across the codebase, they should all flow through getQuota() so plan upgrades are a single-config change.
Adding a feature flag
- Add the entry to
FEATURE_FLAGSinplans.ts. - At the call site where the feature is consumed, gate it:ts
const { enabled, requiresUpgrade } = useFeature('my.new-feature') if (!enabled) { // Show disabled state or upgrade prompt }
Adding a quota
- Add the new field to the
QUOTASshape (both free and pro entries). - Replace any hard-coded limit with
getQuota('my-new-quota').
What's intentionally not here
- Payment integration. This file knows nothing about Stripe, Paddle, or LemonSqueezy. When that ships, it lives in a separate module that calls
_setEffectivePlanForDev(renamed) based on the user's verified subscription state. - UI for upgrade prompts. No Pro buttons, no "Upgrade to unlock" tooltips. Adding them is a UI-only change once the flag system is in active use.
- Feature-flag panel for beta testers. The 0.7.1 polish round considered this; deferred until there's a meaningful difference between free and pro.
Related
- Project backups — uses
getQuota('maxBackupsPerProject') - Preferences — separate concept: per-user editor settings, not per-plan capabilities