Datalumina

Going further

Enterprise ready

What separates this working demo from an enterprise deployment, and the four upgrades you would make first.

The app you deployed is real. It classifies, extracts, validates, and reviews documents on Azure, behind a password, on a public URL. It is also, deliberately, a demo. One container, one SQLite file, one shared password, one environment. Dave says so on camera, this "has by no means been a full guide on how to create secure applications."

This page maps the distance between what you built and what you would run for a client with real invoices flowing through it. Four upgrades, in the order they usually happen. None of them are specced out here; the goal is that you understand what each one means, what it buys, and roughly what the work is.

daveebbelaar/invoice-review2:36:18

Split the backend and the frontend

The single container was a demo convenience. In a production setup the two halves deploy separately, the FastAPI backend as its own Container App, and the React build on a static host such as Azure Static Web Apps, or a second lightweight container behind a CDN.

The split buys you three things:

  • Independent deploys. A copy change in the UI no longer rebuilds and restarts the API, and vice versa. Each half ships on its own schedule.
  • Independent scaling. Static files are cheap to serve from a CDN edge; API replicas scale on actual load. One container means paying for the heavier of the two everywhere.
  • A smaller blast radius. A frontend bug cannot take the API down with it, and the API image no longer carries a copy of the SPA.

What changes in this codebase is mercifully small, because the seams already exist. VITE_API_BASE_URL points at the backend's domain at build time instead of /, and CORS becomes real again. ALLOWED_ORIGIN in the backend config goes back to doing the job it already does in local dev, where the two halves also run as separate origins. The SPA-serving block and FRONTEND_DIST_DIR simply stop being used.

PostgreSQL instead of SQLite

SQLite carried this project honestly, and Dave notes in the video that for an internal tool it can be enough. But you met its ceiling during the deploy, the hard single-replica limit, enforced to avoid database is locked errors on Azure Files. A file is not a database server.

The managed answer on Azure is Azure Database for PostgreSQL Flexible Server, and it buys you three things:

  • Concurrency. Multiple app replicas connect safely, so the min=max=1 constraint disappears and the app can actually scale.
  • Operations. Automated backups, point-in-time restore, high availability, and metrics come with the service instead of being your problem.
  • No file mount. The Azure Files share and the YAML volume dance from the deployment stage are simply deleted.

The migration is tractable because SQLAlchemy sits between the app and the database. build_database in backend/app/database.py already branches on the SQLite URL prefix; a PostgreSQL connection string in a Container App secret replaces it, and the models stay untouched. The one real addition is migrations. metadata.create_all is fine for a demo, but a shared production database wants versioned schema changes with a tool like Alembic, the same change-review-apply loop the document-copilot build uses. Expect roughly 20 to 40 dollars a month, which is exactly the cost the demo avoided and a production system happily pays.

A real authentication layer

The shared password did its one job, keeping strangers from burning your Azure credits. It cannot do the jobs a finance tool actually needs. Everyone who knows the password is the same anonymous person, there is no way to revoke one user without locking out everyone, and "who approved this invoice" has no answer. In finance software that last one is not a nice-to-have, an approval trail is a compliance requirement.

Real authentication means per-user identity. In an Azure shop the natural choice is Microsoft Entra ID, the directory the client's employees already log into, so access follows onboarding and offboarding automatically. Auth providers like Auth0 or Supabase Auth fit when the users are not all in one directory. Either way you get:

  • Accounts and revocation. Access is granted and removed per person, and leaving the company means losing access without a password rotation ceremony.
  • Roles. Maya uploads and reviews; her manager approves above a threshold; an auditor reads history. The field_sources and decision data the app already records becomes meaningful when each action has a person attached.
  • An audit trail. Every approval, rejection, and edit tied to an identity, which is what makes the review history defensible.

In the code, AccessPasswordMiddleware gives way to token validation on every request, the session cookie becomes a real user session, and endpoints gain permission checks. The middleware seam where the password gate sits today is exactly where this slots in.

Staging and production

Everything in this build deployed straight to the one environment that exists, which is fine until the first time a change breaks extraction while Maya is mid-review. The standard answer is two parallel environments, a staging deployment where changes prove themselves, and a production deployment users touch, promoted to only after staging passes.

On Azure that means duplicating the hosting resources under a naming scheme, separate resource groups or a -staging and -prod suffix per resource, with configuration differing only in secrets and environment variables. The app code does not change at all; the same image runs in both places with different settings, which is precisely why the twelve-factor separation of config from code mattered all along.

Two habits make the pair work:

  • Immutable image tags. The deploy used invoice-review:latest, which makes "what is running right now" unanswerable. Tag images with the git commit, promote a specific tag from staging to production, and rollback becomes retagging.
  • CI/CD instead of hands. The runbook you executed is automatable end to end. A pipeline runs az acr build on merge, deploys the new tag to staging, and promotes to production on approval. A human running CLI commands twice, identically, under pressure, is the failure mode this removes.

Where this leaves you

None of these four upgrades touches the heart of what you built. The pipeline, the hybrid extraction, the deterministic policy, the review loop, all of it carries over unchanged; what changes is the scaffolding around it. That is worth noticing, because it means the demo-to-production distance is real but finite, and the architecture decisions from the early stages, typed boundaries, code for policy, layers that do not leak, are what kept it finite.

The better way to internalize this list is to build it. Take one upgrade, swap SQLite for PostgreSQL, or put Entra ID in front, and work it into your copy of the project. As Dave says about Azure itself, follow the end-to-end path first, then start to swap out the components.

Keep going

On this page