Field Notes · 6 MIN

Can one backend serve many AI products?

How we run one stable backend behind many frontends, the single config knob that makes it work, and the free-tier trap that took a whole stack down.

By NactorePublished 14 Jul 2026All articles

Yes, one backend can serve many products, if you treat the backend as stable infrastructure and ship new products as new frontends. In our setup, adding a product means setting one environment variable and allow-listing one origin. It does not mean changing backend code. This post covers the pattern, the one knob that makes it work, and the two ways it can still go wrong.

Key takeaways
  • The pattern is one stable backend for auth, payments, and domain logic, with many thin frontends deployed independently.
  • Each new frontend needs one API base URL setting and an entry in the backend's allowed origins. No backend change is required.
  • Build-time variables in a bundler are baked into the output, so they must be set where the build runs.
  • A free database tier with an automatic usage quota is a production risk, because hitting the quota can take the whole site down.
  • Nactore is an AI-native software engineering partner, and we use this pattern when a client wants several AI products on one foundation.

What does the shared-backend pattern look like?

The backend owns the parts every product needs. That is accounts, payments, and the domain logic. It is a conventional Django and Django REST Framework service on a managed host, with a managed Postgres behind it. The frontends are single-page React apps built with Vite and deployed as static assets to a CDN.

The decision we wrote down early was to stop churning the backend. A backend that changes with every product is a backend whose every deploy risks every product. So new products ship as new frontends, and the backend only changes when a shared capability genuinely changes.

LayerWhat it holdsHow often it changes
BackendAuth, payments, domain logicRarely, and for shared reasons
Frontend per productUI, copy, product-specific flowsOften, independently
ConfigOne API base URL per frontendOnce per frontend

What is the one knob?

Each frontend reads a single build-time variable that points at the backend. In a Vite project, variables prefixed with VITE_ are statically replaced at build time and exposed in client code after bundling. That is exactly what you want for a base URL, and exactly what you do not want for a secret.

Adding a new product is a short, boring sequence.

  1. Create the frontend and set the API base URL variable to the backend's URL.
  2. Allow-list the origin by appending the frontend's origin to the backend's CORS and CSRF trusted-origin settings.
  3. Redeploy the backend so the new settings load. No code changes.
  4. Deploy the frontend to the static host and attach its domain.

The boring sequence is the point. When adding a product is a config change, you can try product ideas cheaply, and you can retire them without touching shared code.

Where do build-time variables live in CI?

This caught us once. Once the frontend deploy moved to a CI workflow, the build ran on the CI runner, not on the host's own build system. A bundler inlines its variables during the build, so values set only in the host's dashboard never reached the bundle. The variables had to live in the workflow's environment block.

The practical rule is to set a build-time variable wherever the build actually executes. If your build runs in CI, the host's dashboard is the wrong place. The same logic explains why a secret must never use a VITE_ prefix, since anything inlined into the bundle is public.

Single-page apps also need a fallback so deep links resolve. Cloudflare Pages treats a project without a top-level 404 page as a single-page app and routes unmatched paths to the root, so client-side routing works.

What can still go wrong?

Two things bit us, and both were infrastructure choices rather than application bugs.

Why did a free database tier take the whole stack down?

The database sat on a serverless free tier with an automatic compute-time quota. When the quota ran out, the provider refused to start the database until the account was upgraded. The health check failed, the host returned errors, and every product on the shared backend went down together.

The shared backend multiplies this risk. One quota-capped dependency is now a single point of failure for every frontend. The old data could not even be dumped, so recovery meant a fresh start. Our rule afterward is that production never runs on an auto-quota-capped free tier. Pay for the dependency that the whole stack stands on, or accept the outage as a scheduled event.

Why can a direct database host be unreachable?

When we moved the database to a new provider, the direct connection host was IPv6-only and unreachable from the backend host, which only had IPv4 egress. The fix was to use the provider's IPv4 session pooler for the connection string, both from the host and from the local machine when running migrations and seeding. If a connection works from your laptop and fails from your host, check the address family before you check the credentials.

Does the shared backend work for AI features?

It works well, with one caveat. AI features add latency and failure modes that other endpoints do not have, so they deserve their own timeouts, retries, and logging. The shared backend gives you one place to enforce those policies, so every product inherits them. Read LLM observability: what to log for the fields we record on every model call.

The caveat is blast radius. A slow model endpoint on a shared service can starve requests for every product. Put model calls behind a bounded worker pool or queue, and keep the health check independent of the model provider.

Pro tip

Treat the shared backend's free-tier limits as a launch checklist item, not an afterthought. Write down every quota, idle-pause rule, and cold-start behavior on the stack, then decide which ones you are willing to be paged for.

When is one backend the wrong call?

Do not share a backend when products need different user pools or different compliance boundaries. Shared auth is convenient until two products need to keep their users strictly separate. In that case, give each product its own users and leads, and share only the infrastructure patterns, not the accounts.

Also hold back if the products have very different traffic shapes. A spiky product can degrade a steady one on the same service.

Frequently asked questions

Do I need a monorepo for this pattern?

No. In our setup the backend and each frontend live in separate repositories and deploy independently. The only coupling is the API base URL and the allowed-origins list.

How do I add a new frontend without a backend deploy?

You do need a backend redeploy to load the new origin settings, but no backend code changes. That is a config change, not a release.

Why not put secrets in the frontend build?

Bundler variables with the public prefix are inlined into client code. Anything placed there is visible to every visitor, so only public values belong there.

Is a free database tier ever acceptable?

For prototypes, yes. For anything that takes money or serves several products, a quota that can shut the database off is a risk you should price in.

Want this built for your team? Book a free 30-minute call.

Want to apply this to your business?

Book a free 30-minute call. We will tell you what we would do first.