A deep technical tour of the architecture that powers demo.azurefed.com â from the AI build pipeline to the concurrency engineering that keeps it bulletproof.
← Back to sign inA self-service portal where a Cloud Solution Architect types a customer name, and minutes later a tailored, interactive Copilot demo appears â researched, branded, and ready to show.
Three moving parts do all the work. The Portal is the web app you log into. The Worker is an autonomous agent that builds demos in the background. The Shared Storage is where finished demos live and get served to end users.
Everything runs serverless on Azure Container Apps â no VMs to patch, scales to zero, deploys as immutable container images.
| Layer | Technology | Why |
|---|---|---|
| Runtime | Node.js 20 + Express | Single-file server, fast, simple |
| Database | SQLite (better-sqlite3) | Zero-ops, embedded, synchronous |
| Hosting | Azure Container Apps | Serverless containers, revisions, instant rollback |
| Registry | Azure Container Registry | Immutable image builds via az acr build |
| Storage | Azure Files (SMB mount) | Persistent demo files survive deploys |
| Rendering | Chromium + Puppeteer | Server-side demo screenshots |
| AI | Azure OpenAI + AI Search + Bing | Research, grounding, content generation |
archive-feature-20260618115517). Azure spins up a new revision and shifts traffic to it. The old revision stays parked at 0% traffic â so rollback is instant: just shift traffic back. No "undo deploy" stress.
When a CSA hits "Build Demo," the work doesn't happen in the web request â it can't, it takes minutes. Instead the portal records the job and hands it off. This is asynchronous job processing.
requestId, derives a URL-safe slug, and writes a row to demo_requests with status pending.GET /api/pending and sees the new job.POST /api/claim/:id. This atomically flips the row to processing (more on why that matters in §6).meta.json) is written to /data/demos/<slug>/ on shared storage.POST /api/callback/:id with status completed. The demo now appears in the gallery.The worker is the secret sauce â a separate Azure Container App running an AI agent that wakes up every ~30 seconds, asks "any work for me?", and builds demos completely unattended.
It's a classic polling consumer. Rather than the portal pushing work (which breaks if the worker is asleep), the worker pulls work on its own schedule. This decouples the two: the portal can restart, the worker can restart, and nothing is lost â pending jobs just wait in the database until someone claims them.
// Worker's heartbeat loop (conceptual) while (running) { const jobs = await fetch('/api/pending'); // what's waiting? for (const job of jobs) { const claimed = await claim(job.id); // call dibs (atomic) await buildDemo(job); // the heavy lifting await callback(job.id, 'completed'); // report back } await sleep(30_000); // nap, repeat }
Here's a war story. SQLite is brilliant â but it relies on byte-range file locks to coordinate writes. Azure Files (the SMB network share) doesn't honor those locks reliably. Result: SQLITE_BUSY crashes on the very first write.
We can't put the live database on the share. But we need data to survive deploys (containers are ephemeral â restart and local disk is wiped). The fix is a two-location dance:
/tmp (local disk SQLite can actually lock).SQLITE_BUSY.VACUUM INTO) to grab a consistent snapshot, then atomically rename it onto the share (a plain file copy SMB does allow).The nastiest bug we fought. For a window of time, two workers were alive â the real Azure worker, and a leftover "zombie" from an old environment. Both polled the same queue. Both tried to build the same demo. Chaos: duplicate demos, fake completions, corrupted state.
The fix is elegant and lives in the database, not the application logic. The claim is a single atomic SQL statement:
UPDATE demo_requests SET status = 'processing' WHERE request_id = ? AND status = 'pending';
SQLite guarantees this runs atomically. Whichever worker's statement lands first changes one row (result.changes === 1) and earns the job. The second worker's identical statement matches zero rows â the status is no longer pending â so it gets changes === 0, the portal returns 409 Conflict, and that worker politely moves on.
Not every demo should be deleted. Some are old, some are one-off, but you might still want to hand a prospect a direct link. So demos have three states.
| State | In gallery? | Direct link? | Reversible? |
|---|---|---|---|
| Active | â Yes | â Yes | â |
| Archived | â Hidden | â Yes | â Restore |
| Deleted | â Gone | â Gone | â Permanent |
How archive works under the hood: it's just a flag. Archiving writes "archived": true into the demo's meta.json. The gallery endpoint filters those out (demos.filter(d => !d.archived)), but the static file server never checks the flag â so the folder's index.html keeps serving at its direct URL. Restore simply deletes the flag. Nothing moves, nothing is copied; one boolean controls visibility.
Two trust zones: humans log in with sessions; the worker authenticates with a shared secret. Every action is logged.
bcrypt-hashed password, server-side session cookie. Admin vs. viewer roles gate destructive actions (delete, archive, user management)./api/pending, /api/claim, /api/callback) require a webhook secret, not a session. No browser, no cookie needed.access_logs table with user, IP, and timestamp... or /, so a crafted request can never escape /data/demos./data/demos. The slug check, the role check, and the session check all have to pass first. Three locks on one door.