Microsoft Federal · Copilot CSA

How the Demo Portal Works

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 in

1 The 10,000-ft view

A 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.

👤 CSA / User Portal Express + SQLite Azure Container App Worker Autonomous AI agent polls every ~30s Shared Storage Azure Files (SMB) /data/demos requests demo poll / claim serves demo writes demo
Figure 1 — Three components: Portal (web app), Worker (background builder), Shared Storage (demo files).

2 Stack & hosting

Everything runs serverless on Azure Container Apps — no VMs to patch, scales to zero, deploys as immutable container images.

LayerTechnologyWhy
RuntimeNode.js 20 + ExpressSingle-file server, fast, simple
DatabaseSQLite (better-sqlite3)Zero-ops, embedded, synchronous
HostingAzure Container AppsServerless containers, revisions, instant rollback
RegistryAzure Container RegistryImmutable image builds via az acr build
StorageAzure Files (SMB mount)Persistent demo files survive deploys
RenderingChromium + PuppeteerServer-side demo screenshots
AIAzure OpenAI + AI Search + BingResearch, grounding, content generation
Key idea: immutable revisions Every deploy builds a brand-new container image with a unique tag (e.g. 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.

3 The demo build pipeline

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.

CSA Portal Worker Storage 1. Build "FBI Demo" INSERT pending 2. GET /api/pending 3. POST /claim (atomic) 4. Research +generate demo 5. Write demo files 6. POST /callback (done) status=completed
Figure 2 — The async build sequence. The portal never blocks; the worker pulls work and reports back.

Step by step

4 The autonomous worker

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
}
Why a separate container? Demo builds are slow and CPU-heavy (AI calls, Chromium rendering). Isolating the worker means a long build never freezes the portal's login or gallery. They scale independently, fail independently, and deploy independently.

5 The SMB database problem

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:

Container (ephemeral) SQLite live writes /tmp/users.db local disk — lockable Azure Files /data/users.db survives deploys VACUUM INTO (snapshot out) seed on boot (copy in)
Figure 3 — Live DB on lockable local disk; consistent snapshots persisted to the share.
The lesson Network file shares and database lock files don't mix. Keep the database on local block storage; treat the share as a durable backup target, not a live filesystem for the DB.

6 The claim-race war

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.

❌ Without a guard Worker Areads pending Worker Breads pending BOTH build itduplicate + corrupt ✅ Atomic claim Worker AUPDATE...WHERE pending Worker BUPDATE...WHERE pending changes=1A wins, builds changes=0 → 409B backs off
Figure 4 — The database row itself is the lock. First writer wins; the loser gets HTTP 409 and walks away.

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.

War story: the zombie We hunted the duplicate completions in the logs, found a second worker claiming jobs from a stale IP, and shut it down. The atomic-claim guard means that even if a zombie ever comes back, it physically cannot double-build a job. The race is dead at the data layer, not patched over in code.

7 Archive & lifecycle

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.

Active in gallery + direct link Archived hidden, link still works Deleted gone forever đŸ“Ļ archive â†Šī¸ restore đŸ—‘ī¸ delete
Figure 5 — Active ⇄ Archived is reversible. Delete is the one-way door.
StateIn 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.

8 Security model

Two trust zones: humans log in with sessions; the worker authenticates with a shared secret. Every action is logged.

Defense in depth Even the admin delete button can only ever remove a folder inside /data/demos. The slug check, the role check, and the session check all have to pass first. Three locks on one door.