Full-Stack Apps

Full-stack apps

A full-stack app is one app that carries both a static front end — served from the Ngris edge like a Static App — and a backend server that Ngris runs for you in-cluster. There is nothing to wire up between them: Ngris routes each incoming request by its path. Requests under your configured backend paths reverse-proxy to your backend container; every other request serves a file from your built front end.

Create the app with type fullstack — that is the difference from a managed (backend-only) app and a static (front-end-only) app. Both the front end and the backend build from a single source (an upload or a connected git repo), exactly like the other app types.

When to reach for full-stack. Use it when a single-page app (React, Vue, Svelte, Next.js…) needs its own API under one origin — the SPA is served statically at the edge for speed, and calls to /api hit your server. No CORS, no second domain, no separate deploy. If you only ship a website, use a Static App; if you only ship a server (no static bundle), use a backend app.

Full-stack apps require the application_hosting entitlement (the same one static hosting uses) plus managed-backend availability on your plan — the backend runs a real, long-running process on the edge, so it needs a plan that offers managed backends. There is no per-request serverless / FaaS runtime.

Path routing — backend paths

The whole design of a full-stack app is one rule: Ngris routes by request path. Set Backend paths (backend_paths) to the URL prefixes your server owns — for example /api, /auth, /graphql. The default is ["/api"].

  • A request whose path matches a backend prefix — the exact prefix or a /prefix/… segment under it — reverse-proxies to your backend container. So /api and /api/users both hit the backend when /api is a backend path, but /apidocs does not (it is not a segment boundary).
  • Every other request serves a static file from your built front end — identical to a Static App: publish_dir, index_document, SPA fallback and a custom 404 all apply.
  • Paths are normalized before the decision, so a crafted /api/%2e%2e/admin collapses to /admin and is treated as a front-end path — it can never sneak past a path rule into the backend.

You can list up to 20 backend prefixes. A special case: a prefix of / routes everything to the backend — that turns the app into a backend-only app (see backend apps) even though it is typed fullstack.

SPA pitfall — make non-API routes fall back to index.html. If your front end is a single-page app with client-side routing (/dashboard, /settings/billing…), those deep links are front-end paths — there is no file at /dashboard on disk. Turn on SPA fallback (spa_fallback) so any unmatched non-backend path serves your index.html and the client router takes over. Without it those routes 404.

A related trap: if your app ships no static bundle at all (a pure API), then / has no file to serve and returns a 404 — unless a backend path covers it. Either add a front end, or make it a backend-only app (backend paths = /) so the server answers / too.

How it builds & runs

A full-stack app builds through the same pipeline as a backend app, and additionally publishes the static assets to the edge. From your one source, Ngris:

  • Builds the backend from your Dockerfile (or an auto-detected Go / Node / Python stack) in a sandboxed job, then signs the image, scans it for vulnerabilities, and pins it to its digest. The running backend is isolated in its own per-app pod and reached only through Ngris's routed path.
  • Publishes the static front end to the edge store, exactly like a Static App — so front-end requests are served from the edge at static-hosting speed, never through the backend.

The build-context rules are the backend app's rules — rather than repeat them, see Backend apps for the details on the zip root, keeping the context self-contained, vendoring local modules, the 100 MB context cap, build-time vs runtime environment variables, the listen port, and the read-only root filesystem with a writable /tmp.

Backend runtime settings — port, health-check path, CPU/memory, replicas, and the backend_paths above — live on the app's Runtime panel (or PUT /v1/applications/{uuid}/runtime).

Preview URLs

Every deploy can get its own public preview URL of the form <deploy-uuid>.<your-plan-domain>, so you can test the exact build before you point a real domain at it — or share a work-in-progress. For a full-stack app the preview is fully live: backend paths hit the running backend and everything else serves that deploy's static front end, just like production.

Previews are opt-in and OFF by default. The unguessable UUID is the capability — a preview host is reachable by anyone who holds the URL, with no login or policy in front of it — so Ngris never exposes a build until you explicitly enable it.

Preview URL returns 404? Enable the preview.

If a preview URL returns 404, the almost-certain cause is that the preview is not enabled for that deploy (preview_enabled is off — the default). Turn it on with the deploy's Enable preview toggle on the Deploys page, or via the API:

curl -X POST "https://api.ngris.com/v1/applications/{uuid}/deploys/{deploy_uuid}/preview" \
  -H "X-API-KEY: <your_api_key>"

The response returns preview_enabled: true and the live preview_url; disable it again with the same route as DELETE. Two more things to check when a preview 404s:

  • The host must use the deploy UUID, not the app UUID — each deploy has its own preview host.
  • The deploy must be ready (or superseded — a former-live version). A still-building or failed deploy has no preview.

Add a database

Because a full-stack app has a backend, it can attach a fully-managed, MySQL-compatible database — Ngris provisions an isolated schema and a least-privilege user and injects the connection into your running backend as environment variables (DATABASE_URL and friends). There is nothing to install or operate. See App databases for the full walkthrough, the injected variables, and per-app isolation.

Deploy a full-stack app

The end-to-end path, in the dashboard:

  1. Create the app. Applications → New, choose type Full-stack.
  2. Configure backend paths. On the Runtime panel, set Backend paths to the prefixes your server owns (default /api). Set the listen Port and a health-check path while you are here. If your front end is a SPA, turn on SPA fallback in the serving settings.
  3. Set environment variables. Add config, tokens, and connection strings under Environment variables; mark secrets as Secret (write-only, encrypted). Build-time front-end vars (e.g. VITE_API_URL) and runtime backend vars both live here.
  4. (Optional) Add a database. From the app's Database tab → Add database; Ngris injects DATABASE_URL and redeploys.
  5. Upload or connect source. Upload a .zip or connect a GitHub/GitLab repo. Ngris builds the backend image and publishes the static front end.
  6. Watch the build. Open the deploy's Log — it tails live — and wait for the deploy to reach ready.
  7. Enable a preview and test. Toggle Enable preview on that deploy and open its <deploy-uuid>.<plan-domain> URL — you are testing the exact build, backend and front end together, before it goes live.
  8. Promote / point a domain. Activate the deploy to make it live, and add a custom domain whenever you are ready.

By API, the shape is the same: create the app (type: "fullstack"), set the runtime with your backend_paths via PUT /v1/applications/{uuid}/runtime, add env vars, push a deploy, poll it to ready, then POST /v1/applications/{uuid}/deploys/{deploy_uuid}/preview to test the exact deploy before activating it.

Stuck? Build failing, a 502/503 from the backend, or a route serving the wrong thing? Walk the deploy troubleshooting guide — it covers build-log errors, readiness/health-check failures, and path-routing surprises step by step.
Iris