# A small app that sells with sub3

A plain Node server and page (Node 22.13+, one dependency: `jose`). It shows every part of an integration:

- wallet sign-in, as a popup or a redirect, and server-side JWT verification;
- checkout links built from the product (`acme/app`) and an item key;
- access from `GET /v1/access`, cached 30 seconds and read fresh after a checkout;
- rules that work for any product without editing (`config.mjs`): any active tier or any lifetime item lets a
  user in; a tier of level _n_ gives 10^_n_ actions per cycle; level 2+ or any lifetime item unlocks a premium
  feature. Items, names and prices are read from `GET /v1/products` at runtime;
- renewals and upgrades between tiers, with the monthly allowance tracked per `cycle.id`;
- lifetime items, which also unlock the premium feature;
- credit packs credited once per receipt from `GET /v1/orders`, including payments whose result was lost.

## Run it

Create your handle and product first, yourself, in the sandbox dashboard: https://sub3.dev/sandbox/dashboard
(free test USDC from https://faucet.circle.com). Give it a tier or two, a lifetime item or a credit pack; any
keys work. Then:

```sh
cd examples/node
npm ci
cp .env.example .env     # set SUB3_PRODUCT to your handle/key
npm start                # http://localhost:3317
```

`.env.example` uses the sandbox (Arc Testnet, test USDC). When it works, create the same handle and product on
https://sub3.dev/dashboard and set `SUB3_ENVIRONMENT=production`; nothing else changes. `APP_ORIGIN` must be the
exact origin the browser shows: session tokens are issued to it, so sign-in works only there.

`npm test` runs the ledger and HTTP tests against the fake sub3 API in
[`../testing/fake-sub3.mjs`](../testing/README.md), with no wallet. `node website/tests/e2e/demos.mjs` (from the
repository root, with the local stack running: `cd website && npm run dev`) drives the real hosted pages.

## Files

| File              | What it does                                                                                                                | Copy it?                       |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------ |
| `public/sub3.js`  | Opens `/login` and `/checkout` (popup or redirect), reads redirect results, builds checkout paths                           | Yes, into your frontend        |
| `sub3.mjs`        | `verifySession`, `getAccess` (cached), `afterCheckout`, `creditPack`, `syncPacks`                                           | Yes, into your backend         |
| `config.mjs`      | What each item unlocks (`allowances`, `premium`, `actionCost`) and `entitlements(access)`; environment                      | Adapt                          |
| `ledger.mjs`      | Once-only receipts, credits, allowance used per cycle, idempotent actions, on any synchronous SQL store                     | Port its keys and transactions |
| `sqlite.mjs`      | The ledger on a SQLite file (`node:sqlite`); Cloudflare uses a Durable Object instead (`ledgerOn`)                          | Example                        |
| `app.mjs`         | Routes: `/api/config`, `/api/account`, `/api/refresh` (after sign-in or checkout), `/api/run`, and `/.well-known/sub3.json` | Example                        |
| `server.mjs`      | The Node HTTP server around `app.mjs` and the static pages                                                                  | Example                        |
| `public/app.js`   | The page: `signIn`, `buy`, `receive`, the buttons and the developer view                                                    | Example                        |
| `public/logo.svg` | This shop's logo, named in `/.well-known/sub3.json`                                                                         | Replace with yours             |

The same app runs on Cloudflare Workers: see [../cloudflare](../cloudflare/README.md).

## Your logo at checkout

`app.mjs` serves `/.well-known/sub3.json` with your handle (from `SUB3_PRODUCT`) and `/logo.svg`. Once this site
is on HTTPS, sub3's sign-in and checkout pages opened from it show this logo. It does not need to be your
merchant's website: never change the merchant's URL to make a product site work. The logo may be on any HTTPS
host. On `localhost` nothing can be verified; deploy first (for example to Cloudflare).

## What to try

1. **Sign in.** Tick "Use redirects" to try the redirect flow. An expired or foreign token is a 401; a sub3
   outage is a 503 and keeps you signed in.
2. **Buy a level-1 tier for a month.** Run a few actions: each uses this cycle's allowance. The premium feature
   is refused.
3. **Renew it, or buy 12 months.** The end date moves; the cycle and the allowance used in it do not.
4. **Upgrade to a level-2 tier.** The checkout warns that it starts now at full price and forfeits the rest of the
   lower tier. After it, `cycle.id` is new, so the larger allowance is available at once, and the premium feature
   works.
5. **A lower tier while a higher one is active** shows "After <date>": it waits until expiry.
6. **Buy a credit pack.** When the allowance runs out, actions spend credits. Close the checkout window right
   after paying: "Refresh" finds the payment through `/v1/orders` and credits it once, however often you refresh.
7. **Buy a lifetime item.** It unlocks the premium feature without a tier, for good.
8. **Make the action fail.** What it took is given back. Each action has an `Idempotency-Key`; a retried request
   is charged once.

Not covered, on purpose: payment refunds, proration, automatic renewal and expiring credits. sub3 does not do them.
