logbook/Docs

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

PlanProjectsLogsPriceHow you get it
free150 lifetime$0Default for every new org
personalUnlimitedUnlimited$5/mo, 1 seatStripe subscription
proUnlimitedUnlimited$20/mo for 5 seats, then $5/seatStripe subscription
founderUnlimitedUnlimited—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

  • free orgs. They have no subscription to lapse. Hitting the 50-log cap stops creating and nothing else, no matter how long they sit there.
  • founder orgs. 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 to pro.
  • 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

VariablePurpose
STRIPE_SECRET_KEYMaster switch. Unset → every cap and lock fails open.
STRIPE_PRICE_TEAMThe graduated seat price. Falls back to STRIPE_PRICE_PRO (the old flat price) if unset.
STRIPE_WEBHOOK_SECRETVerifies /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.