From 43e32b52c614324d1bf0b7cf31f8bdcc893d9680 Mon Sep 17 00:00:00 2001 From: Tom Stark Date: Tue, 14 Jul 2026 11:31:55 +0200 Subject: [PATCH 1/9] feat: add self-report demo site (vanilla PHP publisher for status-probe testing) --- demo/self-report/.dockerignore | 2 + demo/self-report/.gitignore | 1 + demo/self-report/Dockerfile | 19 +++++ demo/self-report/README.md | 75 +++++++++++++++++ demo/self-report/composer.json | 11 +++ demo/self-report/composer.lock | 146 +++++++++++++++++++++++++++++++++ demo/self-report/index.php | 100 ++++++++++++++++++++++ 7 files changed, 354 insertions(+) create mode 100644 demo/self-report/.dockerignore create mode 100644 demo/self-report/.gitignore create mode 100644 demo/self-report/Dockerfile create mode 100644 demo/self-report/README.md create mode 100644 demo/self-report/composer.json create mode 100644 demo/self-report/composer.lock create mode 100644 demo/self-report/index.php diff --git a/demo/self-report/.dockerignore b/demo/self-report/.dockerignore new file mode 100644 index 0000000..0d060bc --- /dev/null +++ b/demo/self-report/.dockerignore @@ -0,0 +1,2 @@ +vendor/ +README.md diff --git a/demo/self-report/.gitignore b/demo/self-report/.gitignore new file mode 100644 index 0000000..48b8bf9 --- /dev/null +++ b/demo/self-report/.gitignore @@ -0,0 +1 @@ +vendor/ diff --git a/demo/self-report/Dockerfile b/demo/self-report/Dockerfile new file mode 100644 index 0000000..42e2664 --- /dev/null +++ b/demo/self-report/Dockerfile @@ -0,0 +1,19 @@ +# Dependencies — resolved from the committed lockfile for reproducible builds. +FROM composer:2 AS deps +WORKDIR /app +COPY composer.json composer.lock ./ +RUN composer install --no-dev --no-interaction --no-progress + +FROM php:8.3-apache + +# App Runner's default container port is 8080; route every request that +# isn't an existing file to the front controller. +RUN sed -i 's/^Listen 80$/Listen 8080/' /etc/apache2/ports.conf \ + && sed -i 's///' /etc/apache2/sites-available/000-default.conf \ + && printf 'FallbackResource /index.php\n' > /etc/apache2/conf-available/fallback.conf \ + && a2enconf fallback + +COPY --from=deps /app/vendor /var/www/html/vendor +COPY index.php /var/www/html/ + +EXPOSE 8080 diff --git a/demo/self-report/README.md b/demo/self-report/README.md new file mode 100644 index 0000000..a2454d9 --- /dev/null +++ b/demo/self-report/README.md @@ -0,0 +1,75 @@ +# Self-Report Demo Site + +Vanilla-PHP publisher for testing the `/.well-known/supertab/status` +self-report endpoint end-to-end against the real (sandbox) Supertab Connect +API. Every request flows through `SupertabConnect::handleRequest()` — the +status endpoint is served by the SDK itself, with zero endpoint-specific +code in this app. It also serves as the canonical "plain PHP" integration +reference. + +Unlike the sibling `demo/` CLI demo (self-contained, mock API), this app +pins the **released Packagist SDK** and talks to the real API. Testing a new +SDK release = bump the pin in `composer.json`, rebuild, push. + +## Configuration + +| Env var | Default | Purpose | +|---------|---------|---------| +| `SUPERTAB_MERCHANT_API_KEY` | — (required) | Sandbox merchant API key | +| `SUPERTAB_BASE_URL` | `https://api-connect.sbx.supertab.co` | API base URL | +| `SUPERTAB_ENFORCEMENT` | `observe` | `disabled` \| `observe` \| `enforce` — reflected in the status payload | +| `SUPERTAB_ANALYTICS` | on (`0`/`false`/`off` to disable) | Toggles analytics → the payload's `eventReporting` | + +## Run locally + +```bash +composer install +SUPERTAB_MERCHANT_API_KEY= php -S localhost:8080 index.php +``` + +Smoke checks: + +```bash +curl -s localhost:8080/healthz # → ok +curl -si localhost:8080/.well-known/supertab/status | head -5 # → 404 {"supertab":true} +curl -s localhost:8080/ | head -3 # → demo HTML page +``` + +## Deploy to AWS App Runner + +App Runner has no managed PHP runtime, so it deploys from a container image +in ECR. + +```bash +AWS_ACCOUNT= AWS_REGION= +REPO=$AWS_ACCOUNT.dkr.ecr.$AWS_REGION.amazonaws.com/supertab-self-report-demo + +aws ecr create-repository --repository-name supertab-self-report-demo --region $AWS_REGION +aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $REPO +docker build --platform linux/amd64 -t $REPO:latest . +docker push $REPO:latest +``` + +Create the service (console or CLI): **source** = the ECR image with +auto-deployment on push, **port** = `8080`, **size** = 0.25 vCPU / 512 MB, +**health check** = HTTP on `/healthz`, and the env vars above (at minimum +`SUPERTAB_MERCHANT_API_KEY`). + +## Register the site (required for probes) + +The backend only mints status challenges (`aud` = origin) for origins it +knows. Register the service URL — `https://..awsapprunner.com` +— as a merchant website in **sandbox**. If the URL changes (service +recreated), re-register. + +## Probe flow + +1. Unauthenticated: `curl -si https:///.well-known/supertab/status` + → `404` + `{"supertab":true}` + `Cache-Control: no-store` (decoy). +2. Garbage bearer: same decoy, never a 500 (challenge verification fails + closed). +3. Backend live-health probe for the registered site → `200` with + `{runtime, sdkVersion, component: {kind: "php-sdk", version}, + enforcement, eventReporting}`. +4. Flip `SUPERTAB_ENFORCEMENT` / `SUPERTAB_ANALYTICS` on the service → + next probe reflects the change. diff --git a/demo/self-report/composer.json b/demo/self-report/composer.json new file mode 100644 index 0000000..1d47968 --- /dev/null +++ b/demo/self-report/composer.json @@ -0,0 +1,11 @@ +{ + "name": "supertab/self-report-demo", + "description": "Vanilla-PHP demo site for testing the /.well-known/supertab/status self-report endpoint against the real API.", + "type": "project", + "license": "MIT", + "require": { + "php": ">=8.1", + "getsupertab/connect-sdk-php": "1.4.0-beta.9" + }, + "minimum-stability": "stable" +} diff --git a/demo/self-report/composer.lock b/demo/self-report/composer.lock new file mode 100644 index 0000000..563cf84 --- /dev/null +++ b/demo/self-report/composer.lock @@ -0,0 +1,146 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "66cd0c5a4fa4536131c20d1d85a6d524", + "packages": [ + { + "name": "firebase/php-jwt", + "version": "v7.1.0", + "source": { + "type": "git", + "url": "https://github.com/googleapis/php-jwt.git", + "reference": "b374a5d1a4f1f67fadc2165cdb284645945e2fc0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/googleapis/php-jwt/zipball/b374a5d1a4f1f67fadc2165cdb284645945e2fc0", + "reference": "b374a5d1a4f1f67fadc2165cdb284645945e2fc0", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.4", + "phpfastcache/phpfastcache": "^9.2", + "phpseclib/phpseclib": "~3.0", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psr/cache": "^2.0||^3.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" + }, + "suggest": { + "ext-sodium": "Support EdDSA (Ed25519) signatures", + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present", + "phpseclib/phpseclib": "Support PS256 (RSASSA-PSS) signatures" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/googleapis/php-jwt", + "keywords": [ + "jwt", + "php" + ], + "support": { + "issues": "https://github.com/googleapis/php-jwt/issues", + "source": "https://github.com/googleapis/php-jwt/tree/v7.1.0" + }, + "time": "2026-06-11T17:54:14+00:00" + }, + { + "name": "getsupertab/connect-sdk-php", + "version": "v1.4.0-beta.9", + "source": { + "type": "git", + "url": "https://github.com/getsupertab/connect-sdk-php.git", + "reference": "ad5459a7f73d8a50f284b4dee6182b33b96f08eb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/getsupertab/connect-sdk-php/zipball/ad5459a7f73d8a50f284b4dee6182b33b96f08eb", + "reference": "ad5459a7f73d8a50f284b4dee6182b33b96f08eb", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-openssl": "*", + "ext-simplexml": "*", + "firebase/php-jwt": "^7.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.5 || ^11.0", + "squizlabs/php_codesniffer": "^3.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "Supertab\\Connect\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Supertab", + "homepage": "https://supertab.co" + } + ], + "description": "Supertab Connect PHP SDK", + "homepage": "https://github.com/getsupertab/connect-sdk-php", + "keywords": [ + "connect", + "jwt", + "license", + "rsl", + "supertab" + ], + "support": { + "issues": "https://github.com/getsupertab/connect-sdk-php/issues", + "source": "https://github.com/getsupertab/connect-sdk-php/tree/v1.4.0-beta.9" + }, + "time": "2026-07-14T08:41:59+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "getsupertab/connect-sdk-php": 10 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=8.1" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/demo/self-report/index.php b/demo/self-report/index.php new file mode 100644 index 0000000..965fabb --- /dev/null +++ b/demo/self-report/index.php @@ -0,0 +1,100 @@ +handleRequest(RequestContext::fromGlobals()); + +foreach ($result->headers as $name => $value) { + header("{$name}: {$value}"); +} + +if (! $result instanceof AllowResult) { + // BLOCK and RESPOND both carry a complete response to emit — the + // RESPOND branch is what serves the self-report status endpoint. + http_response_code($result->status); + echo $result->body; + exit; +} + +// ── Allowed: minimal demo page ─────────────────────────────────────── +$sdkVersion = htmlspecialchars(HttpClient::resolveVersion(), ENT_QUOTES, 'UTF-8'); +$mode = htmlspecialchars($enforcement->value, ENT_QUOTES, 'UTF-8'); + +header('Content-Type: text/html; charset=UTF-8'); +echo << + + + + +Supertab Connect PHP SDK — self-report demo + + +

Supertab Connect PHP SDK — self-report demo

+

This site routes every request through SupertabConnect::handleRequest(). +The backend's status probe is answered at /.well-known/supertab/status +by the SDK itself — there is no endpoint-specific code here.

+
    +
  • SDK version: {$sdkVersion}
  • +
  • Enforcement mode: {$mode}
  • +
+ + +HTML; From 08f04146e86330c8106b017a38f16804093f6fbf Mon Sep 17 00:00:00 2001 From: Tom Stark Date: Tue, 14 Jul 2026 12:43:51 +0200 Subject: [PATCH 2/9] docs: add App Runner deployment runbook --- demo/self-report/DEPLOY.md | 172 +++++++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 demo/self-report/DEPLOY.md diff --git a/demo/self-report/DEPLOY.md b/demo/self-report/DEPLOY.md new file mode 100644 index 0000000..82e6b7d --- /dev/null +++ b/demo/self-report/DEPLOY.md @@ -0,0 +1,172 @@ +# Deploying the Self-Report Demo Site to AWS App Runner + +Steps 1–5 happen once; step 8 is the recurring update path. + +## 0. Prerequisites + +- AWS CLI v2 authenticated against the **Supertab** account (see + [Multiple AWS accounts](#multiple-aws-accounts-named-profiles) below if + your default profile points at another org), Docker Desktop running. +- A **sandbox merchant API key** for Supertab Connect. +- This directory checked out locally (branch `feat/self-report-demo-site`, + or `main` once PR #23 merges). + +```bash +export AWS_PROFILE=supertab # if using a named profile — set BEFORE the next line +export AWS_ACCOUNT=$(aws sts get-caller-identity --query Account --output text) +export AWS_REGION=eu-central-1 # pick your region +export REPO=$AWS_ACCOUNT.dkr.ecr.$AWS_REGION.amazonaws.com/supertab-self-report-demo +``` + +Sanity check you're in the right account before creating anything: +`aws sts get-caller-identity`. + +## 1. Create the ECR repository (once) + +```bash +aws ecr create-repository \ + --repository-name supertab-self-report-demo \ + --region $AWS_REGION +``` + +## 2. Build and push the image + +```bash +cd demo/self-report +aws ecr get-login-password --region $AWS_REGION | \ + docker login --username AWS --password-stdin $REPO +docker build --platform linux/amd64 -t $REPO:latest . +docker push $REPO:latest +``` + +`--platform linux/amd64` matters on Apple Silicon — App Runner runs x86_64. + +## 3. Create the ECR access role (once) + +App Runner needs an IAM role to pull from private ECR. If the account +doesn't already have `AppRunnerECRAccessRole`: + +```bash +aws iam create-role --role-name AppRunnerECRAccessRole \ + --assume-role-policy-document '{ + "Version": "2012-10-17", + "Statement": [{"Effect": "Allow", "Principal": {"Service": "build.apprunner.amazonaws.com"}, "Action": "sts:AssumeRole"}] + }' +aws iam attach-role-policy --role-name AppRunnerECRAccessRole \ + --policy-arn arn:aws:iam::aws:policy/service-role/AWSAppRunnerServicePolicyForECRAccess +``` + +## 4. Create the App Runner service (once) + +```bash +aws apprunner create-service \ + --region $AWS_REGION \ + --service-name supertab-self-report-demo \ + --source-configuration '{ + "AuthenticationConfiguration": {"AccessRoleArn": "arn:aws:iam::'$AWS_ACCOUNT':role/AppRunnerECRAccessRole"}, + "AutoDeploymentsEnabled": true, + "ImageRepository": { + "ImageIdentifier": "'$REPO':latest", + "ImageRepositoryType": "ECR", + "ImageConfiguration": { + "Port": "8080", + "RuntimeEnvironmentVariables": { + "SUPERTAB_MERCHANT_API_KEY": "", + "SUPERTAB_ENFORCEMENT": "observe" + } + } + } + }' \ + --instance-configuration '{"Cpu": "0.25 vCPU", "Memory": "0.5 GB"}' \ + --health-check-configuration '{"Protocol": "HTTP", "Path": "/healthz"}' +``` + +`SUPERTAB_BASE_URL` and `SUPERTAB_ANALYTICS` default to sandbox / on — set +them only to override. + +Console alternative: Services → Create → Container registry/ECR, image +`…/supertab-self-report-demo:latest`, auto-deploy **on**, port **8080**, +0.25 vCPU / 0.5 GB, health check **HTTP** `/healthz`, plus the env vars. + +## 5. Wait for it to go live and grab the URL + +```bash +aws apprunner list-services --region $AWS_REGION \ + --query "ServiceSummaryList[?ServiceName=='supertab-self-report-demo'].[Status,ServiceUrl]" \ + --output table +``` + +Wait for `RUNNING`, then smoke-test: + +```bash +HOST= +curl -s https://$HOST/healthz # → ok +curl -si https://$HOST/.well-known/supertab/status | head -5 # → 404 {"supertab":true}, no-store +curl -s https://$HOST/ | grep "SDK version" # → v1.4.0-beta.9 +``` + +## 6. Register the site in sandbox (required) + +Register `https://` as a merchant website in the **sandbox** +environment. The backend only mints status challenges with `aud` = a +registered origin — probes silently get the decoy otherwise. If the App +Runner service is ever recreated, the URL changes: re-register. + +## 7. Trigger the real end-to-end probe + +Fire a live-health check for the registered site (the backend's +`self_report` check). Expected: `200` with `runtime: null`, +`sdkVersion: "v1.4.0-beta.9"`, +`component: {kind: "php-sdk", version: "v1.4.0-beta.9"}`, +`enforcement: "observe"`, `eventReporting: true`. + +Note: the backend resolves only `ts-sdk` against a registry so far +(laterpay/supertab-connect#1094); `php-sdk` degrades to "show version, no +nudge" until its resolver lands. That's expected, not a failure. + +## 8. Updating (each new SDK release) + +```bash +cd demo/self-report +# bump the pin in composer.json, then: +composer update getsupertab/connect-sdk-php +aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $REPO +docker build --platform linux/amd64 -t $REPO:latest . && docker push $REPO:latest +``` + +Auto-deployment picks up the push and redeploys (~1–2 min). Commit the pin ++ lockfile change back to the repo. + +**Config experiments** (no rebuild): edit `SUPERTAB_ENFORCEMENT` / +`SUPERTAB_ANALYTICS` on the service (console → Configuration → Edit, or +`aws apprunner update-service`) — the next probe reflects the new values. + +**Cost**: ~$7/mo idle (0.25 vCPU / 0.5 GB provisioned) + a few cents ECR +storage. Delete with `aws apprunner delete-service` when no longer needed. + +## Multiple AWS accounts (named profiles) + +Keep your other org's CLI setup untouched by adding a named profile: + +```bash +# Static access keys: +aws configure --profile supertab + +# Or IAM Identity Center / SSO (typical for org accounts): +aws configure sso --profile supertab +# later sessions: aws sso login --profile supertab +``` + +Then either pass `--profile supertab` per command, or activate it for the +shell session (what step 0 assumes): + +```bash +export AWS_PROFILE=supertab +aws sts get-caller-identity # verify the account ID before creating resources +``` + +Docker inherits the choice automatically: `aws ecr get-login-password` +issues the token from the active profile, and `docker login`/`push` just +use that token. The one ordering gotcha: export `AWS_PROFILE` **before** +deriving `AWS_ACCOUNT` in step 0, since the account ID gets baked into +`$REPO`. From aaf471407bd33e392a184718c2dea08f7fb5a2c6 Mon Sep 17 00:00:00 2001 From: Tom Stark Date: Tue, 14 Jul 2026 12:55:51 +0200 Subject: [PATCH 3/9] =?UTF-8?q?docs:=20brace=20${REPO}=20expansions=20?= =?UTF-8?q?=E2=80=94=20zsh=20eats=20$REPO:latest=20via=20the=20:l=20modifi?= =?UTF-8?q?er?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- demo/self-report/DEPLOY.md | 6 +++--- demo/self-report/README.md | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/demo/self-report/DEPLOY.md b/demo/self-report/DEPLOY.md index 82e6b7d..33e641b 100644 --- a/demo/self-report/DEPLOY.md +++ b/demo/self-report/DEPLOY.md @@ -35,8 +35,8 @@ aws ecr create-repository \ cd demo/self-report aws ecr get-login-password --region $AWS_REGION | \ docker login --username AWS --password-stdin $REPO -docker build --platform linux/amd64 -t $REPO:latest . -docker push $REPO:latest +docker build --platform linux/amd64 -t ${REPO}:latest . +docker push ${REPO}:latest ``` `--platform linux/amd64` matters on Apple Silicon — App Runner runs x86_64. @@ -131,7 +131,7 @@ cd demo/self-report # bump the pin in composer.json, then: composer update getsupertab/connect-sdk-php aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $REPO -docker build --platform linux/amd64 -t $REPO:latest . && docker push $REPO:latest +docker build --platform linux/amd64 -t ${REPO}:latest . && docker push ${REPO}:latest ``` Auto-deployment picks up the push and redeploys (~1–2 min). Commit the pin diff --git a/demo/self-report/README.md b/demo/self-report/README.md index a2454d9..e34409e 100644 --- a/demo/self-report/README.md +++ b/demo/self-report/README.md @@ -46,8 +46,8 @@ REPO=$AWS_ACCOUNT.dkr.ecr.$AWS_REGION.amazonaws.com/supertab-self-report-demo aws ecr create-repository --repository-name supertab-self-report-demo --region $AWS_REGION aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $REPO -docker build --platform linux/amd64 -t $REPO:latest . -docker push $REPO:latest +docker build --platform linux/amd64 -t ${REPO}:latest . +docker push ${REPO}:latest ``` Create the service (console or CLI): **source** = the ECR image with From aef107e237bc80db7a564c5859a616544b143587 Mon Sep 17 00:00:00 2001 From: Tom Stark Date: Tue, 14 Jul 2026 13:01:42 +0200 Subject: [PATCH 4/9] docs: paste-proof the IAM/App Runner JSON (single-line policy, file:// source config) --- demo/self-report/DEPLOY.md | 53 +++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/demo/self-report/DEPLOY.md b/demo/self-report/DEPLOY.md index 33e641b..79cd7c5 100644 --- a/demo/self-report/DEPLOY.md +++ b/demo/self-report/DEPLOY.md @@ -48,37 +48,48 @@ doesn't already have `AppRunnerECRAccessRole`: ```bash aws iam create-role --role-name AppRunnerECRAccessRole \ - --assume-role-policy-document '{ - "Version": "2012-10-17", - "Statement": [{"Effect": "Allow", "Principal": {"Service": "build.apprunner.amazonaws.com"}, "Action": "sts:AssumeRole"}] - }' + --assume-role-policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"build.apprunner.amazonaws.com"},"Action":"sts:AssumeRole"}]}' aws iam attach-role-policy --role-name AppRunnerECRAccessRole \ --policy-arn arn:aws:iam::aws:policy/service-role/AWSAppRunnerServicePolicyForECRAccess ``` +(The policy JSON is deliberately one line: copy-pasting indented JSON from +rendered docs can smuggle in non-breaking spaces, which IAM rejects as +`MalformedPolicyDocument`.) + ## 4. Create the App Runner service (once) +Write the source configuration to a file first (the heredoc expands +`$AWS_ACCOUNT`/`$REPO` for you, and `file://` input sidesteps shell-quoting +and copy-paste whitespace issues): + ```bash +cat > /tmp/apprunner-source.json <", + "SUPERTAB_ENFORCEMENT": "observe" + } + } + } +} +EOF + aws apprunner create-service \ --region $AWS_REGION \ --service-name supertab-self-report-demo \ - --source-configuration '{ - "AuthenticationConfiguration": {"AccessRoleArn": "arn:aws:iam::'$AWS_ACCOUNT':role/AppRunnerECRAccessRole"}, - "AutoDeploymentsEnabled": true, - "ImageRepository": { - "ImageIdentifier": "'$REPO':latest", - "ImageRepositoryType": "ECR", - "ImageConfiguration": { - "Port": "8080", - "RuntimeEnvironmentVariables": { - "SUPERTAB_MERCHANT_API_KEY": "", - "SUPERTAB_ENFORCEMENT": "observe" - } - } - } - }' \ - --instance-configuration '{"Cpu": "0.25 vCPU", "Memory": "0.5 GB"}' \ - --health-check-configuration '{"Protocol": "HTTP", "Path": "/healthz"}' + --source-configuration file:///tmp/apprunner-source.json \ + --instance-configuration '{"Cpu":"0.25 vCPU","Memory":"0.5 GB"}' \ + --health-check-configuration '{"Protocol":"HTTP","Path":"/healthz"}' + +rm /tmp/apprunner-source.json # contains your merchant key ``` `SUPERTAB_BASE_URL` and `SUPERTAB_ANALYTICS` default to sandbox / on — set From 2c92b3f12cd1b15c57502b45facab4343f3ac7c8 Mon Sep 17 00:00:00 2001 From: Tom Stark Date: Tue, 14 Jul 2026 13:05:27 +0200 Subject: [PATCH 5/9] =?UTF-8?q?docs:=20bootstrap=20with=20placeholder=20ke?= =?UTF-8?q?y=20=E2=80=94=20registration=20needs=20the=20ServiceUrl=20that?= =?UTF-8?q?=20only=20exists=20post-create?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- demo/self-report/DEPLOY.md | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/demo/self-report/DEPLOY.md b/demo/self-report/DEPLOY.md index 79cd7c5..f7fe348 100644 --- a/demo/self-report/DEPLOY.md +++ b/demo/self-report/DEPLOY.md @@ -74,7 +74,7 @@ cat > /tmp/apprunner-source.json <", + "SUPERTAB_MERCHANT_API_KEY": "placeholder", "SUPERTAB_ENFORCEMENT": "observe" } } @@ -89,9 +89,18 @@ aws apprunner create-service \ --instance-configuration '{"Cpu":"0.25 vCPU","Memory":"0.5 GB"}' \ --health-check-configuration '{"Protocol":"HTTP","Path":"/healthz"}' -rm /tmp/apprunner-source.json # contains your merchant key +rm /tmp/apprunner-source.json ``` +The API key starts as `placeholder` on purpose: sandbox registration needs +the site's domain, and the domain (ServiceUrl) only exists once the service +does. The service runs fine meanwhile — `/healthz` short-circuits before the +config check and challenge verification uses the public platform JWKS; only +analytics delivery would 401-and-drop. After step 6 issues the real key, +swap it in (console → Configuration → Edit env vars, or +`aws apprunner update-service` with the same source-config file) — the +service rolls automatically. + `SUPERTAB_BASE_URL` and `SUPERTAB_ANALYTICS` default to sandbox / on — set them only to override. @@ -116,12 +125,17 @@ curl -si https://$HOST/.well-known/supertab/status | head -5 # → 404 {"supe curl -s https://$HOST/ | grep "SDK version" # → v1.4.0-beta.9 ``` -## 6. Register the site in sandbox (required) +## 6. Register the site in sandbox and set the real key (required) Register `https://` as a merchant website in the **sandbox** -environment. The backend only mints status challenges with `aud` = a -registered origin — probes silently get the decoy otherwise. If the App -Runner service is ever recreated, the URL changes: re-register. +environment — registration is what issues the merchant API key for the +site. The backend only mints status challenges with `aud` = a registered +origin — probes silently get the decoy otherwise. If the App Runner +service is ever recreated, the URL changes: re-register. + +Then replace the `placeholder` API key on the service (console → +Configuration → Edit env vars, or `aws apprunner update-service`) and wait +for the rollout to finish. ## 7. Trigger the real end-to-end probe From 7f6a10ad11043b8f745ad8bad799a88f5555f15b Mon Sep 17 00:00:00 2001 From: Tom Stark Date: Tue, 14 Jul 2026 13:55:05 +0200 Subject: [PATCH 6/9] docs: switch deployment to Fly.io, commit fly.toml (App Runner dropped over IAM friction) --- demo/self-report/DEPLOY.md | 209 ++++++++----------------------------- demo/self-report/README.md | 26 ++--- demo/self-report/fly.toml | 26 +++++ 3 files changed, 78 insertions(+), 183 deletions(-) create mode 100644 demo/self-report/fly.toml diff --git a/demo/self-report/DEPLOY.md b/demo/self-report/DEPLOY.md index f7fe348..39fca3d 100644 --- a/demo/self-report/DEPLOY.md +++ b/demo/self-report/DEPLOY.md @@ -1,197 +1,80 @@ -# Deploying the Self-Report Demo Site to AWS App Runner +# Deploying the Self-Report Demo Site (Fly.io) -Steps 1–5 happen once; step 8 is the recurring update path. +The live instance runs at **https://supertab-self-report-demo.fly.dev** — +one always-warm 256 MB machine (~$2–3/mo), TLS automatic, remote builds +(no local arch concerns). `fly.toml` in this directory is the canonical +config; it pins `auto_stop_machines = 'off'` / `min_machines_running = 1` +because a probe target must never cold-start. -## 0. Prerequisites +> An AWS App Runner variant of this runbook existed previously; it was +> dropped after IAM friction (`iam:PassRole`) — see git history if ever +> needed. -- AWS CLI v2 authenticated against the **Supertab** account (see - [Multiple AWS accounts](#multiple-aws-accounts-named-profiles) below if - your default profile points at another org), Docker Desktop running. -- A **sandbox merchant API key** for Supertab Connect. -- This directory checked out locally (branch `feat/self-report-demo-site`, - or `main` once PR #23 merges). +## One-time setup ```bash -export AWS_PROFILE=supertab # if using a named profile — set BEFORE the next line -export AWS_ACCOUNT=$(aws sts get-caller-identity --query Account --output text) -export AWS_REGION=eu-central-1 # pick your region -export REPO=$AWS_ACCOUNT.dkr.ecr.$AWS_REGION.amazonaws.com/supertab-self-report-demo -``` - -Sanity check you're in the right account before creating anything: -`aws sts get-caller-identity`. - -## 1. Create the ECR repository (once) - -```bash -aws ecr create-repository \ - --repository-name supertab-self-report-demo \ - --region $AWS_REGION -``` - -## 2. Build and push the image +brew install flyctl +flyctl auth login # browser flow (signup included) -```bash cd demo/self-report -aws ecr get-login-password --region $AWS_REGION | \ - docker login --username AWS --password-stdin $REPO -docker build --platform linux/amd64 -t ${REPO}:latest . -docker push ${REPO}:latest -``` - -`--platform linux/amd64` matters on Apple Silicon — App Runner runs x86_64. - -## 3. Create the ECR access role (once) - -App Runner needs an IAM role to pull from private ECR. If the account -doesn't already have `AppRunnerECRAccessRole`: - -```bash -aws iam create-role --role-name AppRunnerECRAccessRole \ - --assume-role-policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"build.apprunner.amazonaws.com"},"Action":"sts:AssumeRole"}]}' -aws iam attach-role-policy --role-name AppRunnerECRAccessRole \ - --policy-arn arn:aws:iam::aws:policy/service-role/AWSAppRunnerServicePolicyForECRAccess -``` - -(The policy JSON is deliberately one line: copy-pasting indented JSON from -rendered docs can smuggle in non-breaking spaces, which IAM rejects as -`MalformedPolicyDocument`.) - -## 4. Create the App Runner service (once) - -Write the source configuration to a file first (the heredoc expands -`$AWS_ACCOUNT`/`$REPO` for you, and `file://` input sidesteps shell-quoting -and copy-paste whitespace issues): - -```bash -cat > /tmp/apprunner-source.json < +HOST=supertab-self-report-demo.fly.dev curl -s https://$HOST/healthz # → ok curl -si https://$HOST/.well-known/supertab/status | head -5 # → 404 {"supertab":true}, no-store -curl -s https://$HOST/ | grep "SDK version" # → v1.4.0-beta.9 +curl -s https://$HOST/ | grep "SDK version" # → the pinned SDK version ``` -## 6. Register the site in sandbox and set the real key (required) +## Register the site in sandbox and set the real key (required) -Register `https://` as a merchant website in the **sandbox** -environment — registration is what issues the merchant API key for the -site. The backend only mints status challenges with `aud` = a registered -origin — probes silently get the decoy otherwise. If the App Runner -service is ever recreated, the URL changes: re-register. +1. Register `https://supertab-self-report-demo.fly.dev` as a merchant + website in the **sandbox** environment — registration issues the + merchant API key. The backend only mints status challenges with `aud` = + a registered origin; unregistered probes silently get the decoy. +2. Swap in the real key (this alone triggers a redeploy, ~30 s): -Then replace the `placeholder` API key on the service (console → -Configuration → Edit env vars, or `aws apprunner update-service`) and wait -for the rollout to finish. + ```bash + flyctl secrets set SUPERTAB_MERCHANT_API_KEY= + ``` -## 7. Trigger the real end-to-end probe +## The end-to-end probe -Fire a live-health check for the registered site (the backend's -`self_report` check). Expected: `200` with `runtime: null`, -`sdkVersion: "v1.4.0-beta.9"`, -`component: {kind: "php-sdk", version: "v1.4.0-beta.9"}`, -`enforcement: "observe"`, `eventReporting: true`. +Fire a backend live-health check (`self_report`) for the registered site. +Expected: `200` with `runtime: null`, `sdkVersion`, +`component: {kind: "php-sdk", version}`, `enforcement: "observe"`, +`eventReporting: true`. Note: the backend resolves only `ts-sdk` against a registry so far (laterpay/supertab-connect#1094); `php-sdk` degrades to "show version, no -nudge" until its resolver lands. That's expected, not a failure. +nudge" until its resolver lands. Expected, not a failure. -## 8. Updating (each new SDK release) +## Updating (each new SDK release) ```bash cd demo/self-report # bump the pin in composer.json, then: composer update getsupertab/connect-sdk-php -aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $REPO -docker build --platform linux/amd64 -t ${REPO}:latest . && docker push ${REPO}:latest -``` - -Auto-deployment picks up the push and redeploys (~1–2 min). Commit the pin -+ lockfile change back to the repo. - -**Config experiments** (no rebuild): edit `SUPERTAB_ENFORCEMENT` / -`SUPERTAB_ANALYTICS` on the service (console → Configuration → Edit, or -`aws apprunner update-service`) — the next probe reflects the new values. - -**Cost**: ~$7/mo idle (0.25 vCPU / 0.5 GB provisioned) + a few cents ECR -storage. Delete with `aws apprunner delete-service` when no longer needed. - -## Multiple AWS accounts (named profiles) - -Keep your other org's CLI setup untouched by adding a named profile: - -```bash -# Static access keys: -aws configure --profile supertab - -# Or IAM Identity Center / SSO (typical for org accounts): -aws configure sso --profile supertab -# later sessions: aws sso login --profile supertab +flyctl deploy --ha=false ``` -Then either pass `--profile supertab` per command, or activate it for the -shell session (what step 0 assumes): +Commit the pin + lockfile change back to the repo. -```bash -export AWS_PROFILE=supertab -aws sts get-caller-identity # verify the account ID before creating resources -``` +**Config experiments** (no rebuild): `flyctl secrets set +SUPERTAB_ENFORCEMENT=enforce` (or `SUPERTAB_ANALYTICS=0`, +`SUPERTAB_BASE_URL=…`) — each set redeploys, and the next probe reflects +the new values. -Docker inherits the choice automatically: `aws ecr get-login-password` -issues the token from the active profile, and `docker login`/`push` just -use that token. The one ordering gotcha: export `AWS_PROFILE` **before** -deriving `AWS_ACCOUNT` in step 0, since the account ID gets baked into -`$REPO`. +**Ops one-liners**: `flyctl status` (machine state), `flyctl logs` +(live tail), `flyctl apps destroy supertab-self-report-demo` (teardown). diff --git a/demo/self-report/README.md b/demo/self-report/README.md index e34409e..007b04d 100644 --- a/demo/self-report/README.md +++ b/demo/self-report/README.md @@ -35,31 +35,17 @@ curl -si localhost:8080/.well-known/supertab/status | head -5 # → 404 {"supert curl -s localhost:8080/ | head -3 # → demo HTML page ``` -## Deploy to AWS App Runner +## Deploy -App Runner has no managed PHP runtime, so it deploys from a container image -in ECR. - -```bash -AWS_ACCOUNT= AWS_REGION= -REPO=$AWS_ACCOUNT.dkr.ecr.$AWS_REGION.amazonaws.com/supertab-self-report-demo - -aws ecr create-repository --repository-name supertab-self-report-demo --region $AWS_REGION -aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $REPO -docker build --platform linux/amd64 -t ${REPO}:latest . -docker push ${REPO}:latest -``` - -Create the service (console or CLI): **source** = the ECR image with -auto-deployment on push, **port** = `8080`, **size** = 0.25 vCPU / 512 MB, -**health check** = HTTP on `/healthz`, and the env vars above (at minimum -`SUPERTAB_MERCHANT_API_KEY`). +See [DEPLOY.md](DEPLOY.md) — the site runs on Fly.io +(https://supertab-self-report-demo.fly.dev), one always-warm machine, +`fly.toml` committed here. ## Register the site (required for probes) The backend only mints status challenges (`aud` = origin) for origins it -knows. Register the service URL — `https://..awsapprunner.com` -— as a merchant website in **sandbox**. If the URL changes (service +knows. Register the service URL — `https://supertab-self-report-demo.fly.dev` +— as a merchant website in **sandbox**. If the URL changes (app recreated), re-register. ## Probe flow diff --git a/demo/self-report/fly.toml b/demo/self-report/fly.toml new file mode 100644 index 0000000..1c07dc8 --- /dev/null +++ b/demo/self-report/fly.toml @@ -0,0 +1,26 @@ +# Fly.io config for the self-report demo site (see DEPLOY.md). +# Always-warm single machine: this is a probe target — it must never +# scale to zero or backend status probes would hit cold starts. + +app = 'supertab-self-report-demo' +primary_region = 'fra' + +[build] + +[http_service] + internal_port = 8080 + force_https = true + auto_stop_machines = 'off' + auto_start_machines = true + min_machines_running = 1 + + [[http_service.checks]] + interval = '30s' + timeout = '5s' + grace_period = '10s' + method = 'GET' + path = '/healthz' + +[[vm]] + size = 'shared-cpu-1x' + memory = '256mb' From f8f79dc02ebafa1dbcaac583d547082dd66bf669 Mon Sep 17 00:00:00 2001 From: Tom Stark Date: Tue, 14 Jul 2026 14:08:30 +0200 Subject: [PATCH 7/9] =?UTF-8?q?fix:=20restore=20Authorization=20header=20u?= =?UTF-8?q?nder=20Apache=20mod=5Fphp=20(SetEnvIf)=20=E2=80=94=20SDK=20read?= =?UTF-8?q?s=20only=20$=5FSERVER?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- demo/self-report/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/self-report/Dockerfile b/demo/self-report/Dockerfile index 42e2664..bac14ce 100644 --- a/demo/self-report/Dockerfile +++ b/demo/self-report/Dockerfile @@ -10,7 +10,7 @@ FROM php:8.3-apache # isn't an existing file to the front controller. RUN sed -i 's/^Listen 80$/Listen 8080/' /etc/apache2/ports.conf \ && sed -i 's///' /etc/apache2/sites-available/000-default.conf \ - && printf 'FallbackResource /index.php\n' > /etc/apache2/conf-available/fallback.conf \ + && printf 'FallbackResource /index.php\nSetEnvIf Authorization "(.+)" HTTP_AUTHORIZATION=$1\n' > /etc/apache2/conf-available/fallback.conf \ && a2enconf fallback COPY --from=deps /app/vendor /var/www/html/vendor From 72795c248075340ceb1372899b86b7ae6c87fb10 Mon Sep 17 00:00:00 2001 From: Tom Stark Date: Tue, 14 Jul 2026 14:54:52 +0200 Subject: [PATCH 8/9] =?UTF-8?q?docs:=20address=20review=20=E2=80=94=20prov?= =?UTF-8?q?ider-agnostic=20comments,=20case-insensitive=20SUPERTAB=5FANALY?= =?UTF-8?q?TICS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- demo/self-report/Dockerfile | 5 +++-- demo/self-report/index.php | 12 ++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/demo/self-report/Dockerfile b/demo/self-report/Dockerfile index bac14ce..41deea9 100644 --- a/demo/self-report/Dockerfile +++ b/demo/self-report/Dockerfile @@ -6,8 +6,9 @@ RUN composer install --no-dev --no-interaction --no-progress FROM php:8.3-apache -# App Runner's default container port is 8080; route every request that -# isn't an existing file to the front controller. +# The app listens on 8080 (see fly.toml's internal_port; also the default +# for most container platforms); route every request that isn't an +# existing file to the front controller. RUN sed -i 's/^Listen 80$/Listen 8080/' /etc/apache2/ports.conf \ && sed -i 's///' /etc/apache2/sites-available/000-default.conf \ && printf 'FallbackResource /index.php\nSetEnvIf Authorization "(.+)" HTTP_AUTHORIZATION=$1\n' > /etc/apache2/conf-available/fallback.conf \ diff --git a/demo/self-report/index.php b/demo/self-report/index.php index 965fabb..74030a0 100644 --- a/demo/self-report/index.php +++ b/demo/self-report/index.php @@ -20,11 +20,11 @@ use Supertab\Connect\SupertabConnect; // ── Proxy scheme fix-up ────────────────────────────────────────────── -// App Runner terminates TLS and forwards plain HTTP with -// X-Forwarded-Proto: https. Fix $_SERVER before anything derives the -// request origin — otherwise the SDK sees http://…, the backend-minted -// challenge audience (https://…) never matches, and every status probe -// gets the 404 decoy. +// TLS-terminating proxies (Fly.io, App Runner, most PaaS) forward plain +// HTTP with X-Forwarded-Proto: https. Fix $_SERVER before anything +// derives the request origin — otherwise the SDK sees http://…, the +// backend-minted challenge audience (https://…) never matches, and every +// status probe gets the 404 decoy. if (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https') { $_SERVER['HTTPS'] = 'on'; } @@ -49,7 +49,7 @@ $baseUrl = getenv('SUPERTAB_BASE_URL') ?: 'https://api-connect.sbx.supertab.co'; $enforcement = EnforcementMode::tryFrom(getenv('SUPERTAB_ENFORCEMENT') ?: '') ?? EnforcementMode::OBSERVE; -$analytics = ! in_array(getenv('SUPERTAB_ANALYTICS'), ['0', 'false', 'off'], true); +$analytics = ! in_array(strtolower((string) getenv('SUPERTAB_ANALYTICS')), ['0', 'false', 'off'], true); $connect = new SupertabConnect( apiKey: $apiKey, From 83690077b7a902e889d5e98cb189b8bf3ae9ad18 Mon Sep 17 00:00:00 2001 From: Tom Stark Date: Tue, 14 Jul 2026 15:06:34 +0200 Subject: [PATCH 9/9] docs: keep the live deployment hostname out of the repo (fly.toml.example + placeholders) --- demo/self-report/.gitignore | 1 + demo/self-report/DEPLOY.md | 20 ++++++++++--------- demo/self-report/README.md | 12 +++++------ .../{fly.toml => fly.toml.example} | 7 +++++-- 4 files changed, 23 insertions(+), 17 deletions(-) rename demo/self-report/{fly.toml => fly.toml.example} (63%) diff --git a/demo/self-report/.gitignore b/demo/self-report/.gitignore index 48b8bf9..ed2a291 100644 --- a/demo/self-report/.gitignore +++ b/demo/self-report/.gitignore @@ -1 +1,2 @@ vendor/ +fly.toml diff --git a/demo/self-report/DEPLOY.md b/demo/self-report/DEPLOY.md index 39fca3d..b3f42fe 100644 --- a/demo/self-report/DEPLOY.md +++ b/demo/self-report/DEPLOY.md @@ -1,10 +1,11 @@ # Deploying the Self-Report Demo Site (Fly.io) -The live instance runs at **https://supertab-self-report-demo.fly.dev** — -one always-warm 256 MB machine (~$2–3/mo), TLS automatic, remote builds -(no local arch concerns). `fly.toml` in this directory is the canonical -config; it pins `auto_stop_machines = 'off'` / `min_machines_running = 1` -because a probe target must never cold-start. +The live instance is one always-warm 256 MB machine (~$2–3/mo), TLS +automatic, remote builds (no local arch concerns). Its hostname is +deliberately kept out of this public repo — find it in the sandbox +merchant-site registration (or ask the team). `fly.toml.example` is the +canonical config template; it pins `auto_stop_machines = 'off'` / +`min_machines_running = 1` because a probe target must never cold-start. > An AWS App Runner variant of this runbook existed previously; it was > dropped after IAM friction (`iam:PassRole`) — see git history if ever @@ -17,7 +18,8 @@ brew install flyctl flyctl auth login # browser flow (signup included) cd demo/self-report -flyctl apps create supertab-self-report-demo +cp fly.toml.example fly.toml # fly.toml is gitignored — app name stays local +flyctl apps create # then set the same name in fly.toml flyctl secrets set SUPERTAB_MERCHANT_API_KEY=placeholder SUPERTAB_ENFORCEMENT=observe --stage flyctl deploy --ha=false # single machine; fly.toml does the rest ``` @@ -31,7 +33,7 @@ analytics delivery would 401-and-drop. ## Smoke test ```bash -HOST=supertab-self-report-demo.fly.dev +HOST=.fly.dev curl -s https://$HOST/healthz # → ok curl -si https://$HOST/.well-known/supertab/status | head -5 # → 404 {"supertab":true}, no-store curl -s https://$HOST/ | grep "SDK version" # → the pinned SDK version @@ -39,7 +41,7 @@ curl -s https://$HOST/ | grep "SDK version" # → the pinned ## Register the site in sandbox and set the real key (required) -1. Register `https://supertab-self-report-demo.fly.dev` as a merchant +1. Register `https://.fly.dev` as a merchant website in the **sandbox** environment — registration issues the merchant API key. The backend only mints status challenges with `aud` = a registered origin; unregistered probes silently get the decoy. @@ -77,4 +79,4 @@ SUPERTAB_ENFORCEMENT=enforce` (or `SUPERTAB_ANALYTICS=0`, the new values. **Ops one-liners**: `flyctl status` (machine state), `flyctl logs` -(live tail), `flyctl apps destroy supertab-self-report-demo` (teardown). +(live tail), `flyctl apps destroy ` (teardown). diff --git a/demo/self-report/README.md b/demo/self-report/README.md index 007b04d..56ef11a 100644 --- a/demo/self-report/README.md +++ b/demo/self-report/README.md @@ -37,16 +37,16 @@ curl -s localhost:8080/ | head -3 # → demo HTML pa ## Deploy -See [DEPLOY.md](DEPLOY.md) — the site runs on Fly.io -(https://supertab-self-report-demo.fly.dev), one always-warm machine, -`fly.toml` committed here. +See [DEPLOY.md](DEPLOY.md) — the site runs on Fly.io as one always-warm +machine (`fly.toml.example` committed here; the live hostname is kept out +of the repo — see the sandbox merchant-site registration). ## Register the site (required for probes) The backend only mints status challenges (`aud` = origin) for origins it -knows. Register the service URL — `https://supertab-self-report-demo.fly.dev` -— as a merchant website in **sandbox**. If the URL changes (app -recreated), re-register. +knows. Register the service URL — `https://.fly.dev` — as a +merchant website in **sandbox**. If the URL changes (app recreated), +re-register. ## Probe flow diff --git a/demo/self-report/fly.toml b/demo/self-report/fly.toml.example similarity index 63% rename from demo/self-report/fly.toml rename to demo/self-report/fly.toml.example index 1c07dc8..60fde76 100644 --- a/demo/self-report/fly.toml +++ b/demo/self-report/fly.toml.example @@ -1,8 +1,11 @@ -# Fly.io config for the self-report demo site (see DEPLOY.md). +# Fly.io config template for the self-report demo site (see DEPLOY.md). +# Copy to fly.toml and set your app name (the real deployment's name/URL +# is deliberately kept out of the repo): +# cp fly.toml.example fly.toml && fly apps create # Always-warm single machine: this is a probe target — it must never # scale to zero or backend status probes would hit cold starts. -app = 'supertab-self-report-demo' +app = 'REPLACE-WITH-YOUR-APP-NAME' primary_region = 'fra' [build]