Skip to main content

Multi app (beta)

A Node.js or Bun application in the same repository as your backend, deployed with it and reached by its own hostname

Multi app is in beta. It works as described here, and it is still being proven — so, like every beta feature, it is not covered by the uptime commitment.

A frontend is a Node.js or Bun application that runs beside your backend, in the same environment and from the same repository. Next.js in front of Drupal is the usual case; any backend the platform runs can have one, except a static site, which has nothing behind it to talk to.

The two deploy together, as one release. A branch's preview has both, a rollback takes both back, and the frontend is never running against a backend from another commit.

If your frontend and backend live in two repositories, this is not what you want: make them two projects.

On one machine, a deploy restarts your frontend, so it answers with errors for a few seconds while your backend goes on answering. On two or more, they are restarted one at a time.

In your repository

Your backend stays where it is, at the root of the repository. The frontend is a directory of its own, described by a frontend block in vallic.yaml. Everything else in the file goes on meaning the backend.

version: 1
type: drupal

runtime:
  php: '8.5'

build:
  steps:
    - name: Dependencies
      run: composer install --no-dev

frontend:
  root: frontend
  runtime:
    node: '24'
  build:
    steps:
      - name: Dependencies
        run: npm ci
      - name: Build
        run: npm run build
      - name: Production dependencies only
        run: npm prune --omit=dev
    cache:
      - node_modules
      - .next/cache
  start: npm start
  port: 3000
  health:
    path: /healthz
  env:
    required:
      - PREVIEW_SECRET
Key What it says
root Its directory in the repository. Required, and it needs a package.json. It cannot be the directory your backend serves.
runtime node or bun, with a version. One of them.
start What runs it, from its directory. Required.
port What it listens on. It is told this as PORT.
build.steps How it is built, in its directory and its own image. Leave them out only if its dependencies are committed.
build.cache Paths under its directory to carry between builds.
health.path A path that answers when it is ready, without asking your backend, which on a first deploy has nothing installed yet. A deploy asks it, and a frontend that does not answer fails the deploy. Without one, a deploy does not wait for it.
env.required The variables it needs, by name. See below.

Nothing is bought for a frontend. It runs on your web machines, and a deploy with no room for it says so and what to resize.

Delete the block and the next deploy takes the frontend away. Your own domains go on answering, from the backend, and go back to the frontend if the block returns.

Build without the backend

A build runs before the release is live, and on the first deploy there is no backend at all. So a build step cannot fetch from it: static generation that asks your backend for pages will fail or read old data. Fetch at runtime.

Reaching it

Your frontend has its own address from its first deploy, frontend--production.gwfdxo99.vallic.cloud for an environment called production. The environment's own address goes on reaching the backend.

Your own domains reach the backend until you send them to the frontend, on the environment's Domains page or with:

vallic domain application example.com frontend

So example.com can be the frontend and cms.example.com the backend. Each has its own primary domain and its own redirects: making example.com canonical redirects the frontend's other names to it and leaves the backend's alone.

A hostname reaches one application. Sending a path such as /api to the backend is your frontend's to do, as a rewrite to BACKEND_URL.

What it is given

Less than your backend, on purpose: it answers the public directly, so it holds only what it needs.

  • Its code, read-only. Its static files are build output in the release. It has no access to uploads or private files, which your backend serves. Nothing can be written under its directory, and that includes what Next.js's image optimiser and incremental regeneration write under .next/.
  • None of the environment's secrets. It does not get the database password. It gets the variables it names under env.required, and a deploy refuses a name nobody has set.
  • Where the backend is:
Variable What it is
BACKEND_URL The backend inside the stack, without the public internet. It skips any cache in front of the backend.
BACKEND_PUBLIC_URL The backend's public address, for what reaches a browser.
BACKEND_HOST The backend's public hostname, without the scheme.
FRONTEND_URL Its own public address. Your backend is told this too, for previews, CORS and links back.

It is given these whatever you set: naming one under env.required changes nothing. On your backend, a FRONTEND_URL of your own replaces the platform's.

Your backend trusts the name in BACKEND_URL through VALLIC_HOSTNAMES, so a plain request to it needs no extra headers. Links your backend builds in that answer use the same internal name, and a browser cannot follow those: build what reaches a browser from BACKEND_PUBLIC_URL, or send X-Forwarded-Host: BACKEND_HOST and X-Forwarded-Proto: https, which the Drupal settings file honours.

Rendering every request from BACKEND_URL reaches your backend uncached. A frontend with no cache of its own can ask more of the backend than the same site did before.

Whether it is answering

Production's availability check asks both your applications, each at its primary domain. If either stops answering, the environment counts as down, and the notification says which address failed.

Behind the CDN

Both hostnames go through one CDN, and each gets its own copy of every page. That is the Hostname option under Vary the cache by, always on while you have a frontend.

Next.js sends no Cache-Control from a route handler, even with dynamic = 'force-dynamic', and a response without one is kept for 30 days. So set it on every route that changes: Response.json(data, { headers: { 'Cache-Control': 'no-store' } }). A page rendered per request already sends its own.

The rest of the CDN tab applies to both. A path you never cache is never cached on either hostname, and a session cookie your frontend names goes under Session cookies.

Changing something in your backend does not clear a frontend page from the CDN. A page the frontend lets the edge keep stays until its time runs out, or until you clear the CDN, so send a short cache time from anything that shows your backend's content.