Deploy the reconciliation Workflow that keeps stored purchases agreeing with the stores, and run a pass on demand. This is the provisioning half of the payments capability.
Synopsis
pithy payments provision [--json]
pithy payments reconcile [--env <environment>] [--subject <holder>] [--rail <rail>] [--dry-run] [--json]Both subcommands need a Cloudflare account. provision deploys a Worker per managed environment; reconcile dispatches a Workflow into an already-deployed one. Neither has a local path.
Flags
| Flag | Applies to | Default | Purpose |
|---|---|---|---|
--env <environment> | reconcile | staging | Which deployed environment to run the pass in. dev is local-only and is refused by name |
--subject <holder> | reconcile | every holder | Reconcile one holder’s purchases, as user:<id> or organization:<id>. The support path: the same steps the cron runs, narrowed. An id on its own is refused — it names whichever user or organization carries it |
--rail <rail> | reconcile | every rail | Reconcile one rail: apple, google, stripe, lemonSqueezy or paddle. Parsed here, so a mistyped rail is a sentence in this terminal rather than a Workflow that burns its retry budget unwatched |
--dry-run | reconcile | false | Report the drift and write nothing |
--json | both | false | One line of machine-readable output |
provision takes no --env. It spans every managed environment in one run, deliberately: the reconcile worker is per environment, and a half-provisioned pair is the state nobody wants to reason about.
What it does
pithy add payments writes bindings and touches no Cloudflare account. provision stands up the one thing those bindings point at: the prebuilt reconcile Worker that hosts the nightly pass.
For each managed environment it checks the account once up front, deploys the Worker, and then writes that environment’s workflows binding into the app’s wrangler.jsonc. The binding cannot be written by add — wrangler requires a name and a class_name on every entry, and the deployed name is per environment.
No credential is written here, and that is not an omission. Apple’s .p8, Google’s service-account key, Stripe’s key pair, and Lemon Squeezy’s and Paddle’s API keys are taken by a human from five consoles; nothing can mint them. They go in through pithy secrets create payments-provider-credentials, and this command deploys the Worker that reads them. A provision run before the secrets are set still succeeds — the first pass is what reports the missing rail.
Where each rail’s credentials come from
payments-provider-credentials is one secret with a block per rail, and each block is issued by a different company. This is the console, not the integration guide.
| Rail | Console | What to take |
|---|---|---|
apple | App Store Connect | The App Store Connect API key. Download the .p8 — it is offered exactly once — and copy the key id and the issuer id from the same screen |
google | Google Cloud IAM | The service account and a JSON key for it. Grant that account access to the app in Play Console; the key itself is minted here |
stripe | Stripe dashboard | The secret key. The webhook signing secret is per endpoint, one screen across |
lemonSqueezy | Lemon Squeezy settings | The API key, and the signing secret of the webhook you point at this project |
paddle | Paddle authentication | The API key, and the notification destination’s signing secret. Not the client token — that is publishable and belongs in pithy.config.ts |
Reconciliation is what a purchase that changed at the store — a refund, a lapse or a chargeback — travels through before an entitlement stops answering yes.
Each console is per environment on its own terms: Stripe’s test mode, Paddle’s and Lemon Squeezy’s sandboxes, Apple’s and Google’s separate keys. Provision staging and production from the matching one, never the same credential twice.
These are also where a credential is replaced. Rotation is manual for this secret because none of the five returns a new value over an API — a human opens the console, mints the replacement, and records it. The store holds both versions while webhooks signed under the old one drain.
reconcile runs that same pass on demand and waits for its report. It dispatches the deployed Workflow, polls until it reaches a terminal state, and prints what it found.
Reconciliation is the repair path, never the primary one: a rising drifted count means webhooks are not arriving, which is the signal the command exists to surface.
--json
payments provision
| Key | Type | Meaning |
|---|---|---|
command | "payments provision" | The subcommand that produced the line |
environments | string[] | Every managed environment provisioned, in order. dev is local-only and never a deploy target |
payments reconcile
$ pithy payments reconcile --env staging --json
{"command":"payments reconcile","env":"staging","report":{"pages":3,"scanned":214,"unchanged":198,"drifted":4,"superseded":11,"skipped":1,"failed":0,"truncated":false,"dryRun":false}}| Key | Type | Meaning |
|---|---|---|
command | "payments reconcile" | The subcommand that produced the line |
env | string | The environment the pass ran in |
report | object | The Workflow’s own return value, passed through verbatim |
report is the deployed Workflow’s output, not the CLI’s. It crosses no schema on the way out, and it is absent from the line entirely when a completed Workflow returned nothing. What the capability returns today:
report key | Type | Meaning |
|---|---|---|
pages | number | Pages read — one durable step each |
scanned | number | Purchases examined |
unchanged | number | Purchases whose stored state already matched the store’s |
drifted | number | Purchases whose stored state disagreed. The number that matters |
superseded | number | Purchases a later period of the same subscription has replaced, settled on this pass |
skipped | number | Purchases no store could be asked about |
failed | number | Purchases a store refused to answer for. Counted, never thrown — one bad row must not end the pass |
truncated | boolean | Whether the run stopped at its page cap with more of the catalog unexamined |
dryRun | boolean | Whether the run only reported |
Errors
The capability is not configured. No Worker under apps/ composes payments.
Cloudflare credentials are missing. Raised before any client is built.
The Secrets Store id is missing. provision only — the reconcile Worker decrypts each rail’s credentials from it.
--env dev. Checked at the flag, before a single Cloudflare client exists.
--env must be one of staging, prod. Got "dev".
This deploys to a Cloudflare account, and dev is local-only. Run `pithy dev` instead.The environment has no wrangler stanza, or no DB id. Each missing value is refused rather than deploying a half-wired Worker — the purchase rows live in that database.
The environment’s secrets database does not exist. Run pithy secrets provision first.
The project has no name. Every deployed name leads with it, and it is never guessed: a wrong one dispatches into a Workflow nothing deployed.
The Workflow did not finish. reconcile polls a dispatched instance and raises when it ends errored or terminated, or is still running at the poll cap.
Examples
$ pithy payments provision
staging: reconcile worker deployed, PAYMENTS_RECONCILE bound.
prod: reconcile worker deployed, PAYMENTS_RECONCILE bound.
Set each rail's credentials with `pithy secrets create payments-provider-credentials` — nothing can mint them.
Done.Answer my subscription isn’t showing up for one person, through the steps the cron runs:
pithy payments reconcile --env prod --subject user:usr_a1b2c3
pithy payments reconcile --env staging --dry-run
pithy payments reconcile --env prod --rail stripe --json