docs(license): PR 2 deploy + operator instructions

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) <noreply@anthropic.com>
This commit is contained in:
2026-05-14 01:33:53 +00:00
parent 2bbaba954b
commit 86ad21db79
2 changed files with 60 additions and 8 deletions

View File

@@ -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=<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

View File

@@ -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: <hex> 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 "<postmark-server-token>" > secrets/postmark_token
# --- PR 2 secrets ---
# echo -n "<postmark-server-token>" > secrets/postmark_token # from postmarkapp.com
# openssl rand -hex 32 > secrets/gumroad_secret # paste into Gumroad's Ping URL: ?secret=<this>
#
# --- production-key follow-up (defer until v1.0 cutover) ---
# echo -n "<ed25519-private-hex>" > 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