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.
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.
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 formmysql://user:pass@host:port/dbname.
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)
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 database — app_<accountID>_<appID> — and a dedicated least-privilege user — a_<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 agolang-migratestep in your entrypoint). Because the app already hasDATABASE_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(amysql://URL) for Node/Prisma/SQLAlchemy, but auser:pass@tcp(host:port)/dbDSN built from theDB_*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(orDATABASE_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:
- 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>" - Read the connection in your code. Use
DATABASE_URL(Node/Python) or build the DSN fromDB_*(Go). Don't set these yourself — Ngris injects them. - 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.
- Verify connectivity. Add a route (e.g.
/db-check) that runsSELECT 1against the connection and returns200— 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.