Architecture Decision Records
This directory holds Hardcount’s Architecture Decision Records (ADRs) — short docs capturing one architectural decision each. Format follows Michael Nygard’s 2011 convention: Title / Date / Status / Context / Decision / Alternatives / Consequences. Every ADR also opens with a short Summary, so a reader learns the current rule and status without reading the whole record.
ADRs are written when a decision is worth re-litigating later — either because reasonable alternatives exist, because the constraints behind the choice will likely shift, or because the decision shapes multiple downstream design choices. They are not prose summaries of every change; routine refactors and bug fixes belong in commit messages + CHANGELOG.md, not here.
Conventions
Section titled “Conventions”- Numbering is sequential; never re-used. Superseded ADRs stay in
the directory with their
**Status:** Superseded by ADR-XXXline — the historical context is the whole point. - Status is one of:
Proposed,Accepted,Superseded by ADR-XXX,Deprecated. Amendments to an accepted ADR are noted in the status line (e.g.Accepted (amended YYYY-MM-DD)); the reason goes in the amendment section, and the Summary is updated to the current rule. - Date is when the ADR was first written. Amendment dates go in the status line and inline section headers.
- Multi-platform claims (“Stripe-parity”, “industry standard”) must quote verified source lines from at least 2-4 reference platforms. Single-platform spot-checks aren’t research.
- New ADRs start from ADR-NNN: Short title that states the decision.
- Deferred work an ADR deliberately scopes out — with a written revisit trigger in its Consequences — gets a thin pointer row in Open follow-ups below. The rationale stays in the ADR (no duplication → nothing to drift); the table only makes open deferrals discoverable in one place. Remove the row when the follow-up ships.
Each row links to the full record. Browse by Topic, or search this page for a term from the glossary.
| # | Date | Status | Topic | Decision |
|---|---|---|---|---|
| 001 | 2026-04-15 | Accepted | Payments | Charge through Stripe PaymentIntents only; Hardcount owns invoices, dunning and payment status |
| 002 | 2026-04-15 | Accepted | Platform & ops | Each domain package owns its store, service and handler; peer domains never import each other |
| 003 | 2026-04-15 | Accepted | Auth & security | Every table carries tenant_id; Postgres Row-Level Security enforces tenant isolation |
| 004 | 2026-04-15 | Accepted | Credits | Append-only credit ledger: each entry carries the running balance; no entry’s amount is ever edited |
| 005 | 2026-04-15 | Accepted (amended); ADR-130 makes package prices decimal too (built in #1381) | Billing | Store and compute money as integer cents; per-unit rates later became decimals (ADR-045) |
| 006 | 2026-04-15 | Accepted (amended) | Platform & ops | Run billing and dunning on an in-process ticker; one replica runs each loop (ADR-114) |
| 007 | 2026-04-29 | Superseded by ADR-011 | Auth & security | The dashboard logs in with a pasted API key kept in browser storage |
| 008 | 2026-04-29 | Superseded by ADR-011 | Auth & security | Dashboard sessions are httpOnly cookies minted from a validated API key |
| 009 | 2026-04-30 | Accepted | Invoices | Every invoice payload carries one server-computed attention field: severity, reason, actions |
| 010 | 2026-05-01 | Accepted (amended) | Timezones | Store and send instants in UTC; show dates in the tenant’s display timezone |
| 011 | 2026-05-01 | Accepted (amended) | Auth & security | Dashboard signs in with email and password; API keys serve only SDK and curl callers |
| 012 | 2026-05-01 | Accepted (amended) | Billing | Billing periods start and end at tenant-timezone midnight; a day is the smallest unit |
| 013 | 2026-05-01 | Accepted | Invoices | Attention reports a missing payment method apart from an invoice that is awaiting payment |
| 014 | 2026-05-02 | Accepted (amended) | Auth & security | When SSO is needed, embed OIDC and SAML libraries in-process; keep passwords homegrown |
| 015 | 2026-05-04 | Accepted | Test clocks & simulated time | Advancing a test clock returns at once; a background worker runs the catch-up |
| 016 | 2026-05-04 | Superseded by ADR-086 | Test clocks & simulated time | Deleting a test clock soft-deletes it and cancels its pinned subscriptions |
| 017 | 2026-05-04 | Accepted | Tax | A background job retries failed tax calculation and finalizes the invoice when it succeeds |
| 018 | 2026-05-04 | Accepted (amended) | Test clocks & simulated time | A failed test-clock advance stores its reason and can be retried |
| 019 | 2026-05-04 | Accepted | Tax | Connecting Stripe retries invoices stuck on a missing or unauthorised tax provider |
| 020 | 2026-05-04 | Accepted (partly superseded) | Invoices | The server drops redundant provider rows from the invoice timeline and shows the charged card |
| 021 | 2026-05-03 | Accepted (amended) | Payments | Paying a hosted invoice saves the card to the customer for later off-session charges |
| 022 | 2026-05-04 | Accepted | Payments | Stripe Checkout returns the operator to the page they started from, not a fixed URL |
| 023 | 2026-05-05 | Accepted (amended) | Payments | After a decline, one banner and one email per moment; no email during on-page payment |
| 024 | 2026-05-05 | Accepted | Invoices | Keep building the timeline from several sources; add new primitives only on a named trigger |
| 025 | 2026-05-05 | Accepted | Invoices | Attention keeps Hardcount’s own explanation apart from the raw text a provider returned |
| 026 | 2026-05-05 | Accepted (amended) | Platform & ops | One function turns errors into HTTP responses; only typed, safe messages pass through |
| 027 | 2026-05-05 | Accepted | Test clocks & simulated time | Test clocks attach to the customer; subscriptions inherit the customer’s clock |
| 028 | 2026-05-05 | Accepted | Billing | Bill every due period in one call; cron and test-clock catch-up bill separate subscriptions |
| 029 | 2026-05-08 | Accepted | Test clocks & simulated time | Every time-driven job has separate wall-clock and per-test-clock flows |
| 030 | 2026-05-08 | Accepted | Test clocks & simulated time | Every action on a clock-pinned entity runs on the clock’s simulated time |
| 031 | 2026-05-14 | Accepted (amended); ADR-130 changes the default timing (built in #1386) | Pricing & usage | Each plan bills its base fee in advance or arrears; usage always bills in arrears |
| 032 | 2026-05-14 | Accepted (amended) | Integrations | A token-protected public JSON endpoint shows one customer’s costs, with sanitized fields |
| 033 | 2026-05-14 | Accepted (amended) | Integrations | LiteLLM pushes each call’s spend to a Hardcount endpoint, which records it as usage events |
| 034 | 2026-05-15 | Accepted; ADR-130 removes the meter lock (built in #1386) and changes the base-fee lock (not built yet) | Pricing & usage | A plan’s billing fields freeze once a live subscription uses it; names stay editable |
| 035 | 2026-05-16 | Accepted | Test clocks & simulated time | During test-clock catch-up, each record carries the simulated time its event happened |
| 036 | 2026-05-16 | Accepted (amended) | Dunning | A tenant keeps several dunning policies, one default, and can assign a policy per customer |
| 037 | 2026-05-18 | Accepted (amended) | Subscriptions | Shared helpers compute the first period after a trial or activation; trial changes are atomic |
| 038 | 2026-05-24 | Accepted | Credits | A credit note splits its amount across refund, credit balance and out-of-band channels |
| 039 | 2026-05-30 | Accepted | Product scope | Remove coupons entirely before launch |
| 040 | 2026-05-30 | Accepted (amended) | Platform & ops | Webhooks and emails always go through the outbox; the on/off flags are removed |
| 041 | 2026-05-30 | Accepted | Tax | If tax calculation fails, block the invoice; there is no fallback to manual tax |
| 042 | 2026-05-31 | Accepted (partly superseded) | Tax | Store tax rates as NUMERIC(7,4) and prorate with a whole-day ratio |
| 043 | 2026-05-31 | Accepted | Tax | Drop the basis-point tax rate column at once; the decimal rate is the only storage |
| 044 | 2026-06-01 | Accepted; ADR-130 changes decision 4 (built in #1386) | Pricing & usage | Meter all AI tokens on one tokens meter; token type, model and provider are dimensions |
| 045 | 2026-06-01 | Accepted; amended 2026-10-24 (rate limits); ADR-130 changes the rate fields (built in #1381) | Pricing & usage | Per-unit price rates are decimals (new rates: at most 12 places, under 10^18 cents); invoice amounts stay integer cents |
| 046 | 2026-06-03 | Accepted | Tax | Manual tax rounds the document total once and spreads the cents by largest remainder |
| 047 | 2026-06-05 | Accepted | Tax | Invoices show the statutory tax rate when taxed lines share one; else the effective rate |
| 048 | 2026-06-06 | Accepted | Credits | Credit clawbacks go through a credit note, so the proportional tax is reversed too |
| 049 | 2026-06-07 | Accepted | Payments | One idempotent settle step owns each Stripe charge’s paid or failed outcome and side effects |
| 050 | 2026-06-08 | Accepted (amended) | Subscriptions | While the period invoice is unpaid, block changes that charge more; adjust ones that credit |
| 051 | 2026-06-09 | Accepted | Product scope | Remove the customer self-serve portal; it is a B2C pattern our users do not need |
| 052 | 2026-06-15 | Accepted | Tax | The tax engine decides tax status by default; manual reverse-charge or exempt overrides it |
| 053 | 2026-06-17 | Accepted | Payments | Hardcount picks the card to charge and names it on the charge; Stripe never chooses |
| 054 | 2026-06-17 | Accepted | Invoices | Show per-unit prices at full precision; one backend function picks the billed or effective rate |
| 055 | 2026-06-18 | Accepted | Billing | Anniversary billing keeps the original day of month, clamped to each month’s last day |
| 056 | 2026-06-19 | Accepted | Subscriptions | A plan swap across billing intervals restructures the cycle in one transaction |
| 057 | 2026-06-20 | Accepted | Credits | A downgrade’s clawback credit note is created in the change transaction and issued with retry |
| 058 | 2026-06-09 | Accepted | Timezones | All month and year billing date math is anchored in the tenant timezone |
| 059 | 2026-06-22 | Accepted | Payments | In-flight payments block void, offline payment and write-off (parked excepted); clawbacks wait |
| 060 | 2026-06-23 | Accepted (amended) | Dunning | Invoices with no payment method enter dunning, like declined charges |
| 061 | 2026-06-24 | Accepted (amended) | Credits | Issuing a credit note commits internal effects in one transaction; external effects retry |
| 062 | 2026-06-25 | Accepted (amended) | Platform & ops | Move the retry sweeps onto one separate in-database obligations queue; the build is deferred |
| 063 | 2026-06-28 | Accepted (amended) | Payments | Stripe webhooks decide refund status; a retry looks the refund up before creating one |
| 064 | 2026-07-02 | Accepted | Dunning | A decline starts dunning at once; a background sweep creates any missed run |
| 065 | 2026-07-02 | Accepted (amended) | Billing | The threshold scan skips ended periods, fires once per cycle and checks every subscription |
| 066 | 2026-07-02 | Accepted (amended); ADR-130 changes the rule-bucket part (built in #1376), the package clamp (removed in #1381) and the window parts (not built yet) | Billing | Threshold fires prorate a reset’s base; tiers continue across a fire that keeps the cycle |
| 067 | 2026-07-02 | Accepted (amended) | Subscriptions | Archiving a customer is refused while a subscription still bills; it never cancels anything |
| 068 | 2026-07-02 | Accepted (amended) | Payments | One open checkout session per invoice; hosted payment and Hardcount charges exclude each other |
| 069 | 2026-07-02 | Accepted | Subscriptions | Cancelling a trial is free at trial end; SQL guards enforce it at every activation |
| 070 | 2026-07-02 | Accepted (amended); ADR-130 changes decisions 1, 4 and 5 (built in #1386) | Pricing & usage | Customer price overrides follow the rule across versions; a period keeps its opening price; a meter pricing edit that would reprice usage is refused while a subscription uses the meter |
| 071 | 2026-07-03 | Accepted (amended) | Credits | An expired credit grant is fully retired in the same transaction as its expiry entry, once no period invoice still owed can draw on it |
| 072 | 2026-07-03 | Accepted (amended) | Platform & ops | Outbox workers claim rows with a time-limited lease; budgets and lease lengths are derived |
| 073 | 2026-07-03 | Accepted (amended) | Platform & ops | One bootstrap path creates a tenant in one transaction; migrations run one at a time |
| 074 | 2026-07-04 | Superseded by ADR-077 | Timezones | Each subscription snapshots its billing timezone at creation |
| 075 | 2026-07-04 | Accepted | Timezones | The process runs in UTC, so API timestamps are the same on any host |
| 076 | 2026-07-04 | Accepted | Timezones | A Go test and typed dashboard helpers make every shown date name its timezone |
| 077 | 2026-07-04 | Accepted | Timezones | One org-level billing timezone; subscriptions have none, and each invoice keeps its issue zone |
| 078 | 2026-07-05 | Accepted (amended) | Credits | A prepaid commit is a credit grant funded at invoice finalize and retired on void |
| 079 | 2026-07-05 | Accepted (amended) | Pricing & usage | Stamp each usage event’s provider cost at ingest; report usage margin per customer |
| 080 | 2026-07-06 | Accepted | Credits | Refund unused paid commit credits at the price paid per credit, in one transaction |
| 081 | 2026-07-06 | Accepted | Auth & security | Invite teammates by single-use email link; removal ends sessions; roles are not enforced |
| 082 | 2026-07-06 | Accepted (amended) | Platform & ops | Up to 10 extra addresses are CC’d on six email types, never on credential emails |
| 083 | 2026-07-07 | Superseded by ADR-085 | Pricing & usage | A recipe reuses an existing plan or meter only if its money settings match |
| 085 | 2026-07-08 | Accepted (amended); ADR-130 removes rule adoption (built in #1386) and step 4 (built in #1389) | Pricing & usage | Applying a recipe only adds: a first apply creates a new plan; repeats create nothing |
| 086 | 2026-07-09 | Accepted (amended) | Test clocks & simulated time | Money sweeps skip simulated invoices; deleting a clock deletes its data except the audit log |
| 087 | 2026-07-11 | Superseded by ADR-116 | Payments | One method collects payment after every engine finalize; each site keeps its own checks |
| 088 | 2026-07-11 | Accepted (amended) | Credits | The credit balance applies to every invoice when it is collected, and again on each later attempt; the card is charged the remainder |
| 089 | 2026-07-13 | Accepted | Audit | A failed audit write never changes the API response; it is logged and counted |
| 090 | 2026-07-13 | Accepted | Audit | Audit rows should commit in the business transaction; most writers still write after commit |
| 091 | 2026-07-14 | Accepted | Timezones | Changing the org timezone never bills a full fee for the short period it creates |
| 092 | 2026-07-14 | Proposed | Timezones | Split the billing timezone from the display timezone; deferred until a partner needs it |
| 093 | 2026-07-16 | Accepted | Auth & security | State-changing cookie requests must come from the same site, checked by origin headers |
| 094 | 2026-07-17 | Accepted (amended) | Auth & security | No login lockout. Repeated wrong passwords wait 1 to 15 minutes, per address and email and per email. A browser that signed in before is not slowed by other people’s guesses (shipped 2026-10-18) |
| 095 | 2026-07-17 | Accepted (amended) | Auth & security | TOTP MFA is designed, not built, and waits for a user or security-review ask; the offline breached-password check and the always-Secure production cookie shipped 2026-10-18 |
| 096 | 2026-07-18 | Accepted (amended); ADR-130 removes the attach and meter-in-use guards (built in #1386) | Pricing & usage | Plans attach only priced meters; a billing meter’s last price cannot be removed |
| 097 | 2026-07-18 | Accepted (amended) | Subscriptions | A scheduled cancel that falls mid-period fires as an immediate cancel at that time |
| 098 | 2026-07-19 | Accepted | Integrations | Ingest Postmark delivery, bounce and spam-complaint webhooks; ignore opens and clicks |
| 099 | 2026-07-21 | Accepted (amended) | Test clocks & simulated time | Say “Simulated” once per scope: one page banner, or one chip per list row |
| 100 | 2026-07-26 | Accepted (amended); ADR-130 removes G1, G2, G2b and G4 (built in #1386) | Pricing & usage | Reject mismatched currencies at every write; the engine never converts |
| 101 | 2026-07-27 | Accepted | Subscriptions | Store each item’s billable date ranges on change; billing intersects them with the period |
| 102 | 2026-07-28 | Accepted (partly superseded, amended) | Payments | Record each charge attempt as a row, updated by PaymentIntent id as Stripe reports |
| 103 | 2026-07-28 | Accepted | Payments | The invoice timeline shows payments only from charge-attempt rows, not from the webhook table |
| 104 | 2026-07-29 | Accepted (amended) | Test clocks & simulated time | One invoice timeline ordered by the entity’s own clock; the real time shows beneath |
| 105 | 2026-07-30 | Accepted (amended) | Payments | Seed the Stripe idempotency key from a per-invoice attempt counter, not updated_at |
| 106 | 2026-07-30 | Superseded by 127 | Payments | Record each charge before calling Stripe, so an unnamed charge can be replayed safely |
| 107 | 2026-07-31 | Accepted (amended) | Payments | An unclear charge parks the invoice; Stripe’s list of the customer’s payments resolves it within a day |
| 108 | 2026-08-02 | Superseded by 127 | Payments | Resolve unknown charges by searching Stripe, and act only on a payment actually found |
| 109 | 2026-08-05 | Accepted (amended) | Invoices | Issued documents print bill-to and supplier details from a copy taken at issue |
| 110 | 2026-08-05 | Accepted (partly superseded) | Invoices | Customers cannot pay a written-off invoice online; its public pages point to support |
| 111 | 2026-08-05 | Accepted | Tax | Writing off an invoice does not reverse its tax; voids and credit notes still do |
| 112 | 2026-08-05 | Accepted (amended) | Dunning | When dunning ends, decide the subscription’s outcome and the invoice’s outcome separately |
| 113 | 2026-08-06 | Accepted | Invoices | Nothing charges a written-off invoice; only a recorded payment or a found charge settles it |
| 114 | 2026-08-30 | Accepted (partly superseded) | Platform & ops | Leader election is one database row per job, with a fencing token on every claim |
| 115 | 2026-08-30 | Accepted | Billing | Every period write locks the subscription and rechecks its period snapshot first |
| 116 | 2026-10-08 | Accepted (amended) | Payments | Finalize queues the invoice in the same write; one collector charges every queued invoice |
| 117 | 2026-10-11 | Proposed | Payments | One PaymentIntent per invoice considered and deferred; Hardcount keeps one per charge attempt |
| 118 | 2026-10-12 | Accepted (amended) | Credits | Credit for unused time on cancel or plan swap is written in the same transaction |
| 119 | 2026-10-12 | Accepted | Billing | Billing failures are saved on the subscription; retries back off and park after eight |
| 120 | 2026-10-14 | Accepted (amended); ADR-130 changed the after_close rule and counter (built in PR-2 and PR-6) | Billing | Invoices are built after a grace period (default 1h); later usage is not billed automatically |
| 121 | 2026-10-16 | Accepted | Pricing & usage | The customer page’s Activity and Margin cards open on the current billing period |
| 122 | 2026-10-17 | Accepted (amended 2026-10-27) | Pricing & usage | LiteLLM usage for an unknown customer or meter is held until an operator releases it |
| 123 | 2026-10-17 | Accepted; amended 2026-10-26: recipes create no policy (ADR-130) | Dunning | New accounts start with a default dunning policy that retries three times and changes nothing |
| 124 | 2026-10-17 | Superseded by ADR-125 | Platform & ops | Run migrations with plain golang-migrate and a 5-second lock limit per file; never built |
| 125 | 2026-10-18 | Accepted (amended 2026-10-22, 2026-10-28) | Platform & ops | From v0.9.0, run migrations with goose from one baseline; v0.9.0 installs fresh only |
| 126 | 2026-10-18 | Accepted | Dunning | After a hard decline, dunning charges that card no more for that invoice until another card is the default |
| 127 | 2026-10-19 | Accepted (amended) | Payments | A charge whose result was lost or unclear blocks every new charge and irreversible action until a sweep has asked Stripe |
| 128 | 2026-10-19 | Accepted | Test clocks & simulated time | Every activity line has one wording, names who acted and follows one clock rule; the backend builds the words |
| 129 | 2026-10-23 | Accepted | Credits | Each credit ledger row stores its currency; credit pays only invoices in it, never converted; a manual invoice uses the customer’s currency |
| 130 | 2026-10-23 | Accepted (lock order, one rater, one aggregation per meter, price shapes, plan prices, catalog-only recipes and ingest’s unbilled statuses shipped; PR-7 cut, PR-8 not built yet) | Pricing & usage | Usage prices are rows on the plan; the most specific price wins; an edit on a used plan applies from each subscriber’s next period |
| 131 | 2026-10-24 | Accepted | Integrations | A webhook event’s data is the resource it is about, as the API returns it, plus the few facts the resource lacks; one resource per family |
ℹ️ ADR-084 was never used. No file has ever carried that number (verified across every ref). It is a skipped number, not a lost decision.
⚠️ This index sat frozen at 082 for a month while ADRs 083–112 shipped — 28 decisions, including the whole written-off-invoice arc (110/111/112), were undiscoverable here. Backfilled 2026-08-05; dates for the 20 whose
**Status:**line carried no date come from each file’s first commit. Nothing enforces this table, so it rots silently: add the row in the same PR as the ADR.
ℹ️ ADR-058 was renumbered from a duplicate ADR-050 (2026-06-21). Two concurrent sessions had each taken
050— the same hazard the migration-numbering rule guards against (pick the next number from origin/main, not a local branch). The earlier-dated 050 (unpaid-source-proration, 2026-06-08) kept the number; the later (date-math, 2026-06-09) became 058 and all its references were updated. The out-of-date-order number is expected for a renumber.
Open follow-ups (deferred)
Section titled “Open follow-ups (deferred)”Work an ADR deliberately scoped out, as thin pointers — the authoritative
rationale and revisit trigger live in each ADR’s Consequences/Deferred section;
this table only makes the open items discoverable in one place. Remove a row when
its follow-up ships. (The first four share one shape: a post-commit side-effect /
external call not yet in-tx or idempotent. None is a regression; each is guarded today and
gated on a named trigger — see feedback_pre_launch_scoping. The rest are activity
lines that ADR-128 left out.)
| Follow-up | ADR | Code site | Revisit trigger |
|---|---|---|---|
Stale-deferred-draft alarm — Part B (ADR-059) defers an automated clawback against an in-flight source until the charge settles. If the charge never settles (a wedged requires_action PI nobody authenticates/cancels), the deferred draft waits unissued. It is not lost (durably captured, auto-issues on settle) and a wedged payment is independently visible (stuck processing invoice, tenant unpaid), so this is an observability gap, not a correctness one: surface a draft deferred > N days so an operator can cancel/await the PI (which auto-resolves the clawback). |
059 §Deferred | internal/creditnote/service.go · RetryPendingClawbackIssue |
operability hardening / first ACH-SEPA design partner |
amount_paid edge — Part C: record from captured — MarkPaid records amount_paid = amount_due at settle. Under Hardcount’s PaymentIntent-only full-capture model this equals the captured amount, so it is not reachable today (Hardcount exposes no partial/manual-capture flow). If partial capture is ever added, record from the PI’s amount_received (the processor’s captured amount) instead. |
059 §Consequences | internal/invoice/postgres.go · MarkPaid |
partial-capture support |
Per-customer balance_low threshold — credit.balance_low fires only on the single tenant-level threshold; consumers cannot implement per-customer low alerts from it (falsified 2026-07-06 — the design doc’s consumer-side mitigation was impossible). Workaround: smallest-customer tenant threshold + always-per-customer balance_depleted + polling GET /v1/credits/grants/{id}. Since ADR-129 the one threshold applies in each currency’s own units. |
078 §D8 (amended), 129 | internal/credit/postgres.go · emitBalanceCrossings |
first DP with heterogeneous commit sizes asking for per-customer low alerts, or a tenant asking for a threshold per currency |
DispatchTx seam — atomic lifecycle-event emission (remaining perimeter) — the subscription lifecycle subset SHIPPED 2026-07-05: subscription.created / .activated / .canceled / .trial_ended are enqueued IN their transition txs at the store level (subscription.PostgresStore.SetOutboxEnqueuer, one emit site per transition covering the operator API + engine schedule paths + trial sweeps; the ~10 scattered post-commit dispatch sites were deleted). The settlement money events (invoice.paid, payment.succeeded) were already in-tx. dunning.started joined them 2026-10-08 (ADR-116: dunning.Store.StartRun writes run + timeline event + webhook in one tx). Remaining: the ~12 other notification events (item/pending-change/collection/dunning/invoice.finalized/voided etc.) still dispatch post-commit — a dropped enqueue leaves no row anywhere and the loss is silent (there is no consumer-reconciliation mechanism). Per-service dispatchEvent ERROR-logs failures. |
dual-write audit (no ADR) + 2026-07-05 reassessment | internal/subscription/postgres.go · enqueueLifecycle (shipped) / internal/domain/webhook_outbound.go · EventDispatcher (rest) |
first DP integration that consumes a non-subscription lifecycle event |
| A customer-level activity timeline — customer events (profile, cards, setup links, credit grants) show only in the audit log. | 128 §Consequences | — | an operator asks “what happened to this customer” and the audit log filter is not enough |
| An activity line when the final dunning action fails — the run retries it every 24 hours while the invoice timeline stays quiet; the Dunning badge says “Final action failed”. | 128 §Consequences | internal/dunning/service.go |
an operator misses a stuck final action, or asks why an invoice shows no change |
| An activity line when a subscription’s billing run fails — the page banner shows the current failure, but past failures have no line. | 128 §Consequences | internal/subscription/handler.go · describeSubscriptionAction |
an operator needs the history of the failures, not just the current state |
Resolved follow-ups
Section titled “Resolved follow-ups”- Clawback post-flip partial-issue window: resolved by ADR-061 (#313). See 057 and 061.
- Cross-interval swap refund lost or double-credited on crash-retry: resolved 2026-07-05 (#381). See 056.
SettleFailedevent, email and dunning in-tx, plus dunning recovery: resolved. See 064.- Commit credit-note retire leg: resolved 2026-07-06. See 080.
Writing a new ADR
Section titled “Writing a new ADR”cp TEMPLATE.md NNN-short-slug.mdPick NNN as the next sequential number — check ls docs/adr/, NOT
your local branch (per feedback_migration_numbering: numbering is
chosen from origin/main to avoid duplicates that only fail at
integration-test time).