Subscription states & paywall
A team's subscription lives in one of seven states. This page is the full state machine: what each state means, what's blocked, and how you move between them.
The states
| State | Meaning | Pool writes blocked? | Duration |
|---|---|---|---|
trial | 14-day trial, starts on team creation | No | Until trial_ends_at |
active | Paid plan, current | No | Until current_period_end |
past_due | Invoice failed, Stripe is retrying the card | No (degraded) | Until grace_period_ends_at |
canceled | Subscription canceled, in the grace window | No (degraded) | Until grace_period_ends_at |
grace | Generic grace state (e.g., post-trial) | No (degraded) | Until grace_period_ends_at (5 days) |
expired | Past grace period, no recovery without re-subscribe | Yes | Indefinite |
none | No subscription at all | Yes | Until a plan or trial is started |
"Degraded" means the team can read the pool but not mutate it — they see cards but can't push, pull, claim, or reassign. "Blocked" means pool writes are rejected and the UI shows a paywall banner.
The normal lifecycle
none → trial → grace → expired
↓
active ↔ past_due → grace → expired
Create a team (owner flow)
→ subscription enters trial with trial_ends_at = now + 14d, grace_period_ends_at = now + 19d
Day 14
→ trial ends → status becomes grace → 5-day window begins
Day 19
→ grace ends → status becomes expired → pool writes now blocked
At any point, owner/admin hits Upgrade
→ Stripe checkout → webhook creates subscription → status becomes active (or trialing during Stripe's own trial if configured)
During active, a card fails
→ Stripe marks invoice payment_failed → webhook → status becomes past_due → Ideafy starts a 5-day grace window
During past_due, a new card works
→ webhook invoice.payment_succeeded → status back to active, window cleared
Owner cancels from the portal
→ webhook customer.subscription.deleted → status canceled → grace window starts. You keep full access until your current paid period ends (Stripe doesn't charge for unused time), then it's 5 more days of degraded access, then expired
The grace period
Grace is 5 days. It exists for three reasons:
- Payment failure isn't always the user's fault. Expired card, bank glitch, card on-hold. Giving a 5-day window avoids punishing people for their bank's problems
- Cancellations are reversible. 5 days lets you walk back a canceled subscription without your team losing pool access
- Weekends. If your trial ends on Friday, you have the weekend plus the week to deal with it
During grace, Ideafy shows a banner: "Subscription expired — upgrade to continue pool operations." Reads still work. You can still browse the pool, open cards, see history. Writes are blocked.
What none means
none happens to a team that was created without a subscription starting. In practice this is rare — the owner flow starts a trial automatically. none mostly shows up when:
- A team is created through an internal migration or import that skipped the trial-start step
- The trial-start flow fails for some reason and the team ends up without a subscription record
Fix: Settings → Billing → Start trial (if available) or Upgrade directly.
How the paywall is enforced
The paywall is enforced in the API layer, not in the UI. A middleware called requirePaidAccess runs on every pool write endpoint:
- Reads the team's subscription row
- Computes the status
- Returns
{ ok: true }if status isactiveortrial - Returns
{ ok: false, status }otherwise - Failing endpoints return HTTP 402 with a
subscription_requirederror code
This means bypassing the UI doesn't bypass the paywall. Calling the API directly, or calling through the MCP server's pool tools, hits the same check.
Pool reads don't go through the middleware — they're always allowed.
What you see as a user
active/trial: no banner, everything workspast_due/canceled/grace: orange banner at the top of the board with "X days left in grace period" and an Upgrade / Update payment buttonexpired/none: red banner with "Pool access blocked — upgrade to continue" and an Upgrade button. Pool write operations return 402 with an error toast
The Refresh status button
If you just completed a Stripe checkout and the app still shows a blocked state, there may be a few seconds of webhook delay. Settings → Billing → Refresh status forces a re-read of the subscription row from Supabase. Usually clears transient lag.
Prev: Stripe checkout & portal Next: General settings Up: User guide index