Documentation menu

Account and billing

Plans and billing

The organization is the billing entity: each one has its own plan. The catalog lives in one place, packages/config/src/plans.ts (@ahena/config/plans), and Core, the API, the dashboard and the pricing page (ahena.io/pricing) all read it.

Plan Price Projects Team Actions / month
Free $0 1 1 1,000
Starter $19/mo 5 1 10,000
Pro $49/mo 25 5 50,000
Business $149/mo Unlimited 20 250,000
Enterprise Custom Unlimited Unlimited Custom (arranged directly)

Limits

  • Projects and members are firm. Creating a project or adding a member beyond the plan is refused with limit_reached (HTTP 402, details.upgrade: true); the dashboard shows the message with a link to Billing. The count runs under an advisory lock, so concurrent requests can't both slip under the limit.
  • One Free organization per owner when it is created or when an unfinished checkout falls back to Free, so limits can't be multiplied with extra organizations. It isn't checked when a cancelled subscription returns an organization to Free, so an owner can end up with more than one.
  • Actions are soft. An action is one provider operation Ahena performs: validate/verify, discover, inspect, state, Doctor (per provider), plan, apply (once, including its post-write verification) and disconnect. Each executionContext in connections.ts counts one, per organization per UTC month (organization_usage). Reads of Ahena's own data aren't actions. Going over never blocks anything: Billing shows a warning, and the audit log records billing.actions_warning (80%) and billing.actions_over (100%) once each per month.
  • Downgrades never delete. Projects and members over the new limits keep working; nothing new can be added until the organization is within them.

Lifecycle

Organization state plan billing_status
Created on Free free active
Created on a paid plan, checkout not finished the chosen plan checkout (no projects or members can be added)
Stripe confirmed payment from the subscription's price lookup key active
Payment failing (Stripe past_due or unpaid) unchanged, paid limits kept past_due
Subscription ended (Stripe canceled or incomplete_expired) free active

Whether failed payments ever end the subscription depends on the Stripe account's settings (Billing → Subscriptions and emails → "if all retries for a payment fail"): "cancel the subscription" moves the organization to Free; "mark as unpaid" or "leave past due" keeps it on past_due with the paid plan's limits indefinitely. A Stripe paused subscription is treated as active.

  • Checkout: POST /v1/orgs/:org/billing/checkout {plan} (owners, billing.manage). Without a subscription it returns a Stripe Checkout URL; with one it switches the subscription's price (prorated: the higher plan applies immediately and the difference is charged on the next invoice) and returns url: null. Prices are found by lookup key (ahena_<plan>_monthly). Session creation sends an Idempotency-Key made of the organization, the plan and a 10-minute window, so double clicks or a second tab within that window get the same session. Sessions created further apart, or for different plans, are separate and Ahena doesn't expire the earlier ones, so two can still both be paid (see duplicate subscriptions below). When the organization already has a Stripe customer, the session uses it; otherwise Stripe creates one from the owner's email.
  • Webhook: POST /v1/billing/stripe/webhook, authenticated only by the Stripe-Signature header (HMAC-SHA256 over the raw body, 5-minute tolerance). The subscription is always read back from Stripe, so a replayed or out-of-order event for the organization's own subscription can't set a stale plan. Each event id is claimed in billing_events before it is processed, so a redelivery, including one that arrives while the first is still running, is answered as a duplicate and not processed again. If processing fails (for example Stripe can't be read), the claim is released and the request fails, so Stripe's retry processes it. Plan changes are audited as billing.plan_changed by the system.
  • Duplicate subscriptions: an organization has at most one subscription in Ahena. If a second subscription becomes active while the organization's current one is still live (two Checkout sessions both paid, or an old session paid after an in-place switch), it never replaces the current subscription or customer. Ahena audits it once as billing.duplicate_subscription (result failure; metadata holds only the two subscription ids and the Stripe event id) and leaves it alone: Ahena doesn't cancel or refund it. The duplicate keeps charging until support or the owner cancels and refunds it in the Stripe Dashboard (the customer portal only shows the organization's own customer). If the current subscription has already ended in Stripe, the new one takes over normally.
  • Portal: POST /v1/orgs/:org/billing/portal opens Stripe's customer portal (payment method, invoices, cancellation).
  • Free fallback: POST /v1/orgs/:org/billing/free turns an unfinished checkout into Free (subject to the one-Free-organization rule). A paying organization cancels in the portal.