# StableFlow OEM (bundled FormaPrint) — Automated Setup Runbook (for Claude Code)

You are an AI coding agent setting up **StableFlow OEM** — the stablecoin
payments/invoicing engine — together with its **bundled FormaPrint OEM** PDF
renderer, on the user's machine, from this distribution kit. StableFlow uses
FormaPrint to render invoice/statement PDFs; the user's StableFlow license
**includes** FormaPrint, so both come up as one stack. Follow these steps in
order. Each says whether **YOU do it autonomously** or whether it **NEEDS
THE USER**. Do not skip the safety rules.

## Safety rules (read first)

- **Never ask the user to paste secrets into the chat / shared output.**
  License keys and DB connection strings go **directly into `.env`** by the
  user (or by you writing a value the user hands you privately) — never
  echoed back.
- **Generate all internal secrets yourself** (the two JWT signing keys, two
  audit HMAC keys, the webhook signing secret) with strong randomness and
  write them straight to `.env`. **Do not print them.**
- **Mint the StableFlow→FormaPrint service token yourself** from the
  FormaPrint JWT signing key and write it into `.env`. Do not print it.
- **Never commit `.env`** or show its contents. The kit's `.gitignore`
  already excludes it — do not override that.
- Operate only in this `distribution/stableflow/` directory.
- Treat connection strings as secrets (they contain passwords).

## Step 1 — Verify Docker (YOU)

```bash
docker --version
docker compose version
docker info            # confirms the daemon is running
```

If any fail: tell the user to install Docker Desktop / start the Docker
daemon, then stop. Do not proceed without a running daemon.

## Step 2 — Collect the values only a human can provide (NEEDS THE USER)

Ask the user for these, from their Celestryll sign-up at
https://celestryll.com/get-started plus their own database. **Tell them to
place these directly into `.env` themselves** (you create `.env` in step 3
first), or to hand them to you only if comfortable — do **not** insist they
paste secrets into the chat:

1. **StableFlow license** (3 values): `STABLEFLOW__LICENSING__LICENTIA__TENANTID`,
   `…__LICENSEKEY`, `…__APIKEY`.
2. **Bundled FormaPrint license** (3 values):
   `FORMAPRINT__LICENTIA__TENANTID`, `FORMAPRINT__LICENTIA__LICENSEKEY`,
   `FORMAPRINT__LICENTIA__APIKEY`. **Emphasize these come WITH the StableFlow
   license — the user does NOT purchase FormaPrint separately.** They are a
   distinct set from the StableFlow trio above.
3. **Database connection(s):** `STABLEFLOW_CONNECTION` and
   `FORMAPRINT_CONNECTION`. Both may be the **same** database (they differ
   only by schema: `stable` vs `forma`), so one connection string can be
   reused for both — confirm with the user.
4. `STABLEFLOW__PUBLICBASEURL` — the payer-facing URL for their brand.

If the user has **no** database of their own, offer the optional bundled
`sql` service: have them uncomment it in `docker-compose.yml`, set
`MSSQL_SA_PASSWORD` and matching `STABLEFLOW_CONNECTION` /
`FORMAPRINT_CONNECTION` (`Server=sql;Database=OemShared;…`). Note this is for
evaluation; a managed, backed-up database is better for production.

## Step 3 — Create and populate `.env` (YOU + USER)

Create `.env` from the template:

```bash
cp .env.template .env
```

- **You write** the prefilled values (3a), the generated secrets (step 4),
  and the service token (step 6).
- **The user writes** (or privately hands you) the six license values, the
  connection string(s), and the public base URL from step 2. Prefer editing
  `.env` directly over echoing values.

### 3a. Confirm the prefilled values (YOU)

Ensure `.env` has:

```
STABLEFLOW__LICENSING__ENABLED=true
STABLEFLOW__LICENSING__MODE=Licentia
STABLEFLOW__LICENSING__LICENTIA__ENDPOINT=https://api.celestryll.com/licensing
STABLEFLOW__LICENSING__LICENTIA__FAILOPEN=false
FORMAPRINT__LICENTIA__ENDPOINT=https://api.celestryll.com/licensing
FORMAPRINT__LICENTIA__FAILOPEN=false
STABLEFLOW__SCHEMANAME=stable
FORMAPRINT__SCHEMANAME=forma
STABLEFLOW__INVOICEDOCUMENT__FORMAPRINT__DOCUMENTTYPE=Invoice
STABLEFLOW__INVOICEDOCUMENT__FORMAPRINT__STATEMENTDOCUMENTTYPE=TransactionStatement
```

> **Licensed vs demo (G2 — critical for boot).** With
> `STABLEFLOW__LICENSING__ENABLED=true` (licensed posture) the StableFlow API
> **requires a provider signing key** (`stableflow-provider-signing-key` + an
> offline RSA verify key) resolvable from a secret store, or it crash-loops at
> startup. If the user is **evaluating without a purchased license and has no
> provider signing key**, set `STABLEFLOW__LICENSING__ENABLED=false` — the API
> then boots permissively (license gate and signing-key requirement both
> skipped). Do not set `false` for a real licensed deployment.

Leave `STABLEFLOW__INVOICEDOCUMENT__FORMAPRINT__TENANTID` **blank** unless
the user registered invoice templates under a custom FormaPrint tenant — a
blank value uses FormaPrint's seeded demo tenant, which already has the
`Invoice` + `TransactionStatement` templates.

## Step 4 — Generate the internal secrets (YOU, autonomously)

Generate five strong random values and write them into `.env`. **Do not
print them.**

```bash
python - <<'PY'
import secrets, pathlib, re
env = pathlib.Path(".env"); text = env.read_text()
def set_key(text, key):
    val = secrets.token_urlsafe(48)          # ~64 chars, >32 bytes entropy
    if re.search(rf"(?m)^{re.escape(key)}=", text):
        return re.sub(rf"(?m)^{re.escape(key)}=.*$", f"{key}={val}", text)
    return text + f"\n{key}={val}\n"
for k in ("JWT__SIGNINGKEY","STABLEFLOW__AUDIT__HMACKEY",
          "STABLEFLOW__WEBHOOKS__SIGNINGSECRET",
          "FORMAPRINT__JWT__SIGNINGKEY","FORMAPRINT__AUDIT__HMACKEY"):
    text = set_key(text, k)
env.write_text(text)
print("Generated 5 internal secrets into .env")
PY
```

(If Python isn't available, use `openssl rand -base64 48` five times and
write each into `.env` with an editor — still without printing the values.)

Remind the user to copy these into their secret manager; losing an audit
HMAC key means past audit rows for that product can't be re-verified.

## Step 5 — Apply the database schemas (YOU, with the admin credential)

Both products' scripts are **shipped in this kit**: StableFlow's in
`./sql/stableflow/` (28 scripts → `stable`), the bundled FormaPrint's in
`./sql/formaprint/` (14 scripts → `forma`). Apply them with the bundled
**apply-schema helper** — do **not** hand-run raw `sqlcmd -i` (StableFlow's
`01_Tables.sql` creates a filtered index that needs `SET QUOTED_IDENTIFIER ON`,
which plain `sqlcmd` leaves OFF; the helper's `-I` fixes this). Run it **twice**,
as a DB admin/migrator (`sa` / owner); it creates the database if absent:

```bash
./apply-schema.sh --database OemShared --schema stable --sql-dir ./sql/stableflow \
  --server <server> --user sa --password '<sa-password>'
./apply-schema.sh --database OemShared --schema forma  --sql-dir ./sql/formaprint \
  --server <server> --user sa --password '<sa-password>'
```

```powershell
./apply-schema.ps1 -Database OemShared -Schema stable -SqlDir ./sql/stableflow `
  -Server <server> -User sa -Password '<sa-password>'
./apply-schema.ps1 -Database OemShared -Schema forma  -SqlDir ./sql/formaprint `
  -Server <server> -User sa -Password '<sa-password>'
```

Both products share one database (`OemShared`), isolated by schema. Needs
`sqlcmd` on PATH (mssql-tools18) or `--sqlcmd <path>`.

Decision points:

- **Azure SQL?** SKIP the `AppSecurity` script in **both** sets
  (StableFlow `24_AppSecurity.sql`, FormaPrint `12_AppSecurity.sql`) — they
  run `CREATE LOGIN`, which Azure SQL rejects. Move them out of their `sql`
  dir first, then create a contained user per app and grant
  `SELECT/INSERT/UPDATE/DELETE/EXECUTE` on `SCHEMA::stable` / `SCHEMA::forma`.
- **Self-hosted SQL Server?** Run all scripts. The `AppSecurity` scripts
  create the least-privilege `stableflow_app` / `formaprint_app` logins (dev
  passwords `StableFlow!App#Dev1` / `FormaPrint!App#Dev1`); set strong
  passwords and use those (**not** `sa`) in the connection strings —
  Production refuses an `sa` runtime connection at boot.
- **SQL Server Express?** Add `--demo` to the StableFlow invocation to SKIP
  `02_CDC.sql` (Express has no SQL Server Agent for the CDC capture jobs); the
  rest applies cleanly. (Compression/Partitioning also need Developer+.)
- Scripts are idempotent — safe to re-run.
- FormaPrint's `06_SeedData.sql` seeds the demo tenant's `Invoice` and
  `TransactionStatement` templates that StableFlow needs.

## Step 6 — Mint the StableFlow→FormaPrint service token (YOU, autonomously)

FormaPrint authenticates every request. StableFlow must present a bearer
token signed with `FORMAPRINT__JWT__SIGNINGKEY` (the key you generated in
step 4). Mint it and write it to
`STABLEFLOW__INVOICEDOCUMENT__FORMAPRINT__SERVICETOKEN` in `.env`. Read the
key from `.env` in-process — **do not print the key or the token.**

```bash
python - <<'PY'
import re, json, base64, hmac, hashlib, time, pathlib
p = pathlib.Path(".env"); text = p.read_text()
env = dict(re.findall(r"(?m)^([A-Za-z0-9_]+)=(.*)$", text))
key = env["FORMAPRINT__JWT__SIGNINGKEY"].encode()
b64 = lambda b: base64.urlsafe_b64encode(b).rstrip(b"=")
now = int(time.time())
hdr = b64(json.dumps({"alg":"HS256","typ":"JWT"}).encode())
pl  = b64(json.dumps({"iss":"FormaPrint","aud":"FormaPrint","scope":"service",
                      "sub":"stableflow-invoice-renderer","iat":now,"nbf":now,
                      "exp":now+365*24*3600}).encode())
sig = b64(hmac.new(key, hdr+b"."+pl, hashlib.sha256).digest())
tok = (hdr+b"."+pl+b"."+sig).decode()
k = "STABLEFLOW__INVOICEDOCUMENT__FORMAPRINT__SERVICETOKEN"
if re.search(rf"(?m)^{k}=", text):
    text = re.sub(rf"(?m)^{k}=.*$", f"{k}={tok}", text)
else:
    text += f"\n{k}={tok}\n"
p.write_text(text)
print("Service token minted (1-year expiry) and written to .env")
PY
```

## Step 7 — Bring up the stack (YOU)

```bash
docker compose pull
docker compose up -d
docker compose ps
```

Four services should be running: `stableflow-api`, `stableflow-worker`,
`formaprint-api`, `formaprint-worker`. If a service is unhealthy, read logs:
`docker compose logs <service>`.

## Step 8 — Poll health until ready (YOU)

Poll each API's liveness then readiness (readiness confirms the DB is
reachable). Ports come from `STABLEFLOW_API_PORT` (default 8080) and
`FORMAPRINT_API_PORT` (default 5000):

```bash
for base in http://localhost:8080 http://localhost:5000; do
  for i in $(seq 1 30); do
    curl -fsS $base/health/live >/dev/null 2>&1 && break
    sleep 2
  done
  curl -fsS $base/health/live
  curl -fsS $base/health/ready
done
```

Then confirm StableFlow can reach the bundled FormaPrint over the internal
network:

```bash
docker compose exec stableflow-api \
  sh -c 'wget -qO- http://formaprint-api:80/health/live || curl -fsS http://formaprint-api:80/health/live'
```

If a readiness never goes healthy, it's almost always the connection string
or an un-applied schema. Diagnose from `docker compose logs <service>`.

## Step 9 — Smoke render an invoice (YOU, autonomously)

Prove the bundle end-to-end: render the seeded FormaPrint `Invoice` template
(this exercises the FormaPrint license + rendering that StableFlow relies
on). Mint a short-lived FormaPrint token in-process and POST an invoice.
**Do not print the key or token.**

```bash
python - <<'PY'
import re, json, base64, hmac, hashlib, time, pathlib, urllib.request
env = dict(re.findall(r"(?m)^([A-Za-z0-9_]+)=(.*)$", pathlib.Path(".env").read_text()))
key = env["FORMAPRINT__JWT__SIGNINGKEY"].encode()
port = env.get("FORMAPRINT_API_PORT", "5000")
b64 = lambda b: base64.urlsafe_b64encode(b).rstrip(b"=")
now = int(time.time())
hdr = b64(json.dumps({"alg":"HS256","typ":"JWT"}).encode())
pl  = b64(json.dumps({"iss":"FormaPrint","aud":"FormaPrint","scope":"service",
                      "sub":"claude-setup-smoke","iat":now,"nbf":now,"exp":now+300}).encode())
sig = b64(hmac.new(key, hdr+b"."+pl, hashlib.sha256).digest())
token = (hdr+b"."+pl+b"."+sig).decode()
body = json.dumps({"documentType":"Invoice","outputFormat":"pdf","data":{
  "invoiceNumber":"INV-SMOKE","merchantName":"Acme Corp","productName":"StableFlow",
  "clientName":"Beta LLC","clientEmail":"ap@beta.example","issuedDate":"2026-08-04",
  "dueDate":"2026-08-18","currency":"USDC","total":250.00,
  "payUrl":"https://pay.example.com/i/INV-SMOKE","feeNote":"Includes 0.5% processing fee.",
  "lines":[{"lineNumber":1,"description":"Subscription — August","reference":"SUB-8",
            "quantity":1,"unitPrice":250.00,"amount":250.00}]}}).encode()
req = urllib.request.Request(f"http://localhost:{port}/reports/generate", data=body, method="POST",
  headers={"Content-Type":"application/json","Authorization":f"Bearer {token}",
           "X-Tenant-Id":"11111111-1111-1111-1111-111111111111"})
resp = json.load(urllib.request.urlopen(req))
print("Smoke render OK — reportId:", resp.get("reportId"))
PY
```

A `reportId` means the FormaPrint license validated and invoice rendering
works — the exact path StableFlow uses when it attaches an invoice PDF.
(`402` with a license reason → the `FORMAPRINT__LICENTIA__*` values are
wrong, go back to step 2/3. Plain `401` → the token/key don't match, re-check
step 4/6.) In production StableFlow makes this call itself on invoice email.

## Step 10 — Report success (YOU)

Summarize for the user, **without printing any secret**:

- Four Docker services running (`docker compose ps`).
- Health: StableFlow + FormaPrint live + ready OK; StableFlow reaches
  FormaPrint internally.
- Smoke render produced a `reportId`.
- StableFlow API base URL (e.g. `http://localhost:8080`) and where to change
  ports.
- Reminders: `.env` holds secrets and is git-ignored; copy the generated
  keys into their secret manager; the FormaPrint service token expires in a
  year (re-mint per step 6); pin `STABLEFLOW_VERSION` / `FORMAPRINT_VERSION`
  for reproducible upgrades.

## Autonomous vs. user-provided — quick map

| Step | You (autonomous) | Needs the user |
|---|---|---|
| 1 Verify Docker | ✅ | install/start Docker if missing |
| 2 Collect license + DB values | — | ✅ StableFlow trio, bundled FormaPrint trio, connection string(s), public URL |
| 3 Create `.env` + prefilled | ✅ create + prefilled values | ✅ writes the 6 license values + connections |
| 4 Generate 5 internal secrets | ✅ generate + write, never print | — |
| 5 Apply BOTH DB schemas | ✅ run scripts | ✅ provide admin DB credential |
| 6 Mint FormaPrint service token | ✅ mint + write, never print | — |
| 7 `docker compose up -d` | ✅ | — |
| 8 Poll health + reachability | ✅ | — |
| 9 Smoke render invoice | ✅ mint token + render | — |
| 10 Report success | ✅ | — |
