TestFinch

Docs

Plans, allowances, credits and retention

How a workspace is billed, what each plan includes, and what the limits mean in practice.

How plans, trials, allowances and credits work, and how to set the provider up. The catalog itself lives in apps/api/src/billing/plans.ts; this page explains the moving parts around it.

What a workspace has

Every workspace holds one entitlement row per product (WorkspaceEntitlement): a plan (free, starter, team, business, enterprise), a status, and the billing state behind it. Plans are per workspace, never per seat. A Suite subscription writes the same plan onto both products.

The plan in force is resolved lazily from the row (LimitsService.effectiveOf), so nothing needs a nightly job:

Allowances and limits

Allowances count in calendar months, from the ledger and the rows themselves, never from a stored counter. Each check lives in LimitsService and is called by the service that does the thing:

Limit Where it is checked Beyond the allowance
Members (admins and members; reporters are free) workspace invites, FlowQA project invites that bring someone new in refused
Projects per product Snag project create, FlowQA project create in an existing workspace refused
Snags a month capture refused
Schedules schedule create refused
Cloud test runs a month cloud execution start (local runs never count) 1 credit each, charged when the run completes
Explorations a month, AI-generated tests a month exploration start 10 credits per accepted test (2 with the customer's own model, phase 4b)

A refused action answers 402 with code plan_limit and details { product, limit, used, cap, plan }; the message names the plan and says where to upgrade. Credits are charged only for accepted output (a generated test that replayed green twice and failed the break-it check), never for attempts.

FlowQA Free also keeps at most 100 tests across the workspace's projects (tests); saving, importing or creating a project with more answers 402 plan_limit with limit: tests.

Lifetime plans

Every workspace that existed when billing launched holds Team on both products for life (WorkspaceEntitlement.lifetimePlan, written by the migration). A lifetime plan is a floor: a trial, a past-due grace or an ended subscription falls back to it, never to Free, and a row written below it still reads as the floor. Operators grant one with lifetime: true on the grant route.

Bring your own model

A workspace can connect its own Anthropic or OpenAI key (Settings, Billing, "Your own model"; PUT /v1/workspaces/:id/billing/model-keys/:provider). The API checks the key with the provider before storing it, encrypted with the FlowQA input cipher and scoped to the workspace, and only ever shows its last four characters.

With a key connected:

OPENAI_STRONG_MODEL picks the OpenAI model (default gpt-4.1). Sign in with ChatGPT (spec 4.3) needs OpenAI's approval of the app first and is not built.

Credits

WorkspaceBilling.creditBalance is the balance; every movement is a CreditTransaction with balanceAfter, so the ledger audits itself. Packs: 1,000 for 10 USD, 5,000 for 45, 20,000 for 160. Charges never take the balance below zero: a cloud run that outruns the credits is still recorded, with what was paid on its rows.

The provider

Dodo Payments is the merchant of record: it handles tax, invoices, cards, dunning emails and the customer portal. We create checkout sessions and read webhooks; plan state moves only through webhooks (or a trial, which needs no card).

Set up, once per environment (test and live are separate in Dodo):

  1. Create the products in the Dodo dashboard: one per plan and interval (snag.team.monthly, flowqa.starter.yearly, suite.business.monthly, ...), one per credit pack (credits_1000, credits_5000, credits_20000), and one monthly subscription per dedicated-agent pack (agents_1, agents_2, agents_3, agents_10). Prices are in plans.ts; yearly is ten months.
  2. Put their ids in DODO_PRODUCTS as JSON keyed by those names.
  3. Create a webhook for https://<api>/v1/billing/webhooks/dodo with the subscription and payment events, and put its signing secret in DODO_PAYMENTS_WEBHOOK_KEY.
  4. Put the API key in DODO_PAYMENTS_API_KEY and set DODO_PAYMENTS_ENVIRONMENT (test_mode or live_mode).

Without the keys, the Billing page shows the plans and the trial, and purchase buttons say billing is not set up.

BILLING_DEFAULT_PLAN is the plan a new workspace starts on, free by default. A pilot can start everyone on team; the API test environment uses business, since its fixtures are paid-size workspaces, and the billing suite sets Free where it tests the Free limits.

Webhooks are verified with the Standard Webhooks signature (webhook-id, webhook-timestamp, webhook-signature), deduplicated by delivery id (BillingEvent), and applied from the session's metadata (workspace_id, scope, plan, interval, or credits_pack), with the product map as the fallback.

Dedicated agents

The add-on of spec 5.2: browser workers reserved for one workspace, bought as a monthly subscription (POST /v1/workspaces/:id/billing/agents/checkout with a pack; a held pack changes in place). Business includes one agent; the add-on stacks on it (WorkspaceEntitlement.addOns.dedicated_agents, read into the dedicated_agents allowance). A workspace with agents gets priority 1 on its cloud runs, which shared workers take before anything else.

The agents themselves are workers we start with FLOWQA_WORKER_WORKSPACE=<workspace id> (see deploy/flowqa-worker/README.md): they claim that workspace's runs, explorations and repairs only, and GET /v1/qa/cloud/status?workspace_id= reports them as dedicated to that workspace's members. Bringing an agent online after a purchase is an operator step; the Billing page says so and shows the agents once they report in.

Retention

Retention follows the plan in force: Snag keeps a snag's media for retention_days (30 on Free, a year on Team and Business) once the snag is closed or rejected, and FlowQA keeps a run's screenshots and traces for artifacts_days (30, 30, 90 or 365) after the run finished. Open snags keep their media whatever their age, and results, snags and runs never go; only the files do. A null allowance (Enterprise, a custom grant) keeps everything.

RetentionService sweeps once an hour, up to 500 rows of each kind per workspace per pass, queuing the files on the same durable cleanup queue project deletion uses, so an object-store outage delays a deletion rather than losing track of it. A purged attachment keeps its row with purgedAt; the snag lists it as media_purged and its download answers 404. A purged run carries artifacts_purged_at; the workbench says the frames were removed rather than showing an empty gallery.

Operators

A platform operator (User.superAdmin) can set a complimentary plan with POST /v1/workspaces/:id/billing/grant ({ product, plan, allowances?, note? }): our own workspaces, pilots, Enterprise deals invoiced outside the provider. Do this for TPH's own workspaces right after the migration, since every workspace starts on Free.