Plans and billing
Logbook is freemium. The free plan is generous enough to run a real project; the paid plan lifts the limits.
There are two separate gates, and telling them apart is the key to this whole document:
- The cap — a free org has used its 50 logs. It stops creating. Everything it already has stays fully readable and editable, forever. This is an upsell, not a punishment, and a free org is never locked out.
- The lock — a paying org's payment lapsed and the grace window has since expired. Everything stops: board, API tokens, reads included.
The plans
| Plan | Projects | Logs | Price | How you get it |
|---|---|---|---|---|
free | 1 | 50 lifetime | $0 | Default for every new org |
personal | Unlimited | Unlimited | $5/mo, 1 seat | Stripe subscription |
pro | Unlimited | Unlimited | $20/mo for 5 seats, then $5/seat | Stripe subscription |
founder | Unlimited | Unlimited | — | Legacy — granted, not purchasable |
free
One project and 50 logs lifetime per org. The limits are constants in
lib/billing.ts (FREE_MAX_PROJECTS = 1, FREE_MAX_ITEMS = 50).
"Lifetime" means the created count, not the current count. It counts every item (log or epic) ever created in the org, across all its projects. Archiving a done item does not refund quota, and neither does deleting work — the counter only goes up. This is deliberate: the created-count is the upgrade-forcing function, so you cannot cycle through the free tier forever by archiving.
personal
$5/month for a single seat, otherwise unlimited. The one-seat cap is what keeps the ladder coherent: priced per seat it would undercut Team at every size below four and there would be no reason to buy Team at all. Capped at one, the tiers answer different questions — "just me" versus "my team" — and never compete. An org that grows past one member has outgrown Personal by definition.
pro
Unlimited projects and logs. $20/month covers the first five seats; each seat beyond that is $5/month. So one person pays $20, five people pay $20, eight people pay $35.
A seat is a member of your Clerk organization — there is no separate seat to assign or licence to buy. Invite someone and the subscription quantity goes up; remove them and it goes down, prorated either way.
Stripe holds this as a single graduated price (tier 1: up to 5 seats, flat
$2000; tier 2: $500 per seat), so the subscription carries one item whose
quantity is the seat count and Stripe does the arithmetic. The constants in
lib/billing.ts (TEAM_BASE_CENTS, TEAM_INCLUDED_SEATS,
EXTRA_SEAT_CENTS) exist only to display the figure before checkout —
Stripe remains the source of truth for what is actually charged.
Seat drift is corrected two ways: Clerk's
organizationMembership.created/deleted webhook (proactive, if subscribed
in the Clerk Dashboard) and a re-sync every time anyone opens Settings
(self-healing, so an unsubscribed deployment still converges).
founder
Unlimited, and not for sale. Every org that existed before billing shipped was
grandfathered onto founder. A founder org never lapses and is never
locked — it has no subscription to lapse.
Grant it with scripts/grant-plan.mjs:
node scripts/grant-plan.mjs <org_id> --list # inspect current state
node scripts/grant-plan.mjs <org_id> founder --yes # grant (--yes required off localhost)
Prefer founder over pro when comping an org. Both are unlimited, but a
pro row with no stripe_customer_id is unreachable by
updateOrgBillingByCustomer (which matches on that column), so it would never
lapse either — for the wrong reason, and indistinguishable from the
checkout defect fixed in b1f0ddd. founder states the intent, and Settings
labels it "granted, not billed".
What the cap gates (and what it never gates)
This section is about the cap — the free-tier limit. The lock is different and is covered further down.
The cap applies on exactly two create paths:
- Creating logs and epics —
create_item,create_items, and the web create actions. Epics count toward the same 50-log quota as logs; a container still consumes one. - Creating projects — the web project-create / onboarding flow. There is no MCP tool to create a project, so this gate is web-only.
Everything else is never gated, on either plane (web or MCP):
- Reading —
list_backlog,pull_item,pull_items,backlog_stats, all queries - Updating items — status changes (including marking done), priority, labels, reassigning, resolutions
- Notes, tasks, and documents
- Archiving
A capped org keeps full access to the data it already has. You can always finish, document, and close out existing work — you just cannot create new logs past the limit until you upgrade.
The error agents see when capped
When a create would exceed the org's plan, the MCP create tools return a
structured, self-explanatory error instead of creating anything
(create_items checks the whole batch up front — all-or-nothing):
{
"error": "upgrade_required",
"message": "This org is on the free plan (50 logs; 50 used). Upgrade to keep logging: https://logbook.gvy.ai/settings",
"items_used": 50,
"max_items": 50,
"upgrade_url": "https://logbook.gvy.ai/settings"
}
The error code is always upgrade_required. The message is a readable
upgrade line; items_used / max_items are the counts; upgrade_url points
at Settings. On the web the same limit surfaces as a readable error message.
The project-creation gate raises the same upgrade_required shape with a
message like The free plan is limited to 1 project. Upgrade to add more: <url>.
When billing is dormant
Billing only enforces once Stripe is configured on the deployment
(STRIPE_SECRET_KEY present). With no Stripe keys, billingEnabled() is
false and every gate passes — the app fails open. So on a deployment
without Stripe configured you will never see upgrade_required, regardless of
plan or usage.
What a lapsed payment does
This changed. Lapsing used to demote an org to free limits, leaving it fully readable. It now locks the org after a grace period. If you are relying on the old behaviour, stop.
When Stripe reports a failed charge (invoice.payment_failed) or the
subscription goes past_due / canceled, the org enters a grace window of
GRACE_DAYS — currently 14 days.
During grace — nothing changes
The board, every MCP tool, and every create path behave exactly as they did while paid. The only difference is a banner in Settings counting the days down. This is deliberate: Stripe's own card-retry schedule runs about this long, so a card that succeeds on retry means the customer never notices the window existed.
After grace — everything stops
Once the window expires the org is locked:
-
The web app — every data page (board, archive, inbox, PRs, contributions, item detail) redirects to
/locked, a paywall carrying the pay button. Settings stays reachable, because a paywall you cannot pay from is just an outage. -
MCP tokens — the endpoint returns HTTP 402 for every tool call, reads included. The gate sits in
handle()before the MCP handler is built, so it covers every tool by construction, including ones added later:{ "error": "payment_required", "message": "This organization's payment failed and the 14-day grace period has ended. Logbook is locked until the subscription is current: https://logbook.gvy.ai/settings", "status": "past_due", "upgrade_url": "https://logbook.gvy.ai/settings" }
A lock deletes nothing. Projects, items, notes and history are all intact;
they are paused, not removed. The moment a payment succeeds
(invoice.paid), the grace window is cleared and access is restored
immediately — there is no re-provisioning step and nothing to restore by hand.
Who can never be locked
freeorgs. They have no subscription to lapse. Hitting the 50-log cap stops creating and nothing else, no matter how long they sit there.founderorgs. No subscription, status always active.- Any org, on a deployment with no
STRIPE_SECRET_KEY. Same fail-open rule as the caps.
The grace deadline does not drift
grace_ends_at is written once, when the org leaves active, and is not
recomputed by later events. Stripe redelivers webhooks and fires
customer.subscription.updated repeatedly throughout dunning; recomputing
now + 14 days on each one would push the deadline forward forever and the
lock would never fall. Returning to active clears the column outright.
A row that lapsed before this column existed has grace_ends_at = NULL.
That is read as a fresh 14-day grace window, not as locked — otherwise
shipping this would have booted every already-lapsed customer instantly —
and getAccess() writes the concrete deadline back on first read.
How to upgrade
Upgrading happens in the Billing panel in Settings. Two Stripe-backed flows drive it:
- Checkout (
POST /api/billing/checkout) starts a Stripe Checkout session for the active org and returns a URL to redirect to. The line item's quantity is the org's current Clerk member count. Complete the subscription there and the org flips topro. - Billing portal (
POST /api/billing/portal) opens the Stripe Billing Portal for the org's existing customer — manage the subscription, update the card, or cancel. Available once the org has a Stripe customer (that is, after a first checkout).
Both are scoped to the org you are currently active in, and both land you back
on /settings when done. If billing is not configured on the deployment, both
return 503 with a clear message.
A locked org reaches the same two buttons from the /locked paywall, which is
the only page it can see besides Settings.
Testing it
npm run test:billing runs the whole billing surface against your local dev
database: seat tiers, the grace state machine, the free cap, MCP 402
enforcement, and a source audit asserting no server action bypasses the lock.
It signs synthetic Stripe webhooks locally, so no Stripe account is needed
and no network call is made. It refuses to run against a non-local
DATABASE_URL.
Deployment configuration
| Variable | Purpose |
|---|---|
STRIPE_SECRET_KEY | Master switch. Unset → every cap and lock fails open. |
STRIPE_PRICE_TEAM | The graduated seat price. Falls back to STRIPE_PRICE_PRO (the old flat price) if unset. |
STRIPE_WEBHOOK_SECRET | Verifies /api/webhooks/stripe. Without it the route returns 503 and no subscription state is ever synced. |
Subscribe the Stripe webhook to checkout.session.completed,
customer.subscription.created, customer.subscription.updated,
customer.subscription.deleted, invoice.payment_failed and invoice.paid.
The last two are what open and close the grace window — without them a failed
payment is noticed late, and a recovered one may not clear the lock.