From 86ad21db795b78f61f8a0aee222b6321ab45e7d7 Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 14 May 2026 01:33:53 +0000 Subject: [PATCH] docs(license): PR 2 deploy + operator instructions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADMIN.md gains a "Running a Gumroad webhook" section: how the URL secret works, how to add a SKU to products.yaml, how to inspect gumroad_events (recent activity + failures-only queries), how to replay a failed delivery, and how to test without buyers via Gumroad's "Send Test Ping" button. The deployed-vs-queued matrix flips Gumroad + Postmark to "code merged, deploy pending" so it's clear the bits exist on main but the live box still runs PR 1. SETUP-LICENSE-SERVER.md §3 commits the eventual compose.yml shape with PR 2 environment + secrets lines included but commented out, ready to uncomment at deploy time. The §3 chown step already covers the new secret files because it uses `chmod 400 secrets/*` / `chown 10001:10001 secrets/*`. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/ADMIN.md | 46 ++++++++++++++++++++++++++++++++++-- docs/SETUP-LICENSE-SERVER.md | 22 ++++++++++++----- 2 files changed, 60 insertions(+), 8 deletions(-) diff --git a/docs/ADMIN.md b/docs/ADMIN.md index 6dce4cf..e0f896e 100644 --- a/docs/ADMIN.md +++ b/docs/ADMIN.md @@ -167,13 +167,55 @@ verifies against, so existing buyers continue to work. | Mint API + Postgres + auth | **Live** | | `datatools-admin` CLI (manual mints) | **Live** | | `licenses.datatools.unalogix.com/health` public | **Live** | -| Gumroad webhook receiver | **PR 2** | -| Postmark transactional email | **PR 2** | +| Gumroad webhook receiver | **PR 2 — code merged, deploy pending** | +| Postmark transactional email | **PR 2 — code merged, deploy pending** | | Buyer renewal / re-delivery portal | **PR 3** | | Cloudflare in front (DDoS / WAF) | Deferred (DNS at supercp/cPanel) | | Production signing keypair | Deferred (still using dev key) | | Automated DB backups | **Pending** — see §"Backups" | +### Running a Gumroad webhook (PR 2) + +Once PR 2 is deployed, sales fire `POST` to +`https://licenses.datatools.unalogix.com/webhooks/gumroad?secret=`. +Auth is the URL secret (Gumroad's recommended pattern). The handler +audit-logs the raw payload, mints idempotently keyed on `sale_id`, +sends the buyer their blob via Postmark, and returns 200 (always — +non-2xx would trigger 3-day retry storms). + +**Adding a new SKU:** + +1. Create the product in Gumroad and copy its `product_id`. +2. Edit `/srv/datatools-license/app/server/config/products.yaml`, + add a row under `gumroad:` with that ID + the tier you sold. +3. `cd /srv/datatools-license && docker compose restart api` — the + config is read at startup and cached. + +**Inspecting webhook activity:** + +```bash +# Recent webhook deliveries (all storefronts share this table) +ssh michael@46.225.166.142 'cd /srv/datatools-license && docker compose exec -T postgres \ + psql -U datatools_api -d datatools_licenses -c \ + "SELECT received_at, order_id, processed, error FROM gumroad_events ORDER BY received_at DESC LIMIT 20;"' + +# Failures only (replay candidates) +ssh michael@46.225.166.142 'cd /srv/datatools-license && docker compose exec -T postgres \ + psql -U datatools_api -d datatools_licenses -c \ + "SELECT id, received_at, order_id, error FROM gumroad_events WHERE processed=false ORDER BY received_at DESC;"' +``` + +**Replaying a failed webhook** (after fixing the products.yaml mapping +or whatever surfaced the error): the safest path is to ask the buyer +to re-trigger via Gumroad's "Send Test Ping" button in their order +record, *or* mint manually via `datatools-admin mint --source manual` +and add a note linking to the original `gumroad_events.id`. + +**Testing without buyers:** Gumroad's seller dashboard has a "Send +Test Ping" button. It sets `test=true` in the payload; the adapter +tags the resulting license with `notes='gumroad test ping'` so it's +trivially filterable later. + --- ## TL;DR — I just need a license for my dev machine diff --git a/docs/SETUP-LICENSE-SERVER.md b/docs/SETUP-LICENSE-SERVER.md index 1a95bdc..d60ff0c 100644 --- a/docs/SETUP-LICENSE-SERVER.md +++ b/docs/SETUP-LICENSE-SERVER.md @@ -204,13 +204,18 @@ services: DATABASE_URL: postgresql+psycopg://datatools_api@postgres:5432/datatools_licenses PG_PASSWORD_FILE: /run/secrets/pg_password DATATOOLS_ADMIN_TOKEN_FILE: /run/secrets/admin_token - # PR 2 adds: POSTMARK_TOKEN_FILE, GUMROAD_WEBHOOK_SECRET_FILE. + # PR 2 — uncomment when Postmark + Gumroad are provisioned. + # POSTMARK_TOKEN_FILE: /run/secrets/postmark_token + # GUMROAD_WEBHOOK_SECRET_FILE: /run/secrets/gumroad_secret # Production keypair (replaces in-tree dev key): set # DATATOOLS_LICENSE_PRIVKEY_FILE: /run/secrets/license_privkey # and DATATOOLS_LICENSE_PUBKEY: before shipping v1.0. secrets: - pg_password - admin_token + # PR 2: + # - postmark_token + # - gumroad_secret ports: - "127.0.0.1:8090:8000" # localhost-only; nginx is the only path in healthcheck: @@ -222,8 +227,11 @@ services: secrets: pg_password: { file: ./secrets/pg_password } admin_token: { file: ./secrets/admin_token } - # PR 2 adds: postmark_token, gumroad_secret. Production keypair - # rotation adds: license_privkey. + # PR 2: + # postmark_token: { file: ./secrets/postmark_token } + # gumroad_secret: { file: ./secrets/gumroad_secret } + # Production keypair rotation adds: + # license_privkey: { file: ./secrets/license_privkey } volumes: datatools_pg_data: @@ -244,10 +252,12 @@ openssl rand -hex 32 > secrets/pg_password # on your laptop to talk to /internal/* via the SSH tunnel. openssl rand -hex 32 > secrets/admin_token -# --- PR 2 / production-key follow-ups (skip for PR 1 bring-up) --- -# echo -n "" > secrets/postmark_token +# --- PR 2 secrets --- +# echo -n "" > secrets/postmark_token # from postmarkapp.com +# openssl rand -hex 32 > secrets/gumroad_secret # paste into Gumroad's Ping URL: ?secret= +# +# --- production-key follow-up (defer until v1.0 cutover) --- # echo -n "" > secrets/license_privkey -# openssl rand -hex 32 > secrets/gumroad_secret # Lock everything down. The numeric 10001 matches the in-container # `app` user (Dockerfile-defined), letting the API read the file