# StableFlow OEM — Distribution Kit (with bundled FormaPrint)

Run **StableFlow OEM** — the stablecoin payments and invoicing engine — on
your own infrastructure, licensed against Celestryll's Licentia authority.
StableFlow renders its branded invoice and transaction-statement PDFs with
**FormaPrint OEM**, which is **bundled** in this kit and runs alongside it.

> **Your StableFlow license includes FormaPrint** for this use. You do
> **not** buy FormaPrint separately — you receive FormaPrint license
> credentials together with your StableFlow license at sign-up. This kit
> brings up **both** products as one stack.

This kit is everything you need to bring the stack up with Docker: a
`docker-compose.yml` that pulls Celestryll's published images, an `.env` you
fill in, and the steps below.

> **Prefer to let an AI agent do it?** If you use Claude Code (or a similar
> coding agent), point it at **[`CLAUDE-SETUP.md`](CLAUDE-SETUP.md)** — a
> runbook that automates most of this setup for you.

## What's in the stack

| Service | Image | Role |
|---|---|---|
| `stableflow-api` | `ghcr.io/mattshanaman/stableflow-oem-api` | Payments/invoicing REST API + auth |
| `stableflow-worker` | `ghcr.io/mattshanaman/stableflow-oem-worker` | On-chain payment detection, reminders, webhooks |
| `formaprint-api` | `ghcr.io/mattshanaman/formaprint-oem-api` | Renders invoice/statement PDFs (bundled) |
| `formaprint-worker` | `ghcr.io/mattshanaman/formaprint-oem-worker` | FormaPrint render/ingestion + audit sealing |

StableFlow watches supported chains for stablecoin payments, matches them to
invoices, and emails invoices/reminders. When an invoice PDF is needed it
calls the bundled FormaPrint over the internal Docker network
(`http://formaprint-api:80`) and attaches the rendered PDF. Both products
validate their license against Celestryll on each run.

## Prerequisites

- **Docker** and **Docker Compose** (Docker Desktop, or Docker Engine + the
  `docker compose` plugin).
- A **Celestryll account and a StableFlow license** — sign up at
  **https://celestryll.com/get-started**. A trial gives you a **Demo**
  license (30-day, online validation); purchasing upgrades you to a
  **Production** license. **Your StableFlow license includes a bundled
  FormaPrint license**, so at sign-up you receive **two** sets of Licentia
  credentials (each is a **TenantId**, **LicenseKey**, **ApiKey**): one for
  StableFlow, one for FormaPrint.
- A **SQL Server or Azure SQL database** you control. Both products can
  share **one** database — StableFlow in the `stable` schema, FormaPrint in
  the `forma` schema. No database of your own? See the optional bundled
  `sql` service in `docker-compose.yml` (fine for evaluation; use a managed,
  backed-up database for production).
- A SQL client to apply the schemas once — `sqlcmd`, Azure Data Studio, or
  SSMS.
- A JWT tool (or the snippet in step 5/9) to mint HS256 service tokens.

## Setup

### 1. Sign up and get your license(s)

Create your account at **https://celestryll.com/get-started** and obtain
your StableFlow credentials **and** the bundled FormaPrint credentials —
two sets of **TenantId / LicenseKey / ApiKey**. Keep them handy.

### 2. Configure `.env`

Copy the template and fill it in:

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

Edit `.env`:

- **From Celestryll (StableFlow):** `STABLEFLOW__LICENSING__LICENTIA__TENANTID`,
  `…__LICENSEKEY`, `…__APIKEY`.
- **From Celestryll (bundled FormaPrint — do not buy separately):**
  `FORMAPRINT__LICENTIA__TENANTID`, `FORMAPRINT__LICENTIA__LICENSEKEY`,
  `FORMAPRINT__LICENTIA__APIKEY`.
- **Your infrastructure:** `STABLEFLOW_CONNECTION` and `FORMAPRINT_CONNECTION`
  (same DB is fine — they differ only by schema), `STABLEFLOW__PUBLICBASEURL`
  (your payer-facing URL). Leave `STABLEFLOW__SCHEMANAME=stable` and
  `FORMAPRINT__SCHEMANAME=forma`.
- **Prefilled (licensed):** leave the two `…__ENDPOINT` values as
  `https://api.celestryll.com/licensing`, `STABLEFLOW__LICENSING__MODE=Licentia`,
  and `STABLEFLOW__LICENSING__ENABLED=true`. **In this licensed posture the
  StableFlow API requires a provider signing key** (`stableflow-provider-signing-key`
  plus an offline RSA verify key) resolvable from your secret store — without it
  the API crash-loops at startup.
- **Demo / evaluation (no license):** set `STABLEFLOW__LICENSING__ENABLED=false`.
  The API then boots permissively with **no** provider signing key required —
  the fastest way to stand the stack up for evaluation. (You may also blank the
  `…__ENDPOINT` values.) Evaluation only; do not run production unlicensed.

`.env` is git-ignored. Never commit it or paste it into shared output.

### 3. Generate the internal secrets

Five secrets are yours to generate — they are **not** provided by
Celestryll. Use strong, unique random values (32+ bytes each):

```bash
openssl rand -base64 48   # -> JWT__SIGNINGKEY                     (StableFlow API/dashboard tokens)
openssl rand -base64 48   # -> STABLEFLOW__AUDIT__HMACKEY          (StableFlow audit chain)
openssl rand -base64 48   # -> STABLEFLOW__WEBHOOKS__SIGNINGSECRET (StableFlow outbound webhooks)
openssl rand -base64 48   # -> FORMAPRINT__JWT__SIGNINGKEY         (FormaPrint API tokens)
openssl rand -base64 48   # -> FORMAPRINT__AUDIT__HMACKEY          (FormaPrint audit chain)
```

Put each value in the matching `.env` variable. Store copies in your secret
manager — losing an audit HMAC key means older audit rows can no longer be
re-verified for that product.

### 4. Apply the database schemas (BOTH products)

Both products' SQL scripts are **shipped in this kit**: StableFlow's in
[`./sql/stableflow/`](sql/stableflow/) (28 scripts → schema `stable`) and the
bundled FormaPrint's in [`./sql/formaprint/`](sql/formaprint/) (14 scripts →
schema `forma`). This kit includes an **apply-schema helper** that runs each
set in the correct order with the required settings — run it **twice**, once
per product, as a DB **admin/migrator** (`sa` or owner):

```bash
# bash (Linux/macOS/WSL/Git Bash) — from the kit directory
./apply-schema.sh --database OemShared --schema stable --sql-dir ./sql/stableflow \
  --server localhost,1433 --user sa --password '<sa-password>'
./apply-schema.sh --database OemShared --schema forma  --sql-dir ./sql/formaprint \
  --server localhost,1433 --user sa --password '<sa-password>'
```

```powershell
# PowerShell (Windows)
./apply-schema.ps1 -Database OemShared -Schema stable -SqlDir ./sql/stableflow `
  -Server localhost,1433 -User sa -Password '<sa-password>'
./apply-schema.ps1 -Database OemShared -Schema forma  -SqlDir ./sql/formaprint `
  -Server localhost,1433 -User sa -Password '<sa-password>'
```

Both products can share **one** database (`OemShared` above) — they are
isolated by schema. The helper creates the database if absent and applies
every script in `--sql-dir` in numeric order, always setting
`QUOTED_IDENTIFIER ON` (`-I`) and aborting on error (`-b`). The scripts are
idempotent (safe to re-run on upgrade). You need a `sqlcmd` on your PATH
(mssql-tools18) or pass `--sqlcmd <path>`.

> **Why the helper, not raw `sqlcmd -i`?** StableFlow's `01_Tables.sql`
> creates a **filtered index** (`... WHERE PaymentAddress IS NOT NULL`), which
> requires `SET QUOTED_IDENTIFIER ON`. Plain `sqlcmd` defaults it **OFF**, so
> hand-running that first script without `-I` fails outright. The helper sets it.

Decision points:

- **`sa` is only for this one-time schema apply.** Each product's **runtime**
  connection string must use its least-privilege app login — `stableflow_app`
  (from `24_AppSecurity.sql`) and `formaprint_app` (from `12_AppSecurity.sql`),
  dev passwords `StableFlow!App#Dev1` / `FormaPrint!App#Dev1`. Production
  refuses an `sa` runtime connection at boot.
- **On Azure SQL, skip the `AppSecurity` script in BOTH sets** —
  StableFlow's `24_AppSecurity.sql` and FormaPrint's `12_AppSecurity.sql` run
  `CREATE LOGIN` (a server-level statement Azure SQL doesn't support). Apply
  the rest, then create a contained user for each app and grant it
  `SELECT/INSERT/UPDATE/DELETE/EXECUTE` on `SCHEMA::stable` / `SCHEMA::forma`.
- **`02_CDC.sql` needs SQL Server Agent.** On **SQL Express** (no Agent), add
  `--demo` to the StableFlow invocation to **skip the CDC script** — the rest
  applies cleanly. (Note: StableFlow's `19_Compression` / `20_Partitioning`
  also need Developer/Standard+; Express is best for a light demo only.) On
  full SQL Server / Azure SQL MI, ensure Agent/CDC is enabled and omit `--demo`.
- The `06_SeedData` scripts seed a demo tenant in each product. FormaPrint's
  seed includes the **Invoice** and **TransactionStatement** templates
  StableFlow needs (see step 6).

### 5. Mint the StableFlow → FormaPrint service token

FormaPrint authenticates every request, so StableFlow presents a bearer
token when it calls FormaPrint to render a PDF. That token is an **HS256
JWT signed with `FORMAPRINT__JWT__SIGNINGKEY`** (the key you set in step 3),
with claims `iss=FormaPrint`, `aud=FormaPrint`, `scope=service`.

Mint it and put it in `.env` as
`STABLEFLOW__INVOICEDOCUMENT__FORMAPRINT__SERVICETOKEN`:

```bash
python - <<'PY'
import os, re, json, base64, hmac, hashlib, time, pathlib
env = dict(re.findall(r"(?m)^([A-Za-z0-9_]+)=(.*)$", pathlib.Path(".env").read_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())
# Long-lived service token (1 year). Re-mint on expiry or key rotation.
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())
print((hdr+b"."+pl+b"."+sig).decode())
PY
```

Copy the printed token into `.env`. (It expires in a year — re-run this to
renew, or whenever you rotate `FORMAPRINT__JWT__SIGNINGKEY`.)

### 6. Confirm the FormaPrint invoice templates exist

StableFlow renders with FormaPrint document types **`Invoice`** and
**`TransactionStatement`**. FormaPrint's `06_SeedData.sql` already seeds
both (plus localized `-es` / `-fr` / `-de` variants) under its **demo
tenant**. If you leave `STABLEFLOW__INVOICEDOCUMENT__FORMAPRINT__TENANTID`
**blank**, StableFlow uses that seeded demo tenant and it works out of the
box. If you instead point at a **custom** FormaPrint tenant, register an
`Invoice` and a `TransactionStatement` template under it first (via
FormaPrint's template API) or invoice rendering returns *template not found*.

### 7. Start the stack

```bash
docker compose up -d
```

This pulls Celestryll's published StableFlow + FormaPrint images and starts
all four services against your database and licenses.

### 8. Verify health

```bash
# StableFlow API (host port from STABLEFLOW_API_PORT, default 8080)
curl -fsS http://localhost:8080/health/live      # liveness
curl -fsS http://localhost:8080/health/ready     # reports DB readiness

# Bundled FormaPrint API (host port from FORMAPRINT_API_PORT, default 5000)
curl -fsS http://localhost:5000/health/live
curl -fsS http://localhost:5000/health/ready
```

Both `/health/ready` returning healthy confirms each API reached your
database. 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'
```

### 9. Smoke test — render an invoice PDF through FormaPrint

This proves the bundle end-to-end: the FormaPrint license validated and the
seeded `Invoice` template renders. Mint a **FormaPrint** service token (same
recipe as step 5) and render the seeded Invoice for FormaPrint's demo tenant:

```bash
TOKEN=<service token signed with FORMAPRINT__JWT__SIGNINGKEY>
TENANT=11111111-1111-1111-1111-111111111111    # FormaPrint seeded demo tenant

curl -fsS -X POST http://localhost:5000/reports/generate \
  -H "Authorization: Bearer $TOKEN" \
  -H "X-Tenant-Id: $TENANT" \
  -H "Content-Type: application/json" \
  -d '{
    "documentType": "Invoice",
    "outputFormat": "pdf",
    "data": {
      "invoiceNumber": "INV-1001", "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-1001",
      "feeNote": "Includes 0.5% processing fee.",
      "lines": [ { "lineNumber": 1, "description": "Subscription — August",
        "reference": "SUB-8", "quantity": 1, "unitPrice": 250.00, "amount": 250.00 } ] } }'
# -> { "success": true, "reportId": "...", "download": "/reports/.../download", ... }
```

A `reportId` in the response means licensing validated and rendering works.
In normal operation StableFlow performs this call itself and attaches the
PDF when it emails an invoice/reminder — no manual step needed.

## Configuration reference

| `.env` variable | Who supplies it | Purpose |
|---|---|---|
| `STABLEFLOW__LICENSING__LICENTIA__TENANTID` | Celestryll (sign-up) | StableFlow Licentia tenant id |
| `STABLEFLOW__LICENSING__LICENTIA__LICENSEKEY` | Celestryll (sign-up) | StableFlow license key |
| `STABLEFLOW__LICENSING__LICENTIA__APIKEY` | Celestryll (sign-up) | StableFlow `X-Licentia-ApiKey` |
| `FORMAPRINT__LICENTIA__TENANTID` | Celestryll (bundled) | FormaPrint Licentia tenant id — **included with StableFlow** |
| `FORMAPRINT__LICENTIA__LICENSEKEY` | Celestryll (bundled) | FormaPrint license key — **included with StableFlow** |
| `FORMAPRINT__LICENTIA__APIKEY` | Celestryll (bundled) | FormaPrint `X-Licentia-ApiKey` — **included with StableFlow** |
| `STABLEFLOW_CONNECTION` | You | StableFlow SQL connection string |
| `FORMAPRINT_CONNECTION` | You | FormaPrint SQL connection string (same DB OK) |
| `STABLEFLOW__SCHEMANAME` | You | StableFlow schema (default `stable`) |
| `FORMAPRINT__SCHEMANAME` | You | FormaPrint schema (default `forma`) |
| `STABLEFLOW__PUBLICBASEURL` | You | Payer-facing base URL in invoices/emails |
| `JWT__SIGNINGKEY` | You (generate) | Signs StableFlow API/dashboard tokens |
| `STABLEFLOW__AUDIT__HMACKEY` | You (generate) | Keys StableFlow's audit chain |
| `STABLEFLOW__WEBHOOKS__SIGNINGSECRET` | You (generate) | Signs outbound webhooks |
| `FORMAPRINT__JWT__SIGNINGKEY` | You (generate) | Signs FormaPrint tokens **and** the service token |
| `FORMAPRINT__AUDIT__HMACKEY` | You (generate) | Keys FormaPrint's audit chain |
| `STABLEFLOW__INVOICEDOCUMENT__FORMAPRINT__SERVICETOKEN` | You (mint) | Bearer token StableFlow sends to FormaPrint (signed with the FormaPrint JWT key) |
| `STABLEFLOW__LICENSING__MODE` | Prefilled | `Licentia` (enforce) |
| `STABLEFLOW__LICENSING__LICENTIA__ENDPOINT` | Prefilled | `https://api.celestryll.com/licensing` |
| `FORMAPRINT__LICENTIA__ENDPOINT` | Prefilled | `https://api.celestryll.com/licensing` |
| `STABLEFLOW__INVOICEDOCUMENT__FORMAPRINT__TENANTID` | Optional | FormaPrint app tenant (blank = seeded demo tenant) |
| `STABLEFLOW_VERSION` / `FORMAPRINT_VERSION` | Optional | Published image tags (default `latest`) |
| `STABLEFLOW_API_PORT` / `FORMAPRINT_API_PORT` | Optional | Host ports (default 8080 / 5000) |
| `STABLEFLOW__REDIS__CONNECTIONSTRING` | Optional | Redis for the worker (blank = SQL fallback) |
| `STABLEFLOW__FEED__APIKEYS__0` | Optional | External-reporting feed key |

`InvoiceDocument__Provider` is fixed to `FormaPrint` and
`InvoiceDocument__FormaPrint__BaseUrl` to `http://formaprint-api:80` in
`docker-compose.yml` (the internal service address) — you don't set these in
`.env`.

## Upgrading

Pin versions in `.env` and bump them when Celestryll publishes releases:

```bash
# in .env
STABLEFLOW_VERSION=1.4.0
FORMAPRINT_VERSION=1.4.0

docker compose pull
docker compose up -d
```

Re-apply each product's `sql/` scripts (idempotent) if the release notes
call for schema changes.

## Troubleshooting

- **`402` / `LICENSE_REJECTED` on StableFlow** — StableFlow's license didn't
  validate. Check `STABLEFLOW__LICENSING__LICENTIA__{TENANTID,LICENSEKEY,APIKEY}`
  and that `…__ENDPOINT` is `https://api.celestryll.com/licensing`. A license
  binds to the first machine that validates it; if you moved hosts, contact
  support for a transfer.
- **Invoice PDF missing / FormaPrint `402`** — the **bundled FormaPrint**
  license didn't validate. Check the `FORMAPRINT__LICENTIA__*` values (the
  set Celestryll issued for FormaPrint, **not** the StableFlow ones).
- **`401 Unauthorized` from FormaPrint when StableFlow renders** — the
  service token is missing/invalid. It must be signed with the **current**
  `FORMAPRINT__JWT__SIGNINGKEY` (claims `iss=FormaPrint`, `aud=FormaPrint`,
  `scope=service`). Re-mint it (step 5) and update
  `STABLEFLOW__INVOICEDOCUMENT__FORMAPRINT__SERVICETOKEN`; restart the
  StableFlow services.
- **`template not found` when rendering an invoice** — no `Invoice` /
  `TransactionStatement` template for the tenant StableFlow named. Leave
  `STABLEFLOW__INVOICEDOCUMENT__FORMAPRINT__TENANTID` blank to use
  FormaPrint's seeded demo tenant, or register those templates under your
  custom tenant (step 6). Confirm FormaPrint's `06_SeedData.sql` ran.
- **StableFlow can't reach FormaPrint** — both services must be on the same
  Compose network (they are, `oemnet`). Verify with the `docker compose exec
  stableflow-api … http://formaprint-api:80/health/live` check in step 8. The
  `BaseUrl` StableFlow uses is the internal `http://formaprint-api:80`, not a
  host port.
- **StableFlow API crash-loops at startup complaining about a signing key /
  `Licensing:SigningKeySecretRef`** — you are in the licensed posture
  (`STABLEFLOW__LICENSING__ENABLED=true`) without a provider signing key in
  your secret store. For a demo, set `STABLEFLOW__LICENSING__ENABLED=false`; for
  a licensed deploy, provision the `stableflow-provider-signing-key` (and the
  offline RSA verify key) Celestryll issues.
- **StableFlow API won't start / `/health/ready` unhealthy** — database
  connectivity. Verify `STABLEFLOW_CONNECTION` (host, port, credentials,
  `Encrypt` / `TrustServerCertificate`), that the DB exists, and that the
  `stable` schema scripts were applied. Logs:
  `docker compose logs stableflow-api`.
- **CDC / SQL Agent errors on startup** — `02_CDC.sql` needs SQL Server
  Agent. On SQL Express skip it; on managed SQL ensure Agent/CDC is enabled.
- **App can't log in to SQL** — confirm you used the least-privilege app
  users (not `sa`) and granted each rights on its own schema
  (`SCHEMA::stable`, `SCHEMA::forma`).

## Support

- Get started / manage your license: **https://celestryll.com/get-started**
- Support: **support@celestryll.com**

When contacting support, include your StableFlow **TenantId** and the
relevant API logs (`docker compose logs stableflow-api` /
`docker compose logs formaprint-api`). **Never** send any LicenseKey, ApiKey,
JWT signing key, audit HMAC key, webhook secret, or the service token.
