diff --git a/docs/03-deployment/CROMERO_DAPP_DEPLOYMENT.md b/docs/03-deployment/CROMERO_DAPP_DEPLOYMENT.md
new file mode 100644
index 00000000..72e92189
--- /dev/null
+++ b/docs/03-deployment/CROMERO_DAPP_DEPLOYMENT.md
@@ -0,0 +1,70 @@
+# CROMERO Dapp — Phoenix Deploy
+
+Deploys [`d-bis/CROMERO`](https://gitea.d-bis.org/d-bis/CROMERO) (a
+Vite + React + thirdweb v5 dapp) to
+`https://d-bis.org/ecosystem/cromero/`.
+
+## Pipeline
+
+1. Push to `main` on `d-bis/CROMERO` triggers
+ `.gitea/workflows/deploy-to-phoenix.yml`, which POSTs
+ `{repo, sha, branch, target: "default"}` to the Phoenix Deploy API.
+2. Phoenix runs the registered target (see
+ `phoenix-deploy-api/deploy-targets.json`):
+ `bash scripts/deployment/phoenix-deploy-cromero-from-workspace.sh`.
+3. The script builds the staged workspace (`npm ci && npm run build`)
+ and rsyncs `dist/` to
+ `${IP_NPMPLUS:-192.168.11.167}:/var/www/ecosystem/cromero/`.
+4. Healthcheck: `https://d-bis.org/ecosystem/cromero/` must return
+ HTTP 200 with `
` in the body.
+
+## One-time nginx setup on the NPMplus host
+
+The deploy script does **not** modify nginx config — install the
+location block once, then redeploys land static files only.
+
+Add the following to whichever nginx server block terminates
+`d-bis.org` (or the NPMplus advanced-config tab for the d-bis.org
+proxy host):
+
+```nginx
+location /ecosystem/cromero/ {
+ alias /var/www/ecosystem/cromero/;
+ try_files $uri $uri/ /ecosystem/cromero/index.html;
+}
+```
+
+Then `nginx -t && nginx -s reload` (or restart the NPMplus
+container).
+
+The Vite app already builds with `base: "/ecosystem/cromero/"` so
+hashed asset URLs resolve under that subpath.
+
+## Required Actions secrets/vars on `d-bis/CROMERO`
+
+| Name | Type | Value |
+| --- | --- | --- |
+| `PHOENIX_DEPLOY_URL` | secret | `http://192.168.11.59:4001/api/deploy` |
+| `PHOENIX_DEPLOY_TOKEN` | secret | matches `PHOENIX_DEPLOY_SECRET` on the Phoenix host |
+| `VITE_THIRDWEB_CLIENT_ID` | secret | thirdweb publishable Client ID |
+| `VITE_PROJECT_WALLET_ADDRESS` | var | recipient `0x…` address |
+| `VITE_CHAIN_138_RPC` | var (optional) | overrides default `https://rpc.d-bis.org` |
+| `VITE_CHAIN_138_EXPLORER` | var (optional) | overrides default `https://explorer.d-bis.org` |
+
+## Manual trigger from a LAN box with `phoenix-deploy-api` access
+
+```bash
+curl -sSf -X POST "http://192.168.11.59:4001/api/deploy" \
+ -H "Authorization: Bearer ${PHOENIX_DEPLOY_TOKEN}" \
+ -H "Content-Type: application/json" \
+ -d '{"repo":"d-bis/CROMERO","branch":"main","target":"default"}'
+```
+
+## Dry run
+
+From this repo:
+
+```bash
+PHOENIX_DEPLOY_WORKSPACE=/path/to/staged/CROMERO \
+ bash scripts/deployment/phoenix-deploy-cromero-from-workspace.sh --dry-run
+```
diff --git a/phoenix-deploy-api/deploy-targets.json b/phoenix-deploy-api/deploy-targets.json
index 5f9ffa01..f776fae5 100644
--- a/phoenix-deploy-api/deploy-targets.json
+++ b/phoenix-deploy-api/deploy-targets.json
@@ -125,6 +125,29 @@
"timeout_ms": 15000
}
},
+ {
+ "repo": "d-bis/CROMERO",
+ "branch": "main",
+ "target": "default",
+ "description": "Deploy CROMERO dapp from the staged Gitea workspace: build dist/, rsync to NPMplus host /var/www/ecosystem/cromero/, served at https://d-bis.org/ecosystem/cromero/.",
+ "cwd": "${PHOENIX_REPO_ROOT}",
+ "command": [
+ "bash",
+ "scripts/deployment/phoenix-deploy-cromero-from-workspace.sh"
+ ],
+ "required_env": [
+ "PHOENIX_REPO_ROOT",
+ "PHOENIX_DEPLOY_WORKSPACE"
+ ],
+ "healthcheck": {
+ "url": "https://d-bis.org/ecosystem/cromero/",
+ "expect_status": 200,
+ "expect_body_includes": "
",
+ "attempts": 12,
+ "delay_ms": 5000,
+ "timeout_ms": 15000
+ }
+ },
{
"repo": "d-bis/proxmox",
"branch": "main",
diff --git a/scripts/deployment/phoenix-deploy-cromero-from-workspace.sh b/scripts/deployment/phoenix-deploy-cromero-from-workspace.sh
new file mode 100755
index 00000000..80503790
--- /dev/null
+++ b/scripts/deployment/phoenix-deploy-cromero-from-workspace.sh
@@ -0,0 +1,131 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Deploy d-bis/CROMERO (Vite + thirdweb v5 dapp) from the Phoenix-staged
+# workspace to the NPMplus host's /var/www/ecosystem/cromero/, served by
+# nginx under https://d-bis.org/ecosystem/cromero/.
+#
+# Phoenix Deploy API target:
+# { repo: "d-bis/CROMERO", branch: "main", target: "default" }
+#
+# Required env (from Phoenix host):
+# PHOENIX_DEPLOY_WORKSPACE Full staged CROMERO checkout
+#
+# Optional env (sane defaults from config/ip-addresses.conf):
+# NPMPLUS_HOST Default: ${IP_NPMPLUS:-192.168.11.167}
+# NPMPLUS_SSH_USER Default: root
+# NPMPLUS_DEPLOY_ROOT Default: /var/www/ecosystem/cromero
+# PUBLIC_URL Default: https://d-bis.org/ecosystem/cromero/
+# VITE_THIRDWEB_CLIENT_ID Build-time only; safe to ship.
+# VITE_PROJECT_WALLET_ADDRESS
+# VITE_CHAIN_138_RPC Default in app: https://rpc.d-bis.org
+# VITE_CHAIN_138_EXPLORER Default in app: https://explorer.d-bis.org
+# DRY_RUN=1 Print actions without executing them.
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
+# shellcheck source=/dev/null
+source "$PROJECT_ROOT/scripts/lib/load-project-env.sh"
+# shellcheck source=/dev/null
+source "$PROJECT_ROOT/config/ip-addresses.conf" 2>/dev/null || true
+
+PHOENIX_DEPLOY_WORKSPACE="${PHOENIX_DEPLOY_WORKSPACE:-}"
+NPMPLUS_HOST="${NPMPLUS_HOST:-${IP_NPMPLUS:-192.168.11.167}}"
+NPMPLUS_SSH_USER="${NPMPLUS_SSH_USER:-root}"
+NPMPLUS_DEPLOY_ROOT="${NPMPLUS_DEPLOY_ROOT:-/var/www/ecosystem/cromero}"
+PUBLIC_URL="${PUBLIC_URL:-https://d-bis.org/ecosystem/cromero/}"
+DRY_RUN="${DRY_RUN:-0}"
+
+usage() {
+ cat <<'USAGE'
+Usage: phoenix-deploy-cromero-from-workspace.sh [--dry-run]
+
+Builds the staged CROMERO workspace, rsyncs the dist/ output to the
+NPMplus host's /var/www/ecosystem/cromero/, and verifies the public
+URL.
+
+The nginx location block must be installed once on the host (out of
+band) — see docs/03-deployment/CROMERO_DAPP_DEPLOYMENT.md.
+USAGE
+}
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --dry-run) DRY_RUN=1; shift ;;
+ -h|--help) usage; exit 0 ;;
+ *) echo "unknown arg: $1" >&2; usage; exit 2 ;;
+ esac
+done
+
+log() { printf '[cromero-phoenix] %s\n' "$*" >&2; }
+die() { printf '[cromero-phoenix][FATAL] %s\n' "$*" >&2; exit 1; }
+run() {
+ if [[ "$DRY_RUN" -eq 1 ]]; then
+ printf '[dry-run] %s\n' "$*" >&2
+ else
+ eval "$*"
+ fi
+}
+need_cmd() { command -v "$1" >/dev/null 2>&1 || die "missing required command: $1"; }
+
+for cmd in ssh rsync tar curl jq mktemp node npm; do
+ need_cmd "$cmd"
+done
+
+[[ -n "$PHOENIX_DEPLOY_WORKSPACE" ]] || die "PHOENIX_DEPLOY_WORKSPACE is required"
+[[ -d "$PHOENIX_DEPLOY_WORKSPACE" ]] || die "staged workspace missing: $PHOENIX_DEPLOY_WORKSPACE"
+
+SSH_TARGET="${NPMPLUS_SSH_USER}@${NPMPLUS_HOST}"
+SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=15 -o StrictHostKeyChecking=accept-new)
+
+log "building CROMERO from staged workspace: ${PHOENIX_DEPLOY_WORKSPACE}"
+pushd "$PHOENIX_DEPLOY_WORKSPACE" >/dev/null
+
+if [[ -f package-lock.json ]]; then
+ run "npm ci --no-audit --no-fund"
+else
+ run "npm install --no-audit --no-fund"
+fi
+
+# Build env. Empty values are fine — src/config.ts has defaults for
+# Chain 138 RPC/explorer, and the dapp prints in-UI banners when the
+# Client ID or wallet address are missing.
+run "VITE_THIRDWEB_CLIENT_ID='${VITE_THIRDWEB_CLIENT_ID:-}' \
+ VITE_PROJECT_WALLET_ADDRESS='${VITE_PROJECT_WALLET_ADDRESS:-}' \
+ VITE_CHAIN_138_RPC='${VITE_CHAIN_138_RPC:-}' \
+ VITE_CHAIN_138_EXPLORER='${VITE_CHAIN_138_EXPLORER:-}' \
+ npm run build"
+
+[[ -f dist/index.html ]] || die "build produced no dist/index.html"
+
+popd >/dev/null
+
+log "rsync dist/ -> ${SSH_TARGET}:${NPMPLUS_DEPLOY_ROOT}/"
+run "ssh ${SSH_OPTS[*]} '${SSH_TARGET}' \"mkdir -p '${NPMPLUS_DEPLOY_ROOT}'\""
+run "rsync -az --delete -e 'ssh ${SSH_OPTS[*]}' \
+ '${PHOENIX_DEPLOY_WORKSPACE}/dist/' \
+ '${SSH_TARGET}:${NPMPLUS_DEPLOY_ROOT}/'"
+
+# Reload nginx if a config file matching cromero exists. We do NOT
+# write the location block from this script — it is installed once,
+# out of band (see CROMERO_DAPP_DEPLOYMENT.md). Skip silently if
+# nginx is not present (NPMplus may run nginx inside a container).
+log "attempting nginx reload on ${NPMPLUS_HOST} (best-effort)"
+run "ssh ${SSH_OPTS[*]} '${SSH_TARGET}' '
+ if command -v nginx >/dev/null 2>&1; then
+ nginx -t && nginx -s reload || true
+ fi
+ if command -v docker >/dev/null 2>&1; then
+ docker exec npmplus nginx -s reload >/dev/null 2>&1 || true
+ fi
+'"
+
+log "verifying ${PUBLIC_URL}"
+HTTP_STATUS="$(curl -sS -o /dev/null -m 15 -w '%{http_code}' "$PUBLIC_URL" || echo 000)"
+if [[ "$HTTP_STATUS" == "200" ]]; then
+ log "OK: ${PUBLIC_URL} returned 200"
+else
+ log "WARN: ${PUBLIC_URL} returned ${HTTP_STATUS} (nginx location block may not yet be installed)"
+fi
+
+log "CROMERO Phoenix deploy completed from ${PHOENIX_DEPLOY_WORKSPACE}"