Build Mac AppDocsMenu

Features

Deliverables: what a paid order produces

The payment provider says "this order was paid". A deliverable decides what the buyer receives. site/lib/deliverables/config.ts lists them:

DeliverableThe buyer getsTypical product
licence-key (default)The provider's licence key, activated by the Mac appA paid Mac app
github-inviteRead access to a private GitHub repositoryA template, source code, a course (and the template's own selling site)

Both at once is allowed: ["licence-key", "github-invite"] sells an app with its source code.

Every paid order also gets a receipt. Every refund gets a confirmation email that says plainly what stopped working. All of it runs in the payment webhook as best-effort effects, so a slow GitHub or a mail outage never turns a settled payment into a retry (site/lib/deliverables/index.ts).

licence-key

Nothing to set up beyond the payment provider (docs/go-live.md). The provider issues the key and emails it; the thank-you page shows it; the app activates it through the site's licence routes. A refund revokes the key at the provider, and the app notices at its next weekly check.

github-invite

How it works

  1. Checkout asks for the buyer's GitHub username in a custom field keyed github_username (GITHUB_USERNAME_FIELD in config.ts).

  2. The paid webhook invites that account with read access (pull) and emails the buyer:

    • "invitation sent" when GitHub accepted it;
    • a link to /claim when the field was empty, misspelled, or GitHub refused.
  3. /claim lets the buyer name the right account later:

    • they enter their email and username;
    • if that address bought, it receives a signed, 24-hour confirmation link;
    • opening the link re-checks the purchase and sends the invitation.

    The form answers everyone identically, so it cannot reveal who bought. Typing a buyer's email gets a stranger nothing, because the link goes to the buyer. No database: the link carries its own signature (site/lib/deliverables/claim.ts).

  4. A refund removes access. Both Creem and Polar send the original checkout's custom fields with the refund. The username comes back with it, so the collaborator is removed and any unaccepted invitation withdrawn.

  5. When the site cannot finish, it tells you. An invitation refused because the token is wrong, or a refund with no username (access claimed through /claim leaves none on the order), sends an email to SITE.supportEmail saying exactly what to do by hand.

Setup

  1. In site/lib/deliverables/config.ts, add "github-invite" to DELIVERABLES.

  2. A private repository holding what you sell → GITHUB_ACCESS_REPO=owner/name.

  3. A fine-grained personal access token (GitHub → Settings → Developer settings → Fine-grained tokens):

    • Repository access: only that repository.
    • Permissions: Administration: Read and write, and nothing else. This is the permission GitHub requires to manage collaborators.

    → GITHUB_ACCESS_TOKEN. Set a reminder for its expiry: an expired token turns every sale into a "claim link" email and a note to you.

  4. CLAIM_SECRET: at least 32 random characters (openssl rand -base64 48).

  5. The checkout field, a required text field keyed exactly github_username:

    • Creem: the product's custom fields;
    • Polar: Settings → Custom Fields, then attach it to the product.
  6. Creem only: turn on licence keys for the product, even though the buyer never uses one. Creem's proof of purchase for /claim is a licence key issued to the address (hasPurchased in site/lib/payments/creem/adapter.ts).

  7. Test once, end to end, in the provider's test mode:

    • a purchase with your own GitHub username → the invitation arrives;
    • a purchase with the field left empty → the claim link → the confirmation email → the invitation;
    • a refund → your access is gone.

Limits worth knowing

  • GitHub invitations expire after seven days. A buyer who waited too long uses /claim, which sends a fresh one.
  • GitHub caps the collaborators a repository can have by plan. If you expect more buyers than your plan's cap, sell through an organization with a team instead: invite to the team (same token permission on the org). This is a change to github.ts, not included yet.
  • Removing access does not delete code a buyer already cloned. The refund email says so; the licence (LICENSE) is what governs what they may do with it.

This page is docs/deliverables.md in the repository, copied 2026-09-25.