Docs

Databases

Add a database to Niral with built-in SQLite or connect to PostgreSQL. Learn persistent data storage, parameterized queries, TLS and multi-server setup.

On this page 5 sections

Niral has a database in the box. SQLite is the default — and for most apps, the finish line. When you outgrow one machine, the same code talks to Postgres instead. No ORM to learn, no driver to install.

SQLite — the default

node:sqlite ships with Node, so there's nothing to set up. Your data lives in data/app.db, which sits outside the release folder — so it survives every deploy — and niral snapshot backs it up.

The fastest start is a recipe — it scaffolds a complete, working database-backed route you own:

niral add sqlite

Or open a database yourself in any <server> block with Node's built-in driver:

<server>
import { DatabaseSync } from "node:sqlite"
const db = new DatabaseSync("data/app.db")
db.exec("CREATE TABLE IF NOT EXISTS posts (id INTEGER PRIMARY KEY, title TEXT)")

export async function load() {
  return { posts: db.prepare("SELECT * FROM posts ORDER BY id DESC").all() }
}
export async function add(title) {
  db.prepare("INSERT INTO posts (title) VALUES (?)").run(title)
}
</server>

One file, one process, zero services — a $5 box runs the whole thing. Most products never need more.

Postgres — when you need it

Heavy concurrent writes, multiple servers, or big data? Point at Postgres. Niral speaks the Postgres wire protocol directly — SCRAM-SHA-256 auth, TLS, parameterized queries and a connection pool — with its own pure-Node driver. No pg, no npm, still zero dependencies.

You don't install Postgres either: use a managed database (Neon, Supabase, AWS RDS, GCP Cloud SQL, Railway, …). They hand you a URL — set one env var:

# managed — install nothing, TLS is built in
NIRAL_DATABASE_URL=postgres://user:[email protected]/db?sslmode=require

# or your own box on a private network
NIRAL_DATABASE_URL=postgres://user:pass@localhost:5432/mydb

Now the ambient sql works in any <server> block:

<server>
export async function load({ params }) {
  // $1, $2 params are SQLi-safe — the value is data, never SQL
  const { rows } = await sql.query(
    "select id, title from posts where author = $1 order by created_at desc",
    [params.author]
  )
  return { posts: rows }
}

export async function publish(title, body) {
  const { rows } = await sql.query(
    "insert into posts (title, body) values ($1, $2) returning id",
    [title, body]
  )
  return rows[0].id
}
</server>

sql.query(text, params) returns { rows, fields }. Types come back decoded — int → number, bool → boolean, json/jsonb → object. A pool is managed for you; just await sql.query(...).

TLS modes

TLS follows libpq semantics via the connection string:

sslmodebehaviour
*(none)* / disableplaintext — localhost or a private network
requireencrypt the connection (managed providers)
verify-fullencrypt and validate the server certificate

Safety

Always pass values as parameters ($1, ?), never string-concatenate them into SQL. Parameters are sent separately from the query text, so an injection payload arrives as plain data — it can never become SQL. This is the same rule for SQLite and Postgres.

Scaling out

Running Postgres across multiple servers? See Scaling to many servers — the same database also powers Niral's realtime backplane and its shared background-job queue, so a whole cluster coordinates through one Postgres with no extra moving parts.