• Docs
  • Login
Talk to an expertTry for free
Blog
Blog
BlogProductCase studiesNewsInsights
Blog

How to run a decoupled web stack without four cloud dashboards

multi-appcloud application platformpreview environmentsconfiguration
01 July 2026
Share
This post is also available in French.

Decoupling and orchestration: managing multi-app stacks on one platform

TL;DR

  • Decoupled architectures (a Next.js frontend, a Django or Drupal backend, a separate worker process) tend to end up spread across multiple cloud providers, each with its own dashboard, billing account, and environment variable set to keep in sync.
  • The networking between those services is usually manual, fragile, and undocumented: hardcoded URLs, shared secrets passed through environment variables, and no guarantee that what connects in staging connects the same way in production.
  • Upsun treats multiple applications as a single project: one config file, one network, one invoice.
  • Services communicate through deterministic internal routing, not through the public internet.
  • When your entire stack lives in one place, environment parity across preview, staging, and production extends to the networking layer, not just the application code.

At some point in the life of most growing web projects, the architecture forks. The frontend moves to a dedicated deployment. A background worker splits off to its own process. An API layer gets extracted. The right choice technically, usually, but the operational complexity that follows is rarely accounted for in advance.

Six months later, there are four cloud dashboards, three billing accounts, and a README section titled "Environment Variables" that's three pages long and visibly out of date.

How vendor sprawl actually happens

Key takeaway: Decoupled architectures don't start with vendor sprawl. They accumulate it incrementally, one "best tool for this specific job" decision at a time, until the operational surface area is too large to manage cleanly.

Nobody sets out to run their frontend on Vercel, their API on Render, their background jobs on Railway, and their database on Supabase. It happens one decision at a time, each individually reasonable.

Vercel is genuinely good at deploying Next.js. Render makes it easy to run a Django or FastAPI backend without configuring a server. Railway has a clean interface for managed Postgres. Each choice optimizes for its specific problem, and at the moment of that choice, the integration cost is deferred.

The integration cost always arrives. It shows up as environment variables that need to stay in sync across four providers. It shows up as debugging a CORS error that only happens in the preview environment because the frontend is on a different subdomain there than it is in production. It shows up as an incident postmortem that traces back to a hardcoded URL that pointed at the staging backend instead of production.

The underlying issue is that the components of the application were designed as a single system but deployed as separate concerns. The operational model doesn't match the architecture.

The networking problem nobody plans for

Key takeaway: When application components live on different platforms, the network between them is implicit, manual, and untested. Deterministic internal networking, where service addresses are known at build time and don't change between environments, removes an entire category of environment-specific bugs.

When a Next.js frontend calls a Django API, something has to tell it where that API lives. In practice, this is usually an environment variable: NEXT_PUBLIC_API_URL=https://api.myapp.com. That variable has a different value in local development, in the preview environment, in staging, and in production. Keeping those values correct and in sync is manual work that happens outside version control, and it fails in ways that are annoying to debug.

The failure mode isn't usually dramatic. It's subtle: a preview environment that silently calls the production API because someone forgot to update the variable. A staging environment that's testing against last week's backend because the deployment didn't propagate. A CORS policy that works in production but blocks requests in the branch environment because the origin domain is different.

Deterministic networking sidesteps the problem by making internal addresses consistent and known. When two applications live in the same Upsun project, they can reach each other by a fixed internal hostname that doesn't change between environments. The frontend always knows where the backend is without an environment variable you have to set or keep in sync. The address is determined by the project config, not by whoever last updated the secrets dashboard.

What a single-project multi-app config might look like

Key takeaway: Defining a decoupled stack in a single config file means the relationships between applications are explicit, version-controlled, and automatically reproduced in every environment, including preview environments for every branch.

Upsun is a platform-as-a-service that manages the infrastructure layer of your application stack, so your team doesn't have to. For multi-app projects, that means the entire stack, frontend, backend, and services, is defined in a single .upsun/config.yaml file. Here's a project with a Next.js frontend and a Python FastAPI backend sharing a Postgres database:

applications:
  frontend:
    type: nodejs:22
    source:
      root: "frontend"          # path to this app within the repo
    relationships:
      api:                      # gives the frontend a fixed internal address for the backend
        service: "backend"
        endpoint: "http"
    web:
      commands:
        start: "npm run start"

  backend:
    type: python:3.12
    source:
      root: "backend"
    relationships:
      database:                 # wires the backend to the Postgres service below
        service: "db"
        endpoint: "postgresql"
    web:
      commands:
        start: "uvicorn main:app --host 0.0.0.0 --port $PORT"

services:
  db:
    type: postgresql:16         # platform manages patching within this version

routes:
  "https://{default}/":
    type: upstream
    upstream: "frontend:http"   # only the frontend is public; the backend stays internal-only

 

The relationships block is where the networking is defined. The frontend has a relationship to the backend called api, which means it can reach the backend at a predictable internal address. The backend has a relationship to the database. Neither relationship requires a hardcoded URL or a hand-maintained environment variable outside the config file.

When a developer opens a branch, the platform spins up both applications and the database, wires them together with the same relationship structure, and assigns each a consistent internal address. The frontend in the preview environment calls the backend in the preview environment, not the production backend.

That's the thing the multi-provider setup can't easily give you: environment isolation that extends all the way through the network, not just to the application containers.

What changes operationally

Key takeaway: A single project for the full stack means a single deployment pipeline, a single place to check logs, a single invoice, and environment parity that includes networking. The operational overhead of running a decoupled architecture drops significantly.

The immediate practical differences are the obvious ones. One deployment pipeline covers the full stack. Logs from the frontend and backend are in the same place. The monthly invoice comes from one place.

The less obvious difference is what happens to onboarding and debugging.

With a multi-provider setup, a new developer joining the team needs accounts on four platforms, access to four dashboards, and a working understanding of how the pieces connect. The "Environment Variables" README section exists because there's no other way to document a network topology that lives in four separate providers' settings panels.

With a single-project setup, the topology is in the config file. A new developer clones the repository, and the config tells them exactly what the stack consists of, how the components relate to each other, and what version of everything is running. The README section on environment variables gets much shorter.

The same applies to debugging. When something goes wrong at the boundary between the frontend and the backend, the relevant logs are in the same place. The network between them is defined in the same file as everything else. There's no need to switch dashboards or correlate timestamps across four separate logging interfaces.

Already running across multiple providers?

Migration doesn't require moving everything at once. The config file describes the target state of the stack, so a practical approach is to start with a single component, typically the backend and its database, define it in Upsun, and establish the internal networking from there. The multi-provider README problem starts shrinking from the first service that moves across.


 

Frequently asked questions (FAQ)

Can the applications in a project use completely different languages and runtimes?

Yes. A project can contain a Node.js frontend, a Python backend, and a Java worker process, each running in its own isolated container. The platform manages the networking between them regardless of what they're built with.

How does the frontend know the internal address of the backend?

Upsun injects relationship information as environment variables at runtime. The frontend gets a set of variables describing how to reach the backend: host, port, and scheme. These are consistent across environments, so the same application code works in every branch without modification.

What happens to the networking in preview environments?

Each branch environment gets its own isolated instance of every application in the project, wired together with the same relationship structure as production. The frontend in a branch environment calls the backend in that same branch environment. The relationship wiring in a branch environment points at that environment's own services, not production's.

Does this mean all services have to be defined in one repository?

Not necessarily. Upsun supports source operations and external integrations that allow components to live in separate repositories while still being orchestrated as a single project. For most teams starting out, a single repository is simpler, but the platform doesn't require it.

How is this different from Docker Compose?

Docker Compose solves local development orchestration. It doesn't manage deployments, environment parity, scaling, or networking across multiple live environments. A single Upsun project config file covers all of those, from the developer's branch environment through to production, with the same configuration.

Stay updated

Subscribe to our monthly newsletter for the latest updates and news.

Your greatest work
is just on the horizon

Free trial