diff --git a/demo-deploy/docker-compose.yml b/demo-deploy/docker-compose.yml
index 0f657af0..64ea99cd 100644
--- a/demo-deploy/docker-compose.yml
+++ b/demo-deploy/docker-compose.yml
@@ -13,6 +13,8 @@
# IMAGE_TAG image tag to pull (default: demo)
# ANTHROPIC_API_KEY optional — enables the AI chat panel
# DEMO_WEB_PORT host port for the UI (default 2100)
+# DEMO_INDEE_PORT host port for the IndeeHub demo proxy (default 2101;
+# must stay 2101 unless the UI's demoAppUrl changes)
services:
neode-backend:
@@ -42,6 +44,8 @@ services:
container_name: archy-demo-web
ports:
- "${DEMO_WEB_PORT:-2100}:80"
+ # IndeeHub whole-origin demo proxy (nginx :2101 in the web image)
+ - "${DEMO_INDEE_PORT:-2101}:2101"
environment:
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
depends_on:
diff --git a/docker-compose.demo.yml b/docker-compose.demo.yml
index cb083fd7..bca2b7c3 100644
--- a/docker-compose.demo.yml
+++ b/docker-compose.demo.yml
@@ -47,6 +47,8 @@ services:
container_name: archy-demo-web
ports:
- "2100:80"
+ # IndeeHub whole-origin demo proxy (nginx :2101 in nginx-demo.conf)
+ - "2101:2101"
depends_on:
- neode-backend
restart: unless-stopped
diff --git a/neode-ui/Dockerfile.web b/neode-ui/Dockerfile.web
index 9eafa714..ff00b812 100644
--- a/neode-ui/Dockerfile.web
+++ b/neode-ui/Dockerfile.web
@@ -57,9 +57,12 @@ COPY neode-ui/docker/nginx-demo.conf /etc/nginx/nginx.conf.template
COPY neode-ui/docker/docker-entrypoint.sh /docker-entrypoint-custom.sh
RUN chmod +x /docker-entrypoint-custom.sh
+# IndeeHub demo sign-in seeder, injected by the :2101 whole-origin proxy
+# (nginx-demo.conf). Demo image only — never in real-node artifacts.
+COPY neode-ui/docker/indee-demo-signin.js /usr/share/nginx/html/__demo/indee-demo-signin.js
-# Expose port
-EXPOSE 80
+# Expose ports (80 = demo UI, 2101 = IndeeHub whole-origin demo proxy)
+EXPOSE 80 2101
# Substitute ANTHROPIC_API_KEY at runtime, then start nginx
ENTRYPOINT ["/docker-entrypoint-custom.sh"]
diff --git a/neode-ui/docker/indee-demo-signin.js b/neode-ui/docker/indee-demo-signin.js
new file mode 100644
index 00000000..4be0171c
--- /dev/null
+++ b/neode-ui/docker/indee-demo-signin.js
@@ -0,0 +1,58 @@
+/**
+ * PUBLIC-DEMO-ONLY IndeeHub sign-in seeder.
+ *
+ * Served at /__demo/indee-demo-signin.js on the :2101 IndeeHub whole-origin
+ * demo proxy (see nginx-demo.conf) and injected into the proxied site's HTML
+ *
via sub_filter. It only ever runs on the demo's :2101 origin inside
+ * the demo iframe — it never ships in real-node artifacts.
+ *
+ * THROWAWAY DEMO IDENTITY — NOT A SECRET. The embedded secp256k1 keypair was
+ * freshly generated for the public demo (2026-07-29) and has never belonged
+ * to any real user. Its whole purpose is to be a shared, public "demo
+ * visitor" Nostr identity so the embedded IndeeHub boots signed in with no
+ * login wall. Anyone extracting this key can only impersonate the demo
+ * visitor, by design (threat T-gjd-01: accepted).
+ *
+ * How it works: IndeeHub's bundle (applesauce-accounts) restores accounts on
+ * boot from localStorage key "indeedhub-accounts" (JSON array of serialized
+ * accounts; a "nsec" private-key account deserializes as
+ * { id, type: "nsec", pubkey, metadata, signer: { key: } }) and
+ * activates the account whose id is stored under "indeedhub-active-account".
+ * This classic script executes before the SPA's deferred module bundle, so
+ * seeding here is visible to that boot-restore. Seeding is idempotent: an
+ * existing non-empty account list is never overwritten.
+ */
+;(function () {
+ 'use strict'
+
+ var ACCOUNTS_KEY = 'indeedhub-accounts'
+ var ACTIVE_KEY = 'indeedhub-active-account'
+
+ // Throwaway demo keypair (see header — public by design, not a secret).
+ var DEMO_SK_HEX = 'ce2ffa96f99968beffc789cbba5d8b52f4a3020454dcaf77c2b553961bf5a8c9'
+ var DEMO_PK_HEX = '7261540160244ec65ce0bf86ba03997e9b1b3b35c277e416bf1c7ba4271fee31'
+ var DEMO_ACCOUNT_ID = 'archy-demo-visitor'
+
+ try {
+ var existing = null
+ try {
+ existing = JSON.parse(localStorage.getItem(ACCOUNTS_KEY))
+ } catch (e) {
+ existing = null
+ }
+ if (Array.isArray(existing) && existing.length > 0) return
+
+ var account = {
+ id: DEMO_ACCOUNT_ID,
+ type: 'nsec',
+ pubkey: DEMO_PK_HEX,
+ metadata: { name: 'Archy Demo' },
+ signer: { key: DEMO_SK_HEX },
+ }
+ localStorage.setItem(ACCOUNTS_KEY, JSON.stringify([account]))
+ localStorage.setItem(ACTIVE_KEY, DEMO_ACCOUNT_ID)
+ } catch (e) {
+ // localStorage unavailable (e.g. blocked third-party storage) — the demo
+ // visitor just sees IndeeHub's normal signed-out state.
+ }
+})()
diff --git a/neode-ui/docker/nginx-demo.conf b/neode-ui/docker/nginx-demo.conf
index 0c2a8c3b..392b9d00 100644
--- a/neode-ui/docker/nginx-demo.conf
+++ b/neode-ui/docker/nginx-demo.conf
@@ -103,10 +103,14 @@ http {
proxy_request_buffering off;
}
- # IndeeHub is no longer proxied same-origin — the sub_filter rewrite
- # approach broke the SPA's runtime-built asset URLs. The demo now opens
- # the real site (https://indee.tx1138.com/) externally instead, via
- # DEMO_EXTERNAL_URLS in useDemoIntro.ts.
+ # IndeeHub is NOT proxied under a /app/indeedhub/ path prefix — the
+ # old sub_filter path-rewrite approach broke the SPA's runtime-built
+ # absolute-root asset URLs. Instead, a dedicated WHOLE-ORIGIN reverse
+ # proxy of https://indee.tx1138.com listens on :2101 (see the second
+ # server block below): the SPA sees itself at '/' so every asset and
+ # router path just works, framing headers are stripped, and a demo
+ # sign-in seed script is injected. useDemoIntro.demoAppUrl points the
+ # in-app iframe at http://:2101/.
# Mempool is NOT proxied upstream anymore — the mock backend serves a
# branded placeholder page for it (see DEMO_APP_PAGES in mock-backend.js),
@@ -177,4 +181,46 @@ http {
add_header Cache-Control "public, immutable";
}
}
+
+ # ── IndeeHub whole-origin demo proxy (:2101) ────────────────────────────
+ # Pure reverse proxy of the LIVE https://indee.tx1138.com site on its own
+ # port — no path prefix, no URL rewriting, so the SPA's absolute-root
+ # asset/router paths work untouched. The upstream's X-Frame-Options
+ # (SAMEORIGIN) and any CSP are stripped so the demo can embed it in the
+ # in-app iframe session, and a PUBLIC-DEMO-ONLY sign-in seed script is
+ # injected into the HTML (it seeds a labelled throwaway demo Nostr
+ # account into the :2101 origin's isolated localStorage, so IndeeHub boots
+ # signed in). Upstream is pinned to a single fixed hostname — this cannot
+ # be used as an open proxy.
+ server {
+ listen 2101;
+ server_name _;
+
+ # Demo sign-in seeder, served same-origin to the proxied SPA.
+ location = /__demo/indee-demo-signin.js {
+ root /usr/share/nginx/html;
+ add_header Cache-Control "no-store";
+ }
+
+ location / {
+ proxy_pass https://indee.tx1138.com;
+ proxy_ssl_server_name on;
+ proxy_ssl_name indee.tx1138.com;
+ proxy_set_header Host indee.tx1138.com;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection $connection_upgrade;
+ proxy_read_timeout 86400;
+
+ # Allow embedding in the demo's iframe session.
+ proxy_hide_header X-Frame-Options;
+ proxy_hide_header Content-Security-Policy;
+
+ # HTML injection: upstream must not compress or sub_filter no-ops.
+ # (sub_filter applies to text/html by default — exactly what we want.)
+ proxy_set_header Accept-Encoding "";
+ sub_filter_once on;
+ sub_filter '' '';
+ }
+ }
}