Stripe setup (operators)

Configure a Stripe account with the Maketools script (products, Plus and Pro prices, Stripe Tax, customer portal, webhook), the remaining manual steps, billing locally and Enterprise activation.

Developers7 min read

This page is for the team that runs Maketools. The billing service uses the official Stripe SDK: Checkout, the customer portal and webhooks. A Stripe account is configured with the scripts/billing/stripe-setup.ts script, then a few settings are made by hand in the Dashboard. Use the test mode for staging, the live mode for production.

The setup script

# Dry run: lists what would be created or updated, writes nothing
STRIPE_SECRET_KEY=sk_test_… bun scripts/billing/stripe-setup.ts \
  --base-url https://staging.example.com --dry-run

# Test mode (staging)
STRIPE_SECRET_KEY=sk_test_… bun scripts/billing/stripe-setup.ts --base-url https://staging.example.com

# Live mode (production): --live is required with a live key, and refused with a test key
STRIPE_SECRET_KEY=sk_live_… bun scripts/billing/stripe-setup.ts --base-url https://example.com --live

--base-url is the origin of the site (https, or http://localhost:<port> in test mode). The script is idempotent: it compares each resource with what it should be and only writes the difference; run again on an up-to-date account, it writes nothing. It ends with a summary (one line per resource: created, updated, unchanged or to do by hand) and the list of the manual steps it found. Detailed usage: scripts/billing/README.md.

It configures:

  1. Two products, maketools_plus and maketools_pro (IDs set at creation, plan metadata), Stripe Tax code txcd_10103001 (software as a service, business use), unit "siège" (seat).

  2. Four prices, in EUR, excluding tax (exclusive tax behaviour), per unit (licensed, not metered): the quantity of the subscription is the number of seats, adjusted every day by the billing-seat-sync job (see Seats and active members).

    Lookup key Plan Amount per seat, excl. VAT
    maketools_plus_monthly Plus €6 per month
    maketools_plus_yearly Plus €60 per year
    maketools_pro_monthly Pro €10 per month
    maketools_pro_yearly Pro €100 per year
  3. Stripe Tax: head office (149 avenue du Maine, 75014 Paris) as the origin address, default tax code and tax-exclusive behaviour, and the French VAT registration. If Stripe Tax is not activated on the account yet, the script carries on and reports it as a manual step.

  4. Customer portal: invoice history, payment method, billing details (address, email, name, phone, VAT number), cancellation at the end of the period without prorated refund, with the cancellation survey (too expensive, missing features, switched service, unused, customer service, too complex, low quality, other), and switching between the four prices, with the difference invoiced immediately. Changing the quantity is not offered: seats are managed by Maketools. The links shown are the French terms of sale and privacy policy (French is the authoritative version); the return link leads to <site>/admin.

  5. Webhook <site>/api/billing/stripe/webhook (through CloudFront: the API refuses calls that do not go through it), subscribed to exactly the events the backend handles and pinned to the same API version. Stripe only returns its signing secret (whsec_…) at creation: the script prints it once, with the command that stores it in the environment's secrets.

Amounts live only in Stripe and in the script's catalog (scripts/billing/stripe-catalog.ts, the source the script pushes); a test checks that the prices shown by the site match this catalog. The backend knows no Price ID: it finds the plan of a subscription from the lookup key of its price, the same in test and live mode. A subscription on a price with neither one of these four keys nor the maketools_plus or maketools_pro product grants nothing (billing_unknown_price alert).

Changing a price

A Stripe price cannot be edited. After an amount changes in the catalog, the script creates a new price that takes over the lookup key, then deactivates the old one. Current subscriptions stay on the old price: it no longer has a key, but the backend still recognises their plan from the Stripe product (maketools_plus or maketools_pro); they pay the old amount until they are moved. The script's summary lists them; move them to the new price (Stripe Dashboard, subscription, "Update subscription", without proration: the new amount applies at renewal). An increase is notified to customers at least 30 days before the renewal it applies to (terms of sale, section 5.4): run the script once that notice has been given, and update the prices shown by the site at the same time.

Manual steps

To do once per account, in the Stripe Dashboard:

  1. Activate the account: identity verification (KYC) and payout bank account.
  2. Public business details: company name MAKETOOLS SAS, SIREN 990 247 603 (RCS Paris), VAT number FR63 990 247 603, head office address, support email.
  3. Branding of Checkout, the portal and invoices: logo, icon, colours.
  4. Invoices: numbering prefix, and a default invoice footer carrying the mandatory business-to-business terms in France: late payment penalties at the ECB rate plus 10 points, a fixed €40 recovery fee (articles L. 441-10 and D. 441-5 of the French Commercial Code), no discount for early payment. The backend also sets this footer on every Stripe customer it creates; the account setting covers the other cases.
  5. Stripe Tax, if the script reported it: activate it, then run the script again.
  6. Unpaid invoices: Smart Retries on, and "mark the subscription as unpaid" after the last retry.
  7. Backend key: a restricted key rather than the secret key (permissions below).

Environment secrets

Secret Value
StripeSecretKey backend key (sk_… or, recommended, a restricted rk_… key)
StripeWebhookSecret signing secret of the webhook (whsec_…), printed by the script when it creates it

In staging and production they are kept in AWS Secrets Manager (self-hosting on AWS), never in the code or in the images. Set the values: bun run infra:secrets <env> --set StripeSecretKey, then --set StripeWebhookSecret (value read from standard input; deploy/secrets.md). There is no Price ID to store any more.

Permissions of a restricted key:

  • backend: write on Customers, Checkout Sessions, Customer portal, Subscriptions and Invoices, read on Prices and Refunds;
  • setup script (a separate key, kept out of the backend secrets): write on Products, Prices, Tax settings, Tax registrations, Customer portal (configurations) and Webhook endpoints.

Checkout and customer portal sessions are limited to 10 per hour per organization, re-syncs from Stripe to 20 per hour, and next-invoice previews to 60 per hour (cached for one minute).

Local development

Without StripeSecretKey (the default of deploy/env/local.env.example), billing is disabled cleanly: every organization is Free, the Checkout buttons explain that Stripe is not configured, and the webhook answers ERR_BILLING_NOT_CONFIGURED.

To force an organization out of Free locally, activate Enterprise with the admin script (below).

To try the real flow in test mode:

  1. configure a test account with --base-url http://localhost:4200 (the script creates no webhook, as Stripe cannot reach localhost);
  2. run stripe listen --forward-to localhost:4000/api/billing/stripe/webhook, which prints a whsec_… secret;
  3. put the test key and this secret in deploy/env/local.env (never committed), then run ./deploy/set-local-secrets.sh.

Activate Enterprise

Enterprise is activated by a platform administrator through the internal API billing.setEnterprisePlan, with the script scripts/admin/enterprise.ts (explicit confirmation: retype the organization slug; named operator; audited as billing.enterprise.granted or billing.enterprise.revoked). Locally, from backend/:

bun ../scripts/encore.ts exec -- \
  bun ../scripts/admin/enterprise.ts grant acme --operator "Jane Ops" --reason "Quote 2026-042"

In staging and production, activate it from the organization's page in the operations console (bun run console), which calls the same internal API through a signed request, under the name of the operator key. If the organization had a Stripe subscription, cancel it in the Stripe dashboard.

Data kept by Maketools

Stripe customer and subscription IDs, status, period, seats, unit amount (excluding tax) and currency of the price, start, cancellation request and end dates, the reason picked in the cancellation survey (never the free-text comment), and the IDs of processed webhook events (idempotency). For Maketools' internal statistics, a payment register: for each subscription invoice (paid or failed) and each refund, its Stripe ID, the organization, the amount excluding tax, the tax, the currency, the plan and the date. No card number, bank detail or billing address: Stripe is a subprocessor and keeps them. Everything is erased with the organization.

Edit this page on GitHub (opens in a new tab)