Databases

Managed databases for apps

Add a fully-managed MySQL-compatible database (MariaDB) to your app with one click — nothing to install, operate, or copy. Ngris provisions a private database and a dedicated user for you, then injects the connection into your running app as environment variables. Your app connects on boot; you never handle a password by hand.

A managed database is available for apps that run a server — a backend app or a full-stack app. Each database is private to your app: it is reachable only from your app's own container, never from the public internet.

MariaDB / MySQL only. The managed engine is a MySQL-compatible database (MariaDB). Other engines may appear in the picker marked “coming soon.” If you need Postgres, Redis, or Mongo today, run it elsewhere and set DATABASE_URL as a normal secret environment variable instead.

Managed databases require the managed_database entitlement, which is on for paid plans and off on the free tier.

Add a database

Open your app's detail page, go to the Database tab, and click Add database. Choose the engine — MySQL today; others may show as “coming soon.” Provisioning takes only a few seconds; the status moves from provisioning to ready.

API

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

Poll the binding until it is ready:

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

The response reports the current status and whether your account is entitled, plus the connection identifiers once the database is ready — but never the password.

Add it before or after your first deploy. Provisioning is independent of deploys. Add the database before you first deploy so your app has it on boot, or add it to an app that's already running — Ngris then rolls the app so it picks up the new connection environment.

How your app connects

This is the most important — and most error-prone — part. Once the database is ready, Ngris sets these variables on your app automatically and (re)deploys it so your process reads them on start-up:

  • DB_HOST, DB_PORT — the in-cluster database host and port (plain).
  • DB_NAME — your app's dedicated database (plain).
  • DB_USER — the least-privilege user scoped to that database (plain).
  • DB_PASSWORD — the user's password (secret — encrypted at rest, decrypted only inside your pod).
  • DATABASE_URL — a ready-to-use connection URL (secret) of the form mysql://user:pass@host:port/dbname.
You never see or set the password. DB_PASSWORD and DATABASE_URL are injected encrypted and decrypted only inside your running container — they are masked in the API and dashboard. Build your connection from the injected env vars (or read DATABASE_URL); never hard-code credentials.

Node (mysql2 / Prisma)

Use DATABASE_URL directly — both accept the mysql:// URL as-is.

// mysql2
import mysql from "mysql2/promise";
const db = await mysql.createConnection(process.env.DATABASE_URL);

// Prisma (schema.prisma)
datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

Python (SQLAlchemy)

SQLAlchemy needs a driver in the URL scheme (e.g. mysql+pymysql://). Either swap the scheme on DATABASE_URL, or build the URL from the injected parts so the scheme is explicit:

import os
from sqlalchemy import create_engine

url = (
    f"mysql+pymysql://{os.environ['DB_USER']}:{os.environ['DB_PASSWORD']}"
    f"@{os.environ['DB_HOST']}:{os.environ['DB_PORT']}/{os.environ['DB_NAME']}"
)
engine = create_engine(url)

Go (go-sql-driver/mysql)

⚠ The Go driver does NOT accept the mysql:// URL. go-sql-driver/mysql wants a DSN in the form user:pass@tcp(host:port)/dbname, not a URL. Passing DATABASE_URL to sql.Open is a common, silent failure — build the DSN from the injected DB_* vars instead.
import (
    "database/sql"
    "fmt"
    "os"

    _ "github.com/go-sql-driver/mysql"
)

dsn := fmt.Sprintf(
    "%s:%s@tcp(%s:%s)/%s?parseTime=true&charset=utf8mb4",
    os.Getenv("DB_USER"), os.Getenv("DB_PASSWORD"),
    os.Getenv("DB_HOST"), os.Getenv("DB_PORT"), os.Getenv("DB_NAME"),
)
db, err := sql.Open("mysql", dsn)

The parseTime=true&charset=utf8mb4 query keeps DATETIME/TIMESTAMP columns scanning into time.Time and gives you full Unicode (including emoji).

Isolation & limits

Every app gets its own databaseapp_<accountID>_<appID> — and a dedicated least-privilege usera_<base36(appID)> — whose grants are scoped to only that database. One app's credentials cannot reach another tenant's data.

  • Not public. The database is reachable only from your app's container inside the cluster — there is no public endpoint to expose or firewall.
  • Per-user caps. Each database user has connection and query-rate limits applied at provision time, so one app can't starve the shared server.
  • Least privilege. The user can read and write its own schema only; the admin credential that creates databases never reaches your app.

Migrations & seed data

Ngris provisions an empty database and the user that owns it — your app owns the schema. Run schema migrations and seeds from your app using the injected connection, the same way you would anywhere else. Two common patterns:

  • On boot. Run your migration tool at start-up before the server begins accepting traffic (e.g. Prisma migrate deploy, an ORM auto-migrate, or a golang-migrate step in your entrypoint). Because the app already has DATABASE_URL / DB_* in its environment, no extra config is needed.
  • One-off. Ship a small command in the same image that reads the same env vars and applies migrations, and run it as a one-shot before promoting a deploy.

The least-privilege user has the schema-level rights needed to create and alter your own tables (CREATE, ALTER, INDEX) inside its database — migrations run under the same credentials your app uses.

Troubleshooting

  • Stuck at “Provisioning.” Provisioning normally finishes in a few seconds. If it doesn't reach ready, hit Add database again from the Database tab — a failed provision resets and retries safely, and no partial database is left behind.
  • App can't connect. Almost always the wrong variable or format. Confirm your driver's expectation: DATABASE_URL (a mysql:// URL) for Node/Prisma/SQLAlchemy, but a user:pass@tcp(host:port)/db DSN built from the DB_* vars for Go. See How your app connects.
  • “Access denied.” You're likely using stale or hard-coded credentials instead of the injected ones. Read DB_USER/DB_PASSWORD (or DATABASE_URL) from the environment at runtime — never bake a password into your image.
  • New database, but the running app can't see it. The env vars land on the next rollout. If you added the database to an already-running app, wait for the automatic roll to finish, or re-deploy to pick up the new connection.

Still stuck? See Deploy troubleshooting.

Tutorial: deploy an app with a database

End to end, from zero to a backend talking to its own database:

  1. Add the database. Open your backend or full-stack app, go to the Database tab, and click Add database (MySQL). Wait for the status to reach ready — or do it in one call:
    curl -X POST "https://api.ngris.com/v1/applications/{uuid}/database" \
      -H "X-API-KEY: <your_api_key>"
  2. Read the connection in your code. Use DATABASE_URL (Node/Python) or build the DSN from DB_* (Go). Don't set these yourself — Ngris injects them.
  3. Deploy. Push or upload your app. On boot it reads the injected variables and connects; run your migrations at start-up so the schema is ready.
  4. Verify connectivity. Add a route (e.g. /db-check) that runs SELECT 1 against the connection and returns 200 — or watch your build/runtime logs for a successful connect on start-up.

From here, everything on the app applies unchanged — env vars, custom domains, rollbacks. The database follows the app: it stays private, and the injected connection is refreshed on every deploy.

Iris