Purchases

Backed by: @pithy-sh/payments. Scope: payments:purchases:read.

What it shows

A product is not an entitlement is why a purchase and an entitlement are separate panes — entitlements is the other. Subscriptions is the recurring subset, and reconciliation is what keeps them agreeing with the store.

The purchase log, paged: the logical product id, the rail, the rail’s own transaction id, the subject, the date, and what it granted.

What it deliberately does not

The stored provider payload. The queries behind these reads do not select it — not filtered afterwards, not selected at all.

The subject is a pair

(subjectType, subjectId) — a user, or an organization, depending on what the project bills.

Both halves, always. Nothing keeps an organization id from equaling some user’s id, so a pane that keyed on the id alone would show one holder’s plan under another’s name.

A product is not an entitlement

The row carries the logical product id you chose, not a store SKU — which is why renaming a SKU in a console never rewrites history.

And many products across many rails may grant one entitlement key, which is what the entitlements pane reads.

Idempotency you can see

UNIQUE (rail, providerTransactionId) is the anchor all three write paths rely on — a client submitting a receipt, the rail’s webhook, and a reconciliation pass.

So a repeat is a no-op rather than a second row, and a client that submits its receipt the moment a purchase completes is doing something safe: the entitlement appears immediately, and the webhook finds the row already there.

Reads landed late, and the cost is worth stating

Every read is audited

Including the ones that found nothing. Reading the record of everyone else’s transactions is itself a security-relevant action.

ESC