Pre-1.0 · one maintainer · Stripe and cards only · no SDKs (client libraries) yet · no production users yet. Docs describe v0.9.1 (29 Oct 2026) · What’s new →

How Hardcount keeps a bill right

Hardcount decides what each customer is charged, so it is built to rules, and each rule comes with its evidence. Where a case is not fully handled yet, this page says so.

What can go wrong, and what Hardcount does

“A customer was charged twice for one invoice.”

Each charge attempt carries one fixed key, so if Hardcount resends that attempt, Stripe treats it as the same charge. Only one charge runs for an invoice at a time.

The rare case left

When Stripe’s answer is lost, Hardcount looks for the payment at Stripe and charges again only when it finds none. If Stripe finishes the first charge after that, as can happen during an outage at Stripe, the customer pays twice. Hardcount spots the second payment and flags it for you to refund.

How card payments are handled →

“The billing job ran twice and sent two invoices.”

Each billing period can be closed only once, so a second run finds it closed and stops. The database also refuses a duplicate invoice for the same period.

The design record →

The rules the code follows

A billing period is billed once.

Every close locks the subscription and checks its period has not moved. Behind that, a unique index refuses a second invoice for the same period, so a crash or two racing workers cannot bill it twice.

Evidence: A billing process killed at five points left 0 duplicate and 0 lost invoices. With the index dropped, four billing engines racing on 40 periods billed 103 invoices, so the test can see a failure. That run predates the subscription lock, the guard now in front of the index. Correctness under failure →

Fail loudly.

If a read that a bill needs fails, the bill stops with an error. It never bills a default. A subscription that keeps failing is parked for you to look at.

Evidence: How it works, and why, is in its design record. Design record 119 →

For card charges, Stripe’s record decides what was paid.

A charge with an unclear result is held while Hardcount checks Stripe for the payment. If Stripe shows none 24 hours after the attempt, the invoice is charged again.

Evidence: Each case, and the known gaps, are listed in one place. Card payment failure handling →

One Postgres.

All billing data lives in Postgres; Redis is for rate limits only. No queue broker, warehouse or workflow engine until a measurement says Postgres is not enough.

Evidence: One large database took 12,000 usage events a second in 4 of 5 ten-minute runs, and in all 5 after one Postgres setting change. The hardware and method are stated. How much it handles →

How it is tested

Every change ships with a test at the right layer: Go tests against a real Postgres for money and state, and browser tests for the dashboard. A fix is proven by watching its test fail without it. These run on every change:

  • Accounts kept apart. Tests check the Postgres row-level security policies, and code that skips them fails a test until it is listed. See it →
  • One charge per invoice. When two workers race to charge an invoice, exactly one wins. See it →
  • A clean 13-month run. A read-only checker finds no bad rows after each close of a 13-month billing run. See it →
  • Generated cases for the money math. Property tests for billing dates, pricing, proration, tax and the credit waterfall. See it →
  • Every doc command runs. CI runs every command in “Your first invoice” against a real build. See it →

How we test → · Sustained throughput →

How it is built

One Go program with its background jobs inside, each job held by one copy at a time. Postgres row-level security keeps accounts apart on most requests. Every decision, including the reversed ones, is written down.

Architecture → · Design decisions → · The code →

Security

Accounts kept apart in the database, personal data and keys encrypted at rest, signed webhooks, and a private way to report a vulnerability.

Security policy →