Subscription Tiers
SkyState offers three subscription tiers. Limits are configured in api/SkyState.Api/appsettings.json under the TierSettings key and loaded at startup via IOptions<TierSettings>.
Tier Comparison
| Limit | Free | Hobby | Pro |
|---|---|---|---|
| Max projects | 3 | 7 | 15 |
| Max API requests / month | 200 | 2,000 | 20,000 |
| Environments per project | 3 fixed (development, staging, production) | same | same |
| Stripe subscription required | No | Yes | Yes |
null in the limit config means unlimited. The Pro tier could be configured with null limits in the future without code changes.
Tier Hierarchy
Tiers are ordered internally: free (0) < hobby (1) < pro (2). This ordering is used to determine whether a tier change is an upgrade or a downgrade:
- Upgrade (e.g., free → hobby, hobby → pro) — takes effect immediately; proration is charged.
- Downgrade (e.g., pro → hobby) — scheduled via a Stripe Subscription Schedule to take effect at the end of the current billing period.
Grace Period
When a payment fails, the account enters a 7-day grace period. During the grace period:
- Project creation limits are not enforced (
CheckProjectLimitAsyncreturnsSuccess(true)) - The account retains its current subscription tier for the duration of the grace period
- After 7 days without payment recovery, the subscription is cancelled by Stripe and the account is reset to the
freetier
The grace period is implemented in BillingService using the payment_failed_at column on the account table.
Adding or Changing Tier Limits
Tier limits live entirely in configuration — no code changes are required to adjust them. In appsettings.json (or environment-specific overrides):
"TierSettings": {
"Tiers": {
"free": {
"MaxProjects": 3,
"MaxApiRequestsPerMonth": 200
},
"hobby": {
"MaxProjects": 7,
"MaxApiRequestsPerMonth": 2000
},
"pro": {
"MaxProjects": 15,
"MaxApiRequestsPerMonth": 20000
}
}
}To add a new tier, you would also need to update the subscription_tier check constraint in the database schema and the GetNextTier logic in BillingService.
Database Representation
The active tier is stored in account.subscription_tier as a lowercase string. The database enforces a check constraint:
check (subscription_tier in ('free', 'hobby', 'pro'))The tier is updated by the Stripe webhook handler when a subscription event is received.