Static Apps

Static Apps & deploys

A Static App hosts a website or single-page app on the Ngris edge — no server, no agent, no CLI. Upload a .zip of your built site or connect a GitHub/GitLab repo, and Ngris serves it over global HTTPS with automatic TLS. Every deploy is immutable and versioned, so rollback is an instant pointer flip.

Two pieces work together: an Application is the content (your files, from an upload or a git repo); an agentless Endpoint points at it and gives it a public URL. Unlike a tunnel, there is no agent to run — the content lives on the edge.

Ngris builds your site. Push (or upload) your source and Ngris auto-detects your framework — Vite, Next.js, Astro, SvelteKit, Create React App, Nuxt, Gatsby, Angular, Hugo, Jekyll — runs install + build in an isolated per-build sandbox, and serves the output. Already-built static files (an index.html at the root, no framework) are served as-is with no build step.

Static Apps require the application_hosting entitlement on your plan. Bandwidth counts against your account's single monthly data-transfer allowance (the same pool as tunnels); over the cap, requests are served a 429 until the period resets.

Beyond static: full-stack apps. An app can also carry a backend that Ngris runs in-cluster (a fullstack app = static front end + a path-routed backend). Push a Go, Node, or Python service (auto-detected) or any Dockerfile (any language or framework, a Next.js server included) and Ngris builds it in a sandboxed job and runs it on the edge behind the same policy engine. It runs a long-running backend; for a per-request, scale-to-zero handler instead, see Serverless Functions. A full-stack app can also get a managed database.

Quick deploy (one call)

The fastest path from code to a live URL creates the app, its first deploy, and the public endpoint in a single request — and returns the live URL.

Dashboard

Open Applications → New, pick Upload or Connect Git, and follow the wizard. When the build is ready the app is live at its generated URL; add a custom domain any time.

API — upload a built site

curl -X POST "https://api.ngris.com/v1/applications/quick-deploy" \
  -H "X-API-KEY: <your_api_key>" \
  -F "name=my-site" \
  -F "file=@dist.zip" \
  -F "wait=true"

API — connect a git repo

curl -X POST "https://api.ngris.com/v1/applications/quick-deploy" \
  -H "X-API-KEY: <your_api_key>" \
  -H "Content-Type: application/json" \
  -d '{"name":"my-site","repo_url":"https://github.com/acme/site","branch":"main"}'

The response includes the live url. Pass wait=true to block up to 90 seconds until the first build is live; otherwise the URL is created immediately and starts serving within a few seconds as the build finishes. See the Quick Deploy API reference for the full response.

Deploy from an upload

Build your site locally, zip the output directory, and upload it. Each upload is a new immutable deploy; the first successful build goes live automatically.

  • Run your build (npm run build, vite build, Hugo…) and zip the output folder (dist/, build/, public/).
  • Make sure index.html sits at the root of the zip (or set Publish directory to the subfolder that holds it).
  • Max upload size is plan-limited (default 100 MB).
curl -X POST "https://api.ngris.com/v1/applications/{uuid}/deploys" \
  -H "X-API-KEY: <your_api_key>" \
  -F "file=@dist.zip" \
  -F "version=v2.1.0"   # optional label

The build runs asynchronously; poll GET /v1/applications/{uuid}/deploys until the newest deploy reaches ready. Each deploy exposes a display_version: your optional version label if set, else the git commit short SHA (git deploys) or v{n} (uploads).

Git push-to-deploy

Connect a GitHub or GitLab repo and every push to the configured branch redeploys automatically. On connect, Ngris runs the first deploy and generates a webhook secret.

1. Connect the repo

Create the app with source: "git" (or use quick-deploy with repo_url). For a private repo, pass a git_token (a GitHub PAT or GitLab token with read access).

2. Register the webhook

Fetch the webhook details with GET /v1/applications/{uuid}/webhook (or copy them from the app's Push to deploy panel), then:

Provider Where Fields
GitHub Repo → Settings → Webhooks → Add webhook Payload URL = url, Content type = application/json, Secret = secret, event = “Just the push event”.
GitLab Settings → Webhooks URL = url, Secret token = secret, enable Push events.

Ngris verifies every delivery's signature (GitHub X-Hub-Signature-256 / GitLab X-Gitlab-Token) against the secret, and deploys the exact pushed commit. Rotate the secret any time with POST /v1/applications/{uuid}/webhook/regenerate — remember to update it in the provider afterward.

Serving settings

Tune how files are served with PUT /v1/applications/{uuid} or the app's Serving settings panel:

  • Publish directory (publish_dir) — the subfolder that is your site root (e.g. dist). Leave blank if index.html is at the archive root.
  • Index document (index_document) — served for directory requests. Default index.html.
  • SPA fallback (spa_fallback) — serve the index for any unmatched path. Turn this on for React/Vue/Svelte apps that use client-side routing.
  • Custom 404 (not_found_path) — a document served for missing paths when SPA fallback is off.

Builds & environment variables

When your source contains a framework (a package.json with a build script, or a Hugo/Jekyll config), Ngris runs the build for you in a locked-down, single-use sandbox: no network except your package registry, no credentials, read-only root, hard CPU/memory/time limits. It installs dependencies, runs the build, and publishes the output directory it detects for your framework (dist/, build/, .next/, public/…).

Environment variables & secrets

Front-end builds need config — API URLs, publishable keys, feature flags. Add them under the app's Environment variables panel (or the API); each one is injected into your build with its exact name, so VITE_API_URL or NEXT_PUBLIC_TOKEN is available to vite build / next build as-is.

  • Mark sensitive values as Secret — they're encrypted at rest and never returned by the API or shown in the dashboard again (write-only).
  • Changes apply on your next deploy; trigger one with a push or a re-upload.
  • Names must match [A-Za-z_][A-Za-z0-9_]*.
curl -X PUT "https://api.ngris.com/v1/applications/{uuid}/env" \
  -H "X-API-KEY: <your_api_key>" \
  -H "Content-Type: application/json" \
  -d '{"name":"VITE_API_URL","value":"https://api.example.com","is_secret":false}'

List them with GET /v1/applications/{uuid}/env (secret values masked) or remove one with DELETE /v1/applications/{uuid}/env/{name}.

Build logs (live)

Every build’s full log is captured — success or failure — so you can see exactly what ran and why a build broke. Open a deploy’s Log from the Deploys page and it tails live while the build runs; or fetch the log directly (an X-Build-Status response header reports whether the build is still running):

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

Runtime logs (live)

Build logs show what happened while your app was being built; runtime logs show what your app prints while it is running — its own stdout/stderr. Reach for runtime logs when a deploy built fine but the app then crashes, crash-loops, or returns 500s at runtime (a panic, a bad connection string, a missing env var). They’re on the Runtime Logs tab of the app’s detail page (live-tailing every few seconds), or fetch them directly:

curl "https://api.ngris.com/v1/applications/{uuid}/runtime-logs?tail=200" \
  -H "X-API-KEY: <your_api_key>"

The X-Runtime-Status response header reports the pod state (running, pending, failed, …). Add &previous=1 to read the previous (crashed) instance’s logs when the current pod has just restarted. Runtime logs apply to managed, fullstack, WordPress, and serverless-function apps — a static app is served straight from the edge with no running process, so it has no runtime logs.

Managed backends

A managed backend runs a long-running server on the Ngris edge — a Go, Node, or Python service, a PHP app, or any stack that ships with a Dockerfile. Create the app with type managed (backend only) or fullstack (static front end + a path-routed backend), from an upload or a connected git repo, exactly like a static app.

How a backend build differs from static. Instead of building to files, Ngris builds your Dockerfile into a container image in a sandboxed job, then signs it, scans it for vulnerabilities, and pins it to its digest. The running backend is gVisor-isolated and reached through a managed endpoint. Because each deploy's image is immutable and digest-pinned, rolling back to an earlier version is instant.

Managed backends require the managed_backends entitlement. They run a long-running process; for a per-request, scale-to-zero handler, use a Serverless Function instead.

Runtime settings

Tune how the backend runs from the app's Runtime settings (or PUT /v1/applications/{uuid}/runtime):

  • Port (port) — the port your server listens on (1–65535).
  • Health check path (health_check_path) — an absolute path probed for readiness (e.g. /healthz). Default /.
  • CPU & memory (cpu_millis 50–4000, memory_mb 64–8192) — requests equal limits (Guaranteed QoS).
  • Replicas (min_replicas 0–10, max_replicas 1–10).
  • Backend paths (backend_paths, fullstack) — the URL prefixes routed to the backend (default ["/api"]); everything else serves the static front end.

Read live runtime status — instances, ready replicas, restarts — from GET /v1/applications/{uuid}/status.

Environment variables at runtime

A backend's environment variables are delivered to the running container, not just the build — so config, tokens, and connection strings reach your process at start-up. Mark sensitive values as Secret: they're encrypted at rest, masked in the API and dashboard (write-only), and decrypted only into the running backend. Changes take effect on the next deploy or rollout.

Managed database

A full-stack app (one with a backend) can add a fully-managed, MySQL-compatible database (MariaDB) from its Database tab on the app detail page. Ngris provisions the database and a user for you and injects the connection into your running backend as environment variables — there is nothing to install, operate, or copy.

MariaDB / MySQL only. The managed engine is a MySQL-compatible database (MariaDB). It is not Postgres, Redis, or Mongo. If you need a different engine, run it elsewhere and set DATABASE_URL as a normal secret environment variable instead.

Provisioning requires the managed_database entitlement, which is on for paid plans and off on the free tier. Add the database from Applications → your app → Database → Add database.

Per-app isolation

Each app gets its own schema and a dedicated least-privilege user whose grants are scoped to just that schema (SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX on that database only — nothing global), on a hardened, in-cluster mariadb-apps server. One app's credentials cannot reach another app's data. Provisioning runs in a locked-down, one-shot job (non-root, all capabilities dropped, read-only root filesystem, seccomp), and the admin credential that creates schemas never reaches your app.

The injected connection

When the database is ready, Ngris sets these variables on your app and redeploys it so the backend picks them up. The password is stored encrypted and appears only as the masked DB_PASSWORD value; read DATABASE_URL and you're connected.

  • DB_HOST, DB_PORT — the in-cluster mariadb-apps host and port.
  • DB_NAME — your app's dedicated schema.
  • DB_USER, DB_PASSWORD — the least-privilege user scoped to that schema (password write-only / encrypted at rest).
  • DATABASE_URL — a ready-to-use mysql:// DSN combining the above.

The Database tab shows provisioning while it's being created, then ready with the connection identifiers (never the password). If provisioning fails you can retry — no partial database is left behind.

Preview deploys

Every ready deploy gets its own shareable preview URL — independent of which version is live — so you can review a build before promoting it, or share a work-in-progress. The URL is served read-only from the edge exactly like the live site (same TLS, same speed).

Find it as the Preview link next to any deploy on the Deploys page, or read preview_url from GET /v1/applications/{uuid}/deploys. The URL embeds the deploy’s unguessable id, so it is shareable without exposing your other versions. Promote a preview to live any time with the deploy’s Activate action — an instant, atomic pointer flip.

Visibility — public or private

Visibility controls which Ngris users can reach your app at all. You set it independently for the production URL and for preview URLs, so you can ship a public site while keeping every preview build inside your team — or lock the whole app down while it's in progress.

Value Who can view
public Anyone with the link — served exactly as before, no sign-in.
private Only members of the app's owning Ngris account.

Defaults: production is public and previews are private — your live site is open to the world, while a fresh preview build is visible only to your team until you choose to share it. Change either from the app's Visibility settings or with PUT /v1/applications/{uuid}/visibility. Visibility is available on every app — there is nothing to enable and no plan gate.

How a private app is enforced

Enforcement happens at the edge, on every request — there is nothing to wire into your app. When an app is private, a visitor must (1) be signed in to Ngris and (2) be a member of the app's owning account:

  • A visitor who isn't signed in is bounced to the Ngris sign-in page, then returned to the app.
  • A signed-in user who is not a member of the owning account gets a branded “You don't have access” page.
  • A public app is served straight through, exactly as before.

This applies both to apps on Ngris platform domains (for example *.ngris.dev) and to your own custom domains (BYOD / CNAME). Preview URLs (<deploy-uuid>.<domain>) honour preview_visibility.

Visibility is not Endpoint Auth. Visibility decides which Ngris users can reach the app — it's account-level gating, run by Ngris. Endpoint Auth is different: that's where you run your own login / registration for your end-users (clients, OAuth providers, RBAC). The two are independent and can be used together — a public app can still require Endpoint Auth for its end-users, and a private app restricts who from your account can even see it.

Custom domain & HTTPS

A Static App is exposed by an agentless endpoint. On a shared domain you get an auto-generated name.ngris.app URL instantly; to use your own domain, add it under Domains, then create the endpoint on it — TLS is provisioned automatically.

curl -X POST "https://api.ngris.com/v1/endpoints" \
  -H "X-API-KEY: <your_api_key>" \
  -H "Content-Type: application/json" \
  -d '{"serving_mode":"agentless","application_uuid":"<app_uuid>","subdomain":"app","is_custom":true,"domain_id":42}'

Everything you can put on a normal endpoint applies here too — auth, firewall, and rate limits all work on agentless endpoints.

Deploy lifecycle — restart, roll out & roll back

Every upload or push creates a new immutable deploy. The app has one live deploy at a time. The first successful build activates itself; from then on you switch versions explicitly from the Deploys tab (or POST /v1/applications/{uuid}/deploys/{deploy_uuid}/activate).

Activating a deploy from the Deploys tab covers three things:

  • Roll back — activate an older, still-rollbackable version. Static apps flip to its built bundle instantly; managed backends re-roll the pod onto that version's digest-pinned image.
  • Restart / roll out — activate the current version to re-ship it (managed backends re-roll the running pod; useful to pick up new env vars).
  • Promote a preview — make a reviewed preview deploy live.

For a static app this is an atomic pointer flip — no rebuild, no downtime. For a managed backend the rollout goes through the runtime: it re-points the Deployment at that deploy's already-built, scanned, signed image and advances the live version only once the new pods are proven ready.

Rollback window (retention)

Only the newest N terminal deploys stay rollbackable (the deploy_max_rollback_versions plan limit — more on higher tiers). Older deploys are pruned: their build artifacts (the static bundle, or the backend's container image) are deleted to bound storage, and they can no longer be rolled back to. The row survives for history, so the Deploys tab still lists a pruned version — just with no Roll back action. The currently-live version is always kept, never pruned.

In the API, each deploy carries rollbackable and pruned flags so you can tell which versions can still be activated. Trying to activate a pruned deploy returns 409 — re-deploy from source instead.