Skip to content
Hardcount
Run it locally
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 →

Writing style for Hardcount docs

How to write docs in this repo so they are easy to read and still precise. Use it when you write a new doc and when you touch an old one. It applies to every Markdown file: README, guides, runbooks, ADRs, design docs, CHANGELOG, the release checklist and the files AI agents read (CLAUDE.md, playbooks).

In short: keep every fact, put the point first, write one idea per sentence, define words a new reader cannot know, and keep history behind a link. Checks lists the tools that enforce this.

All our readers are technical. Many read English as a second language, and most are short of time.

Reader What they need Main docs
Evaluator: an engineer or founder deciding if Hardcount fits, usually not a billing expert What it is, who it is for, how to try it, in 5 minutes README, self-host, architecture
Integrator: a developer wiring a product to the API Exact requests, status codes, copy-paste examples API docs, webhooks, integrations
Operator: an SRE running Hardcount, sometimes during an incident Symptom, then command, then how to check the fix self-host, deploy, docs/ops/
Contributor: an engineer making a first change Where the code is, the rules, and why CONTRIBUTING, architecture, ADRs
Reviewer: a senior engineer skimming for 2 minutes The result, the evidence, the reasoning ENGINEERING.md, benchmarks
AI agent working in the repo Rules it can extract fast and follow exactly CLAUDE.md, playbooks
Maintainer, months later A decision or a change, found fast ADRs, CHANGELOG

The effort a reader spends should go into the ideas, never into untangling the sentences.

  • Precise technical terms. Idempotency, at-least-once, RLS, fencing token, p99, DLQ. Never replace a precise term with a vague one. Define it instead (see rule 3).
  • Every number, command, code block, link and table value.
  • The reasoning. Engineers come for the why. Keep it; put it after the what.
  • The limits. “What this does not show” sections, negative controls and withdrawn claims build trust. Keep them, in one line where one line is enough.
  1. Point first. The first one to three lines of a doc or section say what it is, who it is for, and what the reader gets. History comes after, or behind a link.
  2. One idea per sentence. Aim for 15 to 22 words on average. A sentence over 35 words needs a reason. Most long sentences are chains of dashes, semicolons and parentheses: break the chain first.
  3. Define words a cold reader cannot know. Billing terms (dunning, proration, clawback, credit note, prepaid commit) get a short definition on first use in reader-facing docs. Project words (lane, parked, catch-up, site-set) are defined once in a glossary or replaced with plain words. Write the meaning first and keep the term in parentheses, so search and experts still find it.
  4. Keep history out of the reader’s path. PR numbers, incident stories and ADR numbers used as the reason for a rule go behind a link: “See ADR-041”, not “per ADR-041, which removed the fallback in #312”.
  1. Write global English. No idioms (“bolted on”, “gold-plate”, “hairball”). No stacked “not X, but Y” reversals. A careful reader whose first language is not English should get each sentence in one pass.
  2. Keep emphasis quiet. Bold is for scan anchors. Do not shout with ALL-CAPS or with bold in running text. Leave out defensive words (“honestly”, “this is not a lie”); the evidence makes the point.
  1. Fit the shape to the job. Tasks are numbered steps. Reference is a table. Concepts are short paragraphs, plus a diagram where it helps. Decisions follow the ADR template.
  2. No walls. A paragraph over about 120 words, or a table cell over about 40, becomes a list, a table or a sub-section.

The one-pass test. Read the sentence once, at normal speed, as a new reader would. If you had to read it twice, or hold a parenthesis in your head until the end, rewrite it.

Set on 2026-10-17. Every reader-facing page (README, guides, docs/ pages a user opens) meets it, on top of the rules above. Write for a friendly, busy person who has never heard of Hardcount. The reader should never have to decode a word.

  1. Say what the thing is in one plain line. “Hardcount is a billing system you run yourself” before any feature list.

  2. Say why it exists and who it is for. One or two lines each, near the top.

  3. Give a new idea one everyday comparison. One, not three. “The quick start is a demo car at a showroom: you drive it, you don’t build it.”

  4. Explain every term on first use, plain meaning first (rule 3): “a test clock is a pretend clock for test customers”. Then use the term the same way everywhere after.

  5. End each task step with “You see:” (rule 7 makes tasks numbered steps). After it comes the real output, the screen, or the page the reader lands on. Copy it from a real run, not from memory.

    1. Run `./quickstart.sh`.
    **You see:** a box with the dashboard address, an email, a password
    and a test API key.

On these pages, history, ADR and PR numbers, code paths and internal names all stay out of the reader’s path. One “Related” link at the end is enough. Every number, command, code block and link the reader needs stays (see Keep).

The dashboard and the pages an end customer sees use the reader’s words, not the code’s.

Do not write Write
tenant account
catchup, catch-up running what is happening: “Billing the skipped days…”
event_name or any field name what it means: “The name your app sends with each usage event, for example api_calls”
Configure … what the page is for: “Your business details, invoice defaults, tax and payment setup”

The Test clocks page intro.

Before:

A test clock freezes ‘now’ for the subscriptions you pin to it. Advance the clock to fast-forward billing without waiting for real time to pass.

After:

A test clock is a pretend clock for test customers. Create a clock, add a new customer on it, then move the clock forward to see renewals, invoices and payment retries happen in minutes.

The new text says what a test clock is, uses no undefined words (“pin”, “freezes ‘now’”), and is true: a clock holds customers, not subscriptions.

An activity line is one row on an activity card: the invoice and subscription Activity cards, the Dunning page’s recovery timeline and the customer’s Sent emails card. The backend builds every word of a line. The dashboard formats money and times and lays the row out. Use these rules when you add or change a line. The decision behind them is ADR-128.

A line has up to three parts:

● Payment failed · $24.73 10:13 AM
Card declined: insufficient funds <- problem line
Automatic · Recorded Oct 18, 8:14 PM <- meta line
  1. One event, one wording. An event has the same title on every page it appears on, and the audit log’s headline uses the same verb. Two pages never use two phrasings for one event.

  2. Title: object plus past participle, sentence case, no values.

    • “Invoice finalized”, “Payment failed”, “Credit note issued”.
    • No dashes, no brackets and no actor in the title.
    • Name another object’s number only when it is not the page’s own entity, and then link it (rule 8).
    • Two outcomes of one event share one title. The difference goes in the problem line or the meta line.
  3. Money only when the event moves money, asks for it, or cancels money owed (a void or a write-off).

    • It goes right after the title, in the document’s own currency.
    • The dashboard’s one money formatter prints it: $1,234.50, CA$12.00. Never cents, never ¢, never 12.00 CAD.
    • A title’s amount always means the same thing. A credit note row always shows the credit note’s total; the meta line splits it into its parts.
  4. Who did it, on every line, from a fixed set. The actor is the last part of the meta line, before any Recorded part:

    • a person: by <name>, or by <email> when there is no name;
    • an API key: by API key <key name>;
    • the customer: Customer, on the payment page or Customer, in the portal;
    • anything Hardcount did on its own: Automatic.

    Never system, api_key, operator, “an operator” or “by Hardcount”. The actor comes from a fact written with the row. A row with no such fact shows no actor; only legacy rows lack one.

  5. Plain words. No enum codes, table names, Go errors, meter keys or provider wrappers such as payment failed: …. Test: would a finance person who has never seen the code understand it?

    • Write decline reasons about the cardholder, not to them: “Card declined: insufficient funds”, not “Your card has insufficient funds.”
    • “Uncollectible” is the one word for an invoice written off as bad debt.
  6. One clock rule.

    • A line is dated on the entity’s calendar, at the instant the event happened. For an automatic event, that is the instant the contract names (renewal, trial end, retry slot), never the instant a clock was moved.
    • On a test clock, the meta line ends with · Recorded <real date and time>. “Recorded” is the only word for the real-world time.
    • Nothing else differs between a test-clock line and a real-clock line.
  7. Four tones, one meaning each. The words carry the meaning; the dot only repeats it.

    Tone Colour Meaning
    Neutral grey Something happened
    Success green Money arrived, or was returned as intended
    Warning amber Needs a look, or money will not be collected
    Failure red An attempt failed

    A failure or warning row has a problem line that says what went wrong or what it means, unless the title already says all of it (“Payment retries ran out”). Leave out a problem line that only repeats the title.

  8. Link what the row’s own data names: an invoice, a credit note or a subscription whose id the row carries, and only while it still exists. A deleted object stays plain text. Never link from a guess, and never to a payment-recovery run: the Dunning page cannot open on one run.

ADR. Follow ADR-NNN: Short title that states the decision. The Summary at the top says what was decided and why in two to five plain sentences. When a later change amends the decision, update the Summary’s current rule and add the amendment at the end, dated.

CHANGELOG entry. One entry per user-visible change, at most about 50 words:

- **Every dunning retry now counts against `max_retry_attempts`.** During a
leader handover one charge could go uncounted, so a run could exceed its
retry limit. No charge was doubled. ([#NNN])

A bold outcome, then who is affected and what changes for them, then any action they must take, then a link. The mechanism, the tests and the history go in the PR description or the ADR.

Checklist item (for example in the release checklist). One action and the one result to see, on one line:

- [ ] Refund: a credit note with "Refund to card" → `refund_status=succeeded`,
and the refund shows in the Stripe dashboard.

Runbook section. Symptom, Why, Check (commands), Fix, Verify. A procedure never lives inside a table cell.

Results doc (benchmark, drill). Open with the result and its conditions in one short paragraph. Then the setup, the runs, what it does not show, and any corrections. The story of how the result was reached goes in an appendix.

  • Vale lints prose against rules 2, 5 and 6 (.vale.ini, rules in .vale/styles/Hardcount/). CI comments only on lines a PR adds and never fails the build. Run vale <file> locally.
  • lychee checks that every internal link and #anchor resolves. CI fails on a broken one. Run lychee --offline --include-fragments '**/*.md' locally.
  • Clarity and truth go in separate PRs. A clarity PR changes no facts. A truth PR fixes a doc that disagrees with the code, and re-derives each claim from the code.
  • Prove a clarity PR changed no facts. Compare the numbers, code, code blocks and link targets of each changed file against the base branch, and explain every difference. Check that no link or #anchor broke.
  • Do not rewrite records. ADR bodies, old CHANGELOG entries and dated audit or benchmark records stay as written. Add a summary or a status banner on top instead.