Skip to content
UdriaUdria

Changelog

Production Stripe Billing follows semantic versioning. Every v1.x release published in the five years after a purchase is included in it at no further cost.

1.3.0

The last of the audit findings — the small ones. Two turned out to be checks pointing at nothing, and one is a missing index on the table a billing page reads most.

What you need to do

Apply the new migration when convenient. It is optional and additive, and worth doing before your invoice list gets long. If your code reads the auditId returned when an operator resolves a stuck billing operation, it is now a string rather than a number.

Your invoice list had no index

The invoice table shipped with four indexes and every one of them answers a question the background worker asks — which account owns this charge, this payment, this Stripe invoice. Those matter: a dispute arrives carrying a charge and has to reach the right account without asking Stripe.

Nobody indexed the question your customer asks, which is "show me my invoices". Every other table had an account index. This one, the one a billing page reads most, did not.

On 200,000 invoices across 20,000 accounts, listing one account's twenty most recent invoices read the entire table and sorted it: 12.4ms. With the index, 0.04ms.

migrations/0004_billing_invoices_principal_index.sql adds it, and refuses to run on a table above 50,000 rows — naming the command to run by hand instead, because building an index under a write lock would block the webhook worker that writes invoices. Both the refusal and the recovery it describes are tested, since an instruction that leads back to the error it was meant to resolve is worse than no instruction.

The requirements map is checked now, not just written

Version 1.0.0 was withdrawn partly because 51 entries in the traceability file named tests that did not exist. They were written, and that was the end of it — nothing stopped the next one from naming nothing.

There is now a test that walks the whole map: every entry must resolve to a real test, every test claiming a requirement must be one the map lists, and every requirement must carry a status the release gate can actually read. An unreadable status is worse than a wrong one, because it disappears from both the complete and the incomplete totals.

Writing it turned up two things about checks rather than about the code. The first version only looked at test files and reported six conformance checks as missing — they exist, in a library file beside them; a check that cannot see where the evidence lives invents problems. And asserting the reverse direction reports 32 entries that are not errors at all, so that assertion was left out deliberately, with a note saying why. A check that fails 32 times on its first day gets switched off rather than obeyed.

Two functions that looked like they contradicted each other

One said a stuck operation could move to "completed"; the other refused to let it. Reported as a bug. It is not one: the move is real, and the operator recovery path makes it — with evidence, an identity, and an audit record written in the same transaction. What is refused is the automatic path taking it, because the entire reason the operation is stuck is that nobody yet knows whether you were charged.

One describes what is possible, the other guards what a worker may do. Both now say so, and a test pins the difference so it cannot be tidied into agreement.

Smaller

A test named "refuses a path that climbs out of the configured origin" asserted the opposite — that such a path is accepted and safely normalised, which is correct behaviour. Renamed, because someone scanning test names for coverage would have counted a protection that does not exist.

And the package description claimed one of its migrations needed special handling from your migration tool. It does not, and never did in the shipped version — that note described a design replaced before release, and was telling you to work around a problem you do not have.

Checksum

production-stripe-billing-1.3.0.zip
sha256  48a41e33a9f9d1ac264cd490d9fb0a9d9d56c383c880d923d7c7d5f9d173b9b9
bytes   441317

Upgrading

Replace the folder, apply 0004 when you are ready, and re-run the suite. Nothing else changes.

1.2.0

The second half of the same audit. Five findings, three of them the same shape: one rule written twice, with the copies quietly out of step.

What you need to do

Nothing, unless your code imports coalesceSignal, or passes unresolvedRisk to isSweepEligible or isTerminalForSweep. Those are gone and your compiler will say so.

One rule, written twice, disagreeing — for the fourth time

Three places described which subscriptions the daily reconciliation should re-read. Two of them included accounts with an unresolved dispute. The query that actually builds the list never had, so a cancelled subscription with an open dispute was eligible by the rule and invisible to the code. Both descriptions had tests. Neither test compared them to the query.

This is the fourth release to ship that shape — a helper and the SQL beside it, saying different things, each with its own passing test. The previous three were fixed one at a time. This one is fixed with a test that puts every combination through a real database and requires all three to agree, so the next divergence fails a build rather than reaching you.

We removed the term rather than adding it, and the reason is worth reading

The obvious fix was to add disputes to the query. We tried that first and measured it: it costs the index the reconciliation depends on, taking a page from 0.35ms to 1.58ms on a 200,000-row table.

But the deeper reason is that it would have bought nothing. The sweep re-reads the Stripe Subscription. A dispute's state lives on the Stripe Dispute object. Re-reading a cancelled subscription because a dispute is open against it re-confirms the cancellation and learns nothing about the dispute. Every case where the dispute genuinely warrants a re-read was already covered.

So there is a real gap, and it is now written down instead of papered over: if the charge.dispute.closed webhook is missed for an already-cancelled subscription, the daily sweep will not catch it. Resending the event does (that path was fixed in 1.1.0), and so does an operator. Automatic reconciliation of disputes against Stripe is not in v1.

A check that told your agent not to silence an alarm that could not ring

The package hashes every acceptance test, so that an agent adapting them to your schema cannot quietly weaken what one asserts. The safety rules told the agent not to regenerate those hashes to hide a mismatch. There was no way to produce a mismatch — the hashes were written and never read back.

npm run manifest:check

It names every test file that has changed, been added or gone missing, and the release gate now refuses on it.

The gate now reads evidence, not just a ledger

npm run gate decided whether the package was shippable by reading a requirements file. Changing two characters in that file moved it from 216 complete to 218 with no code touched. For the rows marked "inspection" that file is the record; for the 166 marked "automated" it was a claim about tests nobody had confirmed were run.

npm test now writes its result, and the gate requires that result to exist, to pass, and to be newer than the code it claims to prove. Break something and skip the tests, and the gate says so instead of reporting everything complete.

A test that proved something about PostgreSQL rather than about us

The test for deadlock-free lock ordering wrote its own database statements, so it demonstrated that PostgreSQL serialises locks taken in the same order — true, and nothing to do with this package. The helper that encodes the order had no caller anywhere, in code or in tests.

It now goes through that helper, and a second test takes the locks in the wrong order and confirms the database aborts one of them. Following the rule works; not following it fails. Both are now shown.

Checksum

production-stripe-billing-1.2.0.zip
sha256  71c9e0b01f86354acee4f6de505010692de8fb30298e6304933ce525f928414f
bytes   430982

Upgrading

Replace the folder and re-run the suite. No migration. If your compiler flags coalesceSignal or unresolvedRisk, those are the three removals above and the fix is to delete the call — nothing replaces them.

1.1.0

A second independent audit, this time of the 1.0.2 archive. Five findings, and the release that closes them. None was in the billing logic; four were things the package said about itself that were not true.

What you need to do

Two changes break a build and one changes behaviour, so this is the first release that does not drop cleanly into an existing installation. Your compiler finds the first two:

  • assertLifecycleMutationAllowed now requires an authorised field. Pass the result of your real server-side permission check at every call site. An endpoint that never evaluated authorisation no longer compiles.
  • ingestEvent can return a new unmapped_foreign outcome, so an exhaustive switch over its result needs a case for it.

The third needs a look rather than a compile: if any Stripe subscription you manage carries more than one item — an add-on, a seat line, a metered component beside the main plan — the package now refuses it instead of projecting part of it. See below.

You can now run the evidence you were sold

The package ships four test tiers beyond the offline one. Three of them, plus the host-conformance suite, could not be run at all. Naming any of their files answered No test files found and exited 1 — not a skip, not a failure, an answer indistinguishable from a broken suite. Eleven passing host-conformance tests shipped that no buyer could reach, and the install guide told your agent to run four tiers it could run one of.

The cause was a single line of test configuration, and the same mistake is described in the 1.0.1 notes as a defect that was fixed — for one tier. The other four were left in the state that note calls broken.

npm test                  offline
npm run test:conformance  host contracts — no credentials needed
npm run test:db           concurrency — needs a Postgres server
npm run test:sandbox      Stripe sandbox
npm run test:clock        Stripe Test Clock
npm run test:install      reference installs

A test now fails if any tier loses its entry point again.

A subscription with two items was projected as though it had one

The code read the first item and ignored the rest, underneath a comment claiming it checked. It did not check, no test covered it, and no specification mentioned the rule at all.

The consequence is a billing one: the item id the package stores is the one a later upgrade rewrites, so changing plan would have changed the base item's price and left the add-on billing at its old one, while the customer was told their plan had changed. Stripe does not guarantee the order of the items either, so which one won could differ between two reads.

One item per subscription is now a stated boundary, listed in what the package does not cover, and enforced with an error naming what it found. If your product needs add-ons, this release tells you so at the boundary rather than after the first upgrade.

A redirect check that could be deleted by accident

The rule that rejects control characters in a return URL was written as two invisible bytes rather than as the escape sequences they should have been. It worked. But no test covered it, and any editor or formatter that tidies control characters on save would have silently removed it, leaving 38 of 38 tests passing. The same encoding made the whole file read as binary to search tools, so the module implementing the redirect rules was invisible to anyone grepping for them.

Written properly now, with tests that fail if it goes.

An alert with no way to act on it

When a dispute or refund arrives before the invoice it belongs to, the package retries the correlation a bounded number of times and then marks the event unmapped_foreign — the one state that is meant to reach an operator.

It was also the one state with no exit. The retry sweep stops selecting the row, and the obvious response — fix the correlation, then resend the event from the Stripe dashboard — did nothing at all, while reporting success. The troubleshooting guide named the state and offered no remedy.

A resent event is now re-evaluated and recovered, and the runbook has the procedure.

Configuration was documented where nobody was told to look

All seven environment variables were declared completely — and only in two files the install guide never mentions. There was no .env.example. Two of the seven are security controls: the allowlist every redirect is built from, and the check that stops a preview deployment acting on live billing events. An agent following the documented path reached the end without a configuration contract and invented names.

The install guide now carries the table, .env.example ships, and a test fails if the three ever disagree.

Also

The release gate now checks the locked refusal codes it always said it checked. The daily reconciliation index is keyed on the column the sweep actually orders by. The published throughput figures are derived from the constants rather than written next to them.

And the front page no longer claims a test count. It said 394; the suite had 403. Nothing could have caught that, and a wrong number in the first paragraph of something you paid for is worse than no number.

Checksum

production-stripe-billing-1.1.0.zip
sha256  767eb74c71980074e4dae4a9746e702cf89120e9ff8b8278a5535cb4c2a807e5
bytes   421046

Upgrading

Replace the folder, then deal with the three changes above — two the compiler finds, one you check by hand. Re-run the acceptance suite afterwards, and this time run the other tiers too; that is the point of the release.

No migration is required by any of this. migrations/0003_billing_sweep_index.sql is new but optional: it re-keys the daily reconciliation index and refuses to run on a table above 50,000 rows, printing the CREATE INDEX CONCURRENTLY statement to run yourself instead.

1.0.3

The tests you run and the tests we ran now use the same database.

Superseded before release. 1.0.3 was built and never published, so there is no 1.0.3 download and nobody is running it. Everything described below shipped in 1.1.0 instead, which is the upgrade from 1.0.2. The entry stays because the change is real and the reasoning is worth reading — removing it would leave a gap between 1.0.2 and 1.1.0 with no explanation.

What you need to do: nothing. No specification, migration, domain rule or host contract changed. An integration built from any earlier 1.0.x is correct against 1.0.3.

Three PostgreSQL versions, none of them agreeing

The package's offline suite runs against a real PostgreSQL compiled to WebAssembly, so that npm install && npm test works on a laptop with no database to provision. That engine is pinned in the package — and the pin said one thing while the evidence said another.

Running the suite as you would, inside the package, gave PostgreSQL 16.4. The acceptance evidence behind every completed requirement was produced on PostgreSQL 18.3, because running the suite from our own repository resolved our copy of the engine rather than the package's. Production, meanwhile, was 17.6.

Nothing is known to have been wrong because of it. The problem is that nothing would have said so. SQL that behaves differently across majors gives a green suite to whoever shipped it and a red one to whoever bought it, and no version is recorded anywhere to explain the difference — leaving the failure with the person least equipped to diagnose it.

Both sides are now pinned to the same engine, PostgreSQL 17.5, exactly rather than as a range, and tests/offline/engine-version.test.ts fails if the suite ever runs on a different major or the pin is loosened.

The package also ships a package-lock.json now, so npm ci reproduces the whole test tree rather than only the database. Same argument one step further. These are test-only dependencies — installing the capability into your application does not install them.

What is not claimed

No support tier is stated for a server version, because a tier here means pre-flight refuses to install, and pre-flight does not check your server version. The narrower, true statement is published instead: the shipped SQL uses no feature newer than PostgreSQL 13, and the test evidence covers 17.5.

1.0.2

An independent audit of the 1.0.1 archive, and the findings acted on.

A shipped query that no test executed

ENTITLEMENT_INPUT_SQL is the entitlement read path — the query your application runs on every authorisation check. It had exactly one reference in the package: its own definition. The audit proved the consequence by editing it to select a column that does not exist; all 394 shipped tests still passed. SWEEP_POPULATION_SQL had the same defect.

Both are now executed against a real engine by tests/offline/sql-constants.test.ts. With it in place the same mutation fails two assertions.

Two preflight checks that passed when they could not tell

The dedicated-branch check compared two strings that defaulted to "unknown" and "main" when git was unavailable. They are not equal, so the check passed, reporting On unknown — a safety probe about where your agent is working, announcing that it does not know, and calling that satisfied. It now asks for a decision.

The concurrency check was satisfied by the presence of a test runner. A runner establishes nothing about running two simultaneous transactions, and the package's own harness documents that the in-process engine cannot. It now requires a configured Postgres server.

A file path that did not exist

agent/INSTALL.md told your agent to run tests/offline/concurrency.test.ts. The file ships as concurrency.db.test.ts. Cited wrongly in five places, and a migration pointed at the wrong directory for its bindings. All corrected, and tests/offline/cited-paths.test.ts now fails if any instruction cites a file that is not there.

Verify what you received

QUICKSTART.md now opens with a checksum step, the confirmation email carries the hash, and it is published below. The package treats a Stripe webhook as untrusted until its signature verifies; the archive that installs it deserved the same and did not have it.

Checksum

production-stripe-billing-1.0.2.zip
sha256  215f56af423d526a4b0ec30434831116941d6ca60c1aec3385be0583ff54ab71
bytes   373163

Upgrading

Replace the folder and re-run the acceptance suite. No migration, no code change on your side. If you run preflight again, expect the two checks above to be stricter than they were — that is the point of them.

1.0.1

Three test tiers that could not run now run, and the release gate is harder to satisfy without actually proving anything.

Nothing in the billing specification changed. No migration, no domain rule, no host contract, no acceptance criterion. If you have installed 1.0.0 there is nothing to do: your implementation is unaffected. This release is worth taking for the tooling, not for the behaviour.

Tests that could not be collected

Both reference-install suites imported an extension that does not exist in the bundled Postgres engine. The import threw at module load — before the skip condition could be evaluated — so neither file could even be collected. Neither host needed the extension. They run now.

A test tier that reported nothing

npm run test:db pointed at a file the default configuration excluded, so it collected nothing and exited 1: the concurrency tier could not be run from the package at all. It has its own configuration now, and writes a result the gate reads.

A gate that certified a variable

The concurrency blocker asked whether a database connection string was set. Exporting one was therefore enough to clear a blocker nobody had satisfied, and a run in which every assertion skipped cleared it just as well. It now reads the run's own result and fails if the suite skipped, failed, or never loaded — and says which, because a suite that never loaded reports zero failures and "0 failed" reads like a pass.

Licence

LICENSE.md now quotes udria-b2b-terms-2026-09-23-v1.1. Orders made under v1.0 remain governed by v1.0, which is published at https://udria.co/terms/archive/udria-b2b-terms-2026-09-15-v1.0.

Checksum

production-stripe-billing-1.0.1.zip
sha256  92a2596780c7349bf230666ae7fe697168f37ce16098f3740e6056261c4d6432
bytes   366061

Check the archive before you install it: shasum -a 256 production-stripe-billing-1.0.1.zip. If it does not match, stop and email us. The build is deterministic — identical source produces identical bytes — so this number identifies the release rather than one machine's copy of it.

Upgrading

Replace the folder and re-run the acceptance suite. There is no migration and no code change on your side.

1.0.0

The first public release of Production Stripe Billing.

What you get

  • The billing domain: plan changes with preview-and-apply parity, cancellation and reactivation, scheduled downgrades, trials, dunning and recovery.
  • A preflight that refuses to install into a stack it does not support, rather than installing something that half works.
  • Host contracts for identity, billing authorisation, persistence, migration, scheduling and notification — proven against two real open-source codebases rather than against a sample app written to pass.

Proven against real Stripe

Acceptance tests run against a real Stripe sandbox and against Stripe Test Clocks, including a subscription driven through trial, a failed first invoice, the grace window and recovery.

Checksum

production-stripe-billing-1.0.0.zip
sha256  c122eedc5b06ebbd21461eb5c605ad02b1e866d724677ee4da16bf8ff9d6118a
bytes   361069

Upgrading

Nothing to upgrade from. This is the first release.

See also our compatibility page and our terms of sale and licence.