The Atlas MikeOSS.Azure's docs, bound to the code — and to the migration that built it
108 documents
docs/issues/azure-migration/022-frontend-deployment.md

Proposes getting Mike's web UI onto a public Azure URL alongside the already-deployed API-only backend. A focused audit of frontend/ found the app barely uses Next.js server features — 85 components are already "use client", with no middleware, API routes, server actions, or next/image — so the decision is to ship as a static export (output: 'export'), requiring only two small conversions in frontend/next.config.ts (add the export flag, delete dead /api/sitemap rewrites) and frontend/src/app/page.tsx (server redirect → client useEffect + router.replace). Hosting target is left as an open question across five options (bundle into the backend Container App, a separate frontend Container App, Azure Static Web Apps, Storage static website, or Cloudflare/OpenNext), with deploy.ps1, the GitHub workflow, and infra checks to follow in later phases. When deciding how the frontend is built and hosted on Azure, or why it ships as a static export.

Issue 022 — Frontend deployment to Azure

Goal

Get Mike's web UI in front of users on a public Azure URL, alongside the already-deployed backend API. Today the deployment is API-only — there is no Static Web App, App Service, or additional Container App hosting the frontend. Users cannot reach the app via a browser.

Audit: what does Mike's frontend actually use from Next.js?

A focused read of frontend/ revealed that the app barely uses any Next.js server features. It is effectively a SPA with the App Router as a routing convenience.

Used (works with static export)

Feature Notes
next/font/google (Inter, EB_Garamond) Build-time font fetching — fine for output: 'export'.
Metadata API in root layout Static metadata, no dynamic data fetch.
App Router Used purely for routing. All page contents are "use client".
Client components 85 files start with "use client".

Used (requires conversion)

Feature Resolution
redirect("/assistant") in app/page.tsx (server-side) Replace with "use client" + useEffect + router.replace.
rewrites() in next.config.ts pointing at /api/sitemap/... Delete. The target paths don't exist — the rewrite was dead config left over from an earlier shape.

Not used at all

  • Middleware (middleware.ts) — does not exist.
  • API Routes (route.ts) — none anywhere under src/app/api/.
  • Server Actions ("use server") — zero matches across src/.
  • next/image — zero imports.
  • Server Components doing data fetches.
  • ISR / SSG with dynamic data.
  • Streaming, Edge runtime.

Already configured but unused

  • open-next.config.ts — wired for Cloudflare Workers via OpenNext. Suggests prior Cloudflare deployment intent. Kept as a fallback option but not part of the chosen path.

Decision: ship as a static export

Given the audit, output: 'export' is the honest representation of how this app actually works. The build produces pure HTML/JS/CSS in frontend/out/ that any static host can serve.

This unlocks every cheap and standard hosting option, and removes the need for a Node runtime in the frontend deploy at all.

Conversion required (small)

  1. frontend/next.config.ts

    • Add output: 'export'.
    • Delete the rewrites() block (points at non-existent /api/sitemap/... paths).
    • Keep reactCompiler: true and skipTrailingSlashRedirect: true.
  2. frontend/src/app/page.tsx

    • Convert from server-side redirect("/assistant") to a "use client" component using useEffect + router.replace.

These are the only changes needed. The 85 already-"use client" components and the App Router structure require no edits.

Hosting options

The static build can be served from any of:

Option Pros Cons
Bundle into the backend Container App Same FQDN as the API. One deploy unit. No new Azure resources. Cheapest. Mirrors the MatterAI shape (single zip, single host). Frontend deploys are coupled to backend deploys. Requires Express static middleware + SPA fallback.
Separate frontend Container App Consistent with the existing backend / postgrest pattern in Bicep. Independent deploy lifecycle. Runs e.g. nginx on the static out/. Adds a Container App resource. CORS to wire up between the two FQDNs.
Azure Static Web Apps (free SKU) Global CDN, free SSL, auto-CI from GitHub. Adds a new platform service to operate (separate from Container Apps). Requires its own GitHub Actions integration.
Storage Static Website Pennies. No CDN by default, no custom domain SSL without extra wiring.
Cloudflare Workers via OpenNext Already configured (open-next.config.ts). Global edge. Not Azure — fails the all-Azure story.

The choice depends on whether we want a single Container App deployment now (simpler, mirrors MatterAI) or to keep frontend / backend deploy lifecycles separate (more standard for SaaS).

Open question. The conversion to static export is independent of this decision and lands first.

Implementation steps

Phase 1 — frontend conversion (this issue)

  • [x] Audit Next.js feature usage (above)
  • [ ] next.config.ts — add output: 'export', remove dead rewrites
  • [ ] app/page.tsx — client-side redirect
  • [ ] Verify npm run build produces a complete frontend/out/
  • [ ] Commit

Phase 2 — pick a hosting target

  • [ ] Decide between bundle-into-backend, separate Container App, or Static Web Apps based on operational preference
  • [ ] Document the chosen hosting in this issue
  • [ ] Implement (Dockerfile / Bicep / workflow changes)

Phase 3 — deployment integration

  • [ ] Update deploy.ps1 and .github/workflows/deploy.yml to handle the frontend build / deploy step alongside the backend
  • [ ] Update check-azure.ps1 so the resource-inventory check no longer flags the missing frontend
  • [ ] Update docs/azure-production-hardening.md — strike the "no frontend" line

Followups (out of scope here)

  • Custom domain + SSL once we have a stable URL.