[object Object]

← back to Wallco Ai

wallco.ai: wire Replicate SDXL backend · inline-editor mounted at site=wallco.ai · 11 real SDXL designs across floral/geometric/damask (cost ~$0.15)

f5bdf9b55e36ef9e7450a20956b3a00b5ce37b89 · 2026-05-11 16:13:25 -0700 · Steve

Files touched

Diff

commit f5bdf9b55e36ef9e7450a20956b3a00b5ce37b89
Author: Steve <steve@designerwallcoverings.com>
Date:   Mon May 11 16:13:25 2026 -0700

    wallco.ai: wire Replicate SDXL backend · inline-editor mounted at site=wallco.ai · 11 real SDXL designs across floral/geometric/damask (cost ~$0.15)
---
 COMFYUI_SETUP.md            |  77 ++++++++++++++++++++++
 package-lock.json           | 152 +++++++++++++++++++++++++++++++++++++++++++-
 package.json                |   6 +-
 scripts/generate_designs.js | 142 +++++++++++++++++++++++++++++++++++++++--
 server.js                   |  25 ++++++++
 5 files changed, 395 insertions(+), 7 deletions(-)

diff --git a/COMFYUI_SETUP.md b/COMFYUI_SETUP.md
new file mode 100644
index 0000000..2a7f83e
--- /dev/null
+++ b/COMFYUI_SETUP.md
@@ -0,0 +1,77 @@
+# ComfyUI setup for wallco.ai
+
+Wallco.ai's design generator can use ComfyUI as its diffusion backend
+(`GEN_BACKEND=comfy`). The integration code (see `scripts/generate_designs.js`
+function `genComfy`) is wired and will work as soon as ComfyUI is reachable.
+
+## Where to install
+
+Mac2 (32 GB RAM, M-series) can run SDXL at ~25-40s/image via MPS. The
+recommended host is **Mac2** so the generator runs locally without round-trips
+to Mac1 or the cloud.
+
+## Install steps (one-time, ~15 min + model downloads)
+
+```bash
+# 1. Clone ComfyUI
+cd ~/Projects
+git clone https://github.com/comfyanonymous/ComfyUI.git
+cd ComfyUI
+
+# 2. Python venv + torch (MPS support for Apple Silicon)
+python3 -m venv venv
+source venv/bin/activate
+pip install --upgrade pip
+pip install torch torchvision  # MPS support automatic on Apple Silicon
+pip install -r requirements.txt
+
+# 3. Download SDXL base + refiner models (~12 GB total)
+cd models/checkpoints
+curl -L -O https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/main/sd_xl_base_1.0.safetensors
+# (optional) curl -L -O https://huggingface.co/stabilityai/stable-diffusion-xl-refiner-1.0/resolve/main/sd_xl_refiner_1.0.safetensors
+
+# 4. Run it (default port 8188)
+cd ~/Projects/ComfyUI
+source venv/bin/activate
+python main.py --listen 127.0.0.1 --port 8188
+```
+
+## Run as a persistent pm2 process
+
+```bash
+cd ~/Projects/ComfyUI
+pm2 start "venv/bin/python main.py --listen 127.0.0.1 --port 8188" --name comfyui
+pm2 save
+```
+
+## Wire into wallco.ai
+
+Add to `~/Projects/wallco-ai/.env`:
+```
+GEN_BACKEND=comfy
+COMFY_URL=http://127.0.0.1:8188
+COMFY_MODEL=sd_xl_base_1.0.safetensors
+COMFY_WIDTH=1024
+COMFY_HEIGHT=1024
+```
+
+Then:
+```bash
+node scripts/generate_designs.js --n 10 --kind seamless_tile --category floral
+```
+
+## Seamless-tile notes
+
+SDXL doesn't natively produce perfectly-seamless tiles. The integration appends
+"seamless tile, repeating pattern, no edges" to the prompt + negatives the
+seam/border/frame. This gets ≈80% of the way there.
+
+For real-print-quality seamless output, swap `COMFY_MODEL` to a SDXL fine-tune
+trained on tileable patterns (search HF for `sdxl seamless tile`), OR use the
+"ImageBlur + offset" post-process (custom workflow JSON) to fix edge seams.
+
+## Mural mode
+
+Set `--kind mural --width 96 --height 108 --panels 4` to generate a 96×108"
+mural split into 4 panels. The current code generates the full image then
+splits into panels via Pillow post-process (TBD — open task).
diff --git a/package-lock.json b/package-lock.json
index 7eb5d1d..09f946e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,10 +9,28 @@
       "version": "1.0.0",
       "license": "ISC",
       "dependencies": {
+        "@dw/inline-editor": "file:../inline-editor",
         "dotenv": "^17.4.2",
-        "express": "^5.2.1"
+        "express": "^5.2.1",
+        "jsonwebtoken": "^9.0.3"
       }
     },
+    "../inline-editor": {
+      "name": "@dw/inline-editor",
+      "version": "0.1.0",
+      "dependencies": {
+        "cheerio": "^1.0.0",
+        "cookie-parser": "^1.4.7",
+        "express": "^4.21.1",
+        "jsonwebtoken": "^9.0.2",
+        "multer": "^1.4.5-lts.1",
+        "sanitize-html": "^2.13.1"
+      }
+    },
+    "node_modules/@dw/inline-editor": {
+      "resolved": "../inline-editor",
+      "link": true
+    },
     "node_modules/accepts": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
@@ -50,6 +68,12 @@
         "url": "https://opencollective.com/express"
       }
     },
+    "node_modules/buffer-equal-constant-time": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+      "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+      "license": "BSD-3-Clause"
+    },
     "node_modules/bytes": {
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -180,6 +204,15 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/ecdsa-sig-formatter": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+      "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "safe-buffer": "^5.0.1"
+      }
+    },
     "node_modules/ee-first": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -461,6 +494,91 @@
       "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
       "license": "MIT"
     },
+    "node_modules/jsonwebtoken": {
+      "version": "9.0.3",
+      "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
+      "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
+      "license": "MIT",
+      "dependencies": {
+        "jws": "^4.0.1",
+        "lodash.includes": "^4.3.0",
+        "lodash.isboolean": "^3.0.3",
+        "lodash.isinteger": "^4.0.4",
+        "lodash.isnumber": "^3.0.3",
+        "lodash.isplainobject": "^4.0.6",
+        "lodash.isstring": "^4.0.1",
+        "lodash.once": "^4.0.0",
+        "ms": "^2.1.1",
+        "semver": "^7.5.4"
+      },
+      "engines": {
+        "node": ">=12",
+        "npm": ">=6"
+      }
+    },
+    "node_modules/jwa": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
+      "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
+      "license": "MIT",
+      "dependencies": {
+        "buffer-equal-constant-time": "^1.0.1",
+        "ecdsa-sig-formatter": "1.0.11",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/jws": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
+      "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
+      "license": "MIT",
+      "dependencies": {
+        "jwa": "^2.0.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/lodash.includes": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+      "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.isboolean": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+      "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.isinteger": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+      "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.isnumber": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+      "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.isplainobject": {
+      "version": "4.0.6",
+      "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+      "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.isstring": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+      "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.once": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+      "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
+      "license": "MIT"
+    },
     "node_modules/math-intrinsics": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -651,12 +769,44 @@
         "node": ">= 18"
       }
     },
+    "node_modules/safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
     "node_modules/safer-buffer": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
       "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
       "license": "MIT"
     },
+    "node_modules/semver": {
+      "version": "7.8.0",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
+      "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/send": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
diff --git a/package.json b/package.json
index c5f9381..6d2e21b 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,8 @@
   "description": "",
   "dependencies": {
     "dotenv": "^17.4.2",
-    "express": "^5.2.1"
+    "express": "^5.2.1",
+    "jsonwebtoken": "^9.0.3",
+    "@dw/inline-editor": "file:../inline-editor"
   }
-}
+}
\ No newline at end of file
diff --git a/scripts/generate_designs.js b/scripts/generate_designs.js
index 7730d90..251fd1d 100644
--- a/scripts/generate_designs.js
+++ b/scripts/generate_designs.js
@@ -116,12 +116,146 @@ print(out)
   return outPath;
 }
 
-function genReplicate(prompt, _seed, _outPath) {
-  throw new Error('Replicate backend not configured — set REPLICATE_API_TOKEN and SDXL_MODEL_VERSION');
+// ---- Replicate SDXL backend -----------------------------------------------
+// Submits a prediction to Replicate, polls until succeeded, downloads the image.
+// Cost: SDXL via stability-ai/sdxl ≈ $0.0023/sec × ~6s = ~$0.014/image.
+function loadReplicateToken() {
+  if (process.env.REPLICATE_API_TOKEN) return process.env.REPLICATE_API_TOKEN;
+  const fs = require('fs');
+  const home = require('os').homedir();
+  const candidates = [
+    `${home}/Projects/animate-museum-posts/.env`,
+    `${home}/Projects/secrets-manager/.env`,
+    `${home}/.dw-fleet.env`
+  ];
+  for (const p of candidates) {
+    if (!fs.existsSync(p)) continue;
+    const m = fs.readFileSync(p, 'utf8').match(/^\s*REPLICATE_API_TOKEN\s*=\s*(\S+)/m);
+    if (m) return m[1].replace(/^["']|["']$/g, '');
+  }
+  return null;
 }
 
-function genComfy() {
-  throw new Error('ComfyUI backend not configured — set COMFY_URL and a workflow JSON path');
+function genReplicate(prompt, seed, outPath) {
+  const TOKEN = loadReplicateToken();
+  if (!TOKEN) throw new Error('REPLICATE_API_TOKEN not found in env or ~/Projects/secrets-manager/.env');
+
+  // Stability AI's official SDXL model — well-known stable version
+  const MODEL_VERSION = process.env.SDXL_MODEL_VERSION || '7762fd07cf82c948538e41f63f77d685e02b063e37e496e96eefd46c929f9bdc';
+  const WIDTH  = parseInt(process.env.REPLICATE_WIDTH  || '1024', 10);
+  const HEIGHT = parseInt(process.env.REPLICATE_HEIGHT || '1024', 10);
+
+  const positive = `${prompt}, seamless tile, repeating pattern, no edges, fabric pattern, wallpaper design, archival quality, high detail`;
+  const negative = 'low quality, blurry, edges, seam, border, frame, signature, watermark, text, hands, fingers, deformed';
+
+  const body = {
+    version: MODEL_VERSION,
+    input: {
+      prompt: positive,
+      negative_prompt: negative,
+      width: WIDTH,
+      height: HEIGHT,
+      num_inference_steps: 30,
+      guidance_scale: 7.5,
+      seed,
+      refine: 'expert_ensemble_refiner',
+      apply_watermark: false
+    }
+  };
+
+  // Submit
+  const submit = execSync(`curl -sf -m 30 -H 'Authorization: Bearer ${TOKEN}' -H 'Content-Type: application/json' -X POST 'https://api.replicate.com/v1/predictions' -d @-`, {
+    input: JSON.stringify(body),
+    encoding: 'utf8'
+  });
+  let pred;
+  try { pred = JSON.parse(submit); } catch { throw new Error('Replicate submit non-JSON: ' + submit.slice(0, 200)); }
+  if (!pred.id) throw new Error('Replicate: no prediction id — ' + JSON.stringify(pred).slice(0, 300));
+
+  // Poll
+  const start = Date.now();
+  let final;
+  while (Date.now() - start < 3 * 60_000) {
+    execSync('sleep 3');
+    const poll = execSync(`curl -sf -m 10 -H 'Authorization: Bearer ${TOKEN}' 'https://api.replicate.com/v1/predictions/${pred.id}'`, { encoding: 'utf8' });
+    const p = JSON.parse(poll);
+    if (p.status === 'succeeded') { final = p; break; }
+    if (p.status === 'failed' || p.status === 'canceled') {
+      throw new Error(`Replicate ${p.status}: ${JSON.stringify(p.error || p.logs || '').slice(0, 300)}`);
+    }
+  }
+  if (!final) throw new Error(`Replicate timed out after 3 min (id=${pred.id})`);
+
+  const imageUrl = Array.isArray(final.output) ? final.output[0] : final.output;
+  if (!imageUrl) throw new Error('Replicate returned no image URL');
+
+  // Download
+  execSync(`curl -sf -m 60 -L -o ${JSON.stringify(outPath)} ${JSON.stringify(imageUrl)}`);
+  if (!fs.existsSync(outPath) || fs.statSync(outPath).size < 1000) {
+    throw new Error(`Replicate image download failed (${imageUrl})`);
+  }
+  return outPath;
+}
+
+// ---- ComfyUI backend ------------------------------------------------------
+// Submits a SDXL txt2img workflow to ComfyUI at COMFY_URL (default 127.0.0.1:8188).
+// Polls /history/<prompt_id> for the result, then pulls the image bytes.
+function genComfy(prompt, seed, outPath) {
+  const COMFY = process.env.COMFY_URL || 'http://127.0.0.1:8188';
+  const MODEL = process.env.COMFY_MODEL || 'sd_xl_base_1.0.safetensors';
+  const WIDTH = parseInt(process.env.COMFY_WIDTH || '1024', 10);
+  const HEIGHT = parseInt(process.env.COMFY_HEIGHT || '1024', 10);
+
+  // Standard SDXL txt2img workflow expressed as ComfyUI's API JSON.
+  // For seamless tiling, append a strong "seamless tile, repeating pattern, no edges"
+  // suffix to the prompt — works adequately with SDXL. A proper SDXL tile-mode model
+  // would be cleaner; swap COMFY_MODEL later.
+  const positivePrompt = `${prompt}, seamless tile, repeating pattern, no edges, fabric pattern, wallpaper design, high detail, archival quality`;
+  const negativePrompt = 'low quality, blurry, edges, seam, border, frame, signature, watermark, text, hands, fingers';
+
+  const workflow = {
+    '3': { class_type: 'KSampler', inputs: { seed, steps: 28, cfg: 7.5, sampler_name: 'euler', scheduler: 'normal', denoise: 1.0, model: ['4', 0], positive: ['6', 0], negative: ['7', 0], latent_image: ['5', 0] } },
+    '4': { class_type: 'CheckpointLoaderSimple', inputs: { ckpt_name: MODEL } },
+    '5': { class_type: 'EmptyLatentImage', inputs: { width: WIDTH, height: HEIGHT, batch_size: 1 } },
+    '6': { class_type: 'CLIPTextEncode', inputs: { text: positivePrompt, clip: ['4', 1] } },
+    '7': { class_type: 'CLIPTextEncode', inputs: { text: negativePrompt, clip: ['4', 1] } },
+    '8': { class_type: 'VAEDecode', inputs: { samples: ['3', 0], vae: ['4', 2] } },
+    '9': { class_type: 'SaveImage', inputs: { filename_prefix: `wallco_${seed}`, images: ['8', 0] } }
+  };
+
+  // Submit
+  const submit = execSync(`curl -sf -m 30 -H 'Content-Type: application/json' -X POST '${COMFY}/prompt' -d @-`, {
+    input: JSON.stringify({ prompt: workflow, client_id: 'wallco-ai' }),
+    encoding: 'utf8'
+  });
+  let promptId;
+  try { promptId = JSON.parse(submit).prompt_id; } catch { throw new Error('ComfyUI submit returned non-JSON: ' + submit.slice(0, 200)); }
+  if (!promptId) throw new Error('ComfyUI did not return prompt_id');
+
+  // Poll history (max 5 min)
+  const start = Date.now();
+  let images;
+  while (Date.now() - start < 5 * 60_000) {
+    require('child_process').execSync('sleep 2');
+    let hist;
+    try {
+      hist = JSON.parse(execSync(`curl -sf -m 5 '${COMFY}/history/${promptId}'`, { encoding: 'utf8' }));
+    } catch { continue; }
+    const entry = hist[promptId];
+    if (entry && entry.outputs && entry.outputs['9'] && entry.outputs['9'].images) {
+      images = entry.outputs['9'].images; break;
+    }
+  }
+  if (!images) throw new Error(`ComfyUI generation timed out after 5 min (prompt_id=${promptId})`);
+
+  // Pull the first image
+  const im = images[0];
+  const viewUrl = `${COMFY}/view?filename=${encodeURIComponent(im.filename)}&subfolder=${encodeURIComponent(im.subfolder || '')}&type=${encodeURIComponent(im.type || 'output')}`;
+  execSync(`curl -sf -m 60 -o ${JSON.stringify(outPath)} ${JSON.stringify(viewUrl)}`);
+  if (!fs.existsSync(outPath) || fs.statSync(outPath).size < 1000) {
+    throw new Error(`ComfyUI image pull failed (${viewUrl})`);
+  }
+  return outPath;
 }
 
 function generate(prompt, seed, outPath) {
diff --git a/server.js b/server.js
index a61df85..61611b6 100644
--- a/server.js
+++ b/server.js
@@ -12,11 +12,36 @@ const express = require('express');
 const path = require('path');
 const fs = require('fs');
 const { execSync } = require('child_process');
+const inlineEditor = require('@dw/inline-editor');
 
 const app = express();
 const PORT = process.env.PORT || 9792;
 const DATA_ROOT = path.join(__dirname, 'data');
 
+// Inline editor (Wix-style admin editing). Auth required — admin role only.
+app.use(inlineEditor({
+  site: 'wallco.ai',
+  uploadDir: path.join(__dirname, 'public', 'uploads'),
+  authMode: process.env.AUTH_MODE || 'dev',
+  devSecret: process.env.AUTH_DEV_SECRET || 'wallco-dev-secret-change-for-prod'
+}));
+
+// Dev login (creates HS256 cookie). Strips out in prod.
+if ((process.env.AUTH_MODE || 'dev') === 'dev') {
+  const jwt = require('jsonwebtoken');
+  app.get('/_devlogin', (_req, res) => {
+    const token = jwt.sign({
+      email: 'steve@designerwallcoverings.com',
+      role: 'admin',
+      iss: 'https://auth.agentabrams.com'
+    }, process.env.AUTH_DEV_SECRET || 'wallco-dev-secret-change-for-prod',
+       { algorithm: 'HS256', expiresIn: '8h' });
+    res.cookie('dw_auth', token, { httpOnly: false });
+    res.type('text/plain').send('Admin logged in (dev). Refresh / to see edit affordances.\n');
+  });
+  app.get('/_devlogout', (_req, res) => { res.clearCookie('dw_auth'); res.send('Logged out\n'); });
+}
+
 function psql(sql) {
   return execSync(`psql dw_unified -At -q`, { input: sql, encoding: 'utf8' }).trim();
 }

← 2853438 wallco.ai v0.1: PG schema (pd_source_designs + spoon_all_des  ·  back to Wallco Ai  ·  ship v1 customer-facing storefront + Kamatera deploy 3f15587 →