Skip to content

Per-user databases

Give every user of your app their own isolated SQLite database. One call maps a user identifier to a database (created the first time you ask for it), and mints a token scoped to only that database. Because the database boundary is the tenant boundary, your queries stay single-tenant: SELECT * FROM orders — no WHERE tenant_id, no row-level rules, no cross-tenant fan-out. The isolation lives in the topology, not the query.

import { PerSQL } from "@persql/sdk";
const persql = new PerSQL({ token: process.env.PERSQL_TOKEN });
const { token, database, namespaceSlug } = await persql.users.provision(
"user_42",
{ template: "app-template" }
);
// Query that user's database — ordinary SQL, only their rows exist.
const db = persql.database(namespaceSlug, database.slug);
await db.query("SELECT * FROM orders");

provision is idempotent on the subject: the same identifier always lands on the same database, so it’s safe to call on every sign-in. The first call creates the database; later calls reuse it. Each call returns a fresh token.

POST /v1/users
Authorization: Bearer psql_live_...
{
"subject": "user_42",
"template": "app-template",
"role": "write",
"expiresIn": 3600
}

Returns the user’s database, a token scoped to it, and created (whether this call provisioned the database or reused an existing one).

fieldmeaning
subjectYour app’s identifier for the user. Any string up to 256 chars — a UUID, an email, an external id. Hashed into a stable database slug.
templateOptional. A database in the same workspace whose schema (tables, indexes, triggers — never its rows) seeds the new database, so it’s query-ready on the first request. Omit for an empty database.
rolewrite (default) or read. Governs what the minted token may do.
expiresInToken lifetime in seconds, 60–86400. Defaults to one hour.

Requires an unscoped admin token. Provisioning is a privileged operation, so a per-user token can’t provision more users.

The minted token is scoped to exactly one database and runs the full /v1 SQL surface against it. Two ways to use it:

  • Server-side — keep using your own workspace token; you already have access to every user’s database. The per-user token is optional here.
  • Client-side — hand the per-user token to that user’s browser or app. It reaches only their database, so an untrusted client can query directly without a proxy. A leaked token’s blast radius is one user.

Usage bills to your workspace — your users never see PerSQL. The token is short-lived by default and revocable; mint a fresh one each time the user authenticates.

The per-user token deliberately cannot run DDL. Your app owns the schema: seed it with template, and roll out changes with migrations. The token reads and writes rows; it never alters structure.

A populated database is metered like any other (storage, requests, rows); a fresh schema-only database is nearly free until it’s used. Auto-expire disposable per-user databases by setting a TTL when you provision, or delete them when a user leaves.

  • Sign in with PerSQL — let an app provision a database in the end user’s own workspace instead of yours.
  • Branches — schema-only databases keyed by an external ref, for per-PR previews.
  • Forking — copy a database’s schema (and optionally its rows) into a new one.