1. guides
prorm
  • prorm documentation
  • Known test gaps
  • Architecture
  • Core concepts
  • Database types & coverage
  • Getting started
  • Going further
  • compliance
    • Compliance
    • The security toolkit: `prorm audit`
    • Checklists by regulation
    • Declaring personal data
    • Evidence bundles
    • Evidence
    • Frameworks: `@Security`
    • Jurisdictions
    • Platform policies
    • Protecting data
    • The regulation dataset
    • Residency and cross-border transfers
    • Retention & integrity
    • Status
    • Subject rights
  • decorators
    • prorm with decorators
    • Models with decorators
    • Querying a decorated model
    • Relations with decorators
  • diagrams
    • Diagram generators
    • Chen diagram
    • Class diagram
    • Dependency diagram
    • ER diagram
    • Flow diagram
    • From your models to a diagram
    • Gantt diagram
    • Index diagram
    • Migration diagram
    • Model diagram
    • Package diagram
    • Crow’s-foot diagram
    • Schema documentation
    • Sequence diagram
    • State diagram
    • Tree diagram
  • dialects
    • SQL dialects
    • AlloyDB Dialect
    • AlloyDB Omni Dialect
    • Amazon Athena Dialect
    • Aurora DSQL Dialect
    • Amazon Aurora MySQL Dialect
    • Amazon Aurora PostgreSQL Dialect
    • Azure Database for MySQL Dialect
    • Azure Database for PostgreSQL Dialect
    • Azure SQL Database Dialect
    • Azure SQL Managed Instance Dialect
    • Google BigQuery Dialect
    • Citus Dialect
    • ClickHouse dialect
    • Cloud Authentication (`cloud-auth`)
    • Cloud SQL for SQL Server Dialect
    • Cloud SQL for MySQL Dialect
    • Cloud SQL for PostgreSQL Dialect
    • CockroachDB Dialect
    • Azure Cosmos DB for PostgreSQL Dialect
    • CrateDB Dialect
    • Cloudflare D1 Dialect
    • Databricks dialect
    • IBM Db2 Dialect
    • Dolt Dialect
    • Apache Doris Dialect
    • Druid Dialect
    • DuckDB Dialect
    • Exasol dialect
    • Microsoft Fabric Data Warehouse Dialect
    • Firebird Dialect
    • Greenplum Dialect
    • SAP HANA Dialect
    • MySQL HeatWave Dialect
    • Azure Data Explorer (Kusto) Dialect — T-SQL
    • MariaDB Dialect
    • Materialize Dialect
    • MonetDB Dialect
    • MotherDuck Dialect
    • Microsoft SQL Server Dialect
    • MySQL Dialect
    • Neon Dialect
    • OceanBase Dialect
    • openGauss Dialect
    • Oracle Dialect
    • Oracle Autonomous Database Dialect
    • ParadeDB Dialect
    • PolarDB Dialect
    • PostgreSQL Dialect
    • Presto Dialect
    • QuestDB Dialect
    • Aurora RDS Data API Dialect (`rdsdata`)
    • Amazon RDS for Db2 Dialect
    • Amazon RDS for MariaDB Dialect
    • Amazon RDS for SQL Server Dialect
    • Amazon RDS for MySQL Dialect
    • Amazon RDS for Oracle Dialect
    • Amazon RDS for PostgreSQL Dialect
    • Amazon Redshift
    • Amazon Redshift Data API Dialect
    • RisingWave Dialect
    • rqlite Dialect
    • SingleStore Dialect
    • Snowflake Dialect
    • Google Cloud Spanner Dialect
    • Spanner (PostgreSQL interface) Dialect
    • SQLite Dialect
    • StarRocks Dialect
    • Supabase Dialect
    • Azure Synapse Analytics (dedicated SQL pool)
    • Teradata Dialect
    • TiDB Dialect
    • TimescaleDB Dialect
    • Trino Dialect
    • Turso / libSQL Dialect
    • Vertica dialect
    • Vitess Dialect
    • Yellowbrick Dialect
    • YugabyteDB Dialect
  • guides
    • Access rules and behaviour profiles
    • ArangoDB (AQL)
    • Associations
    • Audit levels and escalation
    • Audit logging
    • In the browser
    • Bulk operations
    • Caching
    • Capabilities, tiers and team roles
    • CLI
    • Cloudflare
    • Compliance & data protection
    • Connection Pooling & Performance
    • Security in the console
    • Couchbase: structured N1QL queries
    • Data types
    • Decorators
    • Eager loading
    • Enums and constants
    • Error handling & error types
    • Extensions
    • External fields
    • Foreign data wrappers
    • Graph databases
    • Hooks & lifecycle events
    • Indexes & constraints
    • InfluxDB: structured Flux pipelines
    • klock-errors
    • klock — the `.klock` file
    • Local databases (`prorm db up`)
    • Structured log search — Splunk, Graylog, Papertrail, Sumo Logic
    • Logging
    • Grafana Loki: structured LogQL queries
    • MCP server
    • Migrate — the data transfer subsystem
    • Migrations & Schema Sync
    • Models from an existing database
    • Defining models
    • Multiple databases
    • OrientDB, Neptune and TigerGraph
    • Passwords
    • Importing a Prisma schema
    • Structured PromQL and MetricsQL
    • Query firewall
    • QueryInterface
    • Query operators reference
    • Query optimization
    • Composing find options with `pipe()` and `Q`
    • Query safety: the fail-closed WHERE guard
    • Querying
    • Raw Queries & Query Building
    • Read replicas
    • Schema diffing & migration generation
    • Views, triggers & other schema objects
    • Scopes & default scopes
    • Seeders
    • SPARQL / RDF triplestores
    • SQL constants
    • SQL features
    • SQL function builders
    • SQLite advanced features
    • StoreKey — a prod-orm database as the storage
    • Streaming results
    • Terraform
    • Transactions & Isolation Levels
    • Type mapping: legacy and modern
    • TypeScript types
    • Database users, roles & privileges
    • Validation & constraints
    • Virtual & Computed Fields
    • Writing docs
  • javascript
    • prorm in JavaScript
    • Models in JavaScript
    • Querying in JavaScript
    • Relations in JavaScript
  • runbooks
    • Operational runbooks
    • Connection failures
    • Credential rotation
    • Failover
    • Incident checklist
    • Deploying klock v2
    • Migration rollback
  • running-databases
    • Running databases in Docker
    • Running prorm under Bun in Docker
    • Running Cassandra in Docker with prorm
    • Running ClickHouse in Docker with prorm
    • Running CockroachDB in Docker with prorm
    • Running prorm under Deno in Docker
    • Running Elasticsearch in Docker with prorm
    • Running MariaDB in Docker with prorm
    • Running MongoDB in Docker with prorm
  • prorm control plane
    • Introduction
    • Authentication
    • Errors
    • Getting started
      • Service
        • Is it up
        • This description
    • Account
      • Sign-up and verification
        • Create an account
        • Exchange credentials for a token
        • Verify with the emailed code
        • Verify by clicking the emailed link
        • Name the parent or guardian
        • What a consent link is about
        • Give consent for a child's account
        • Ask the parent or guardian again
        • Send a new code and link
      • Family
        • Everyone this account is responsible for
        • One child, in detail
        • Turn a limit on or off
      • Profile
        • Who am I
        • Change the account
        • Every address on the account
        • Add an address
        • Confirm an address with the code mailed to it
        • Mail a fresh code to an unconfirmed address
        • Make an address primary
        • Remove an address
        • What this caller may do
    • Security
      • Sessions
        • Everywhere this account is signed in
        • Sign out everywhere else
        • Sign one out
        • A token for the CLI on a named machine
        • A token for the customer console, from the staff one
        • Park a sign-in for the CLI to collect
        • The CLI collecting its sign-in
        • The team's members' live sessions
        • Sign a member's session out
        • Sign an account out everywhere
        • Everywhere the account is signed in
        • Sign an account's session out
      • Single sign-on
        • Begin a single sign-on
        • The provider's return
        • Link the signed-in account to a team's provider
        • Which team an address signs in to
        • The team's single sign-on, as a member sees it
        • Set the team's OpenID Connect provider up, or change it
        • Remove the provider
        • The team's domain claims and the DNS record each needs
        • Prove a claimed domain
      • MFA and passwords
        • Start enrolling a second factor
        • Prove the app has it
        • Remove the second factor
        • Ask for a reset link
        • Set a new password with the emailed token
      • Klock
        • Open a device-bound klock session
        • Check in, for the next grant
        • Step up the session asking
        • Rotate the device key
        • Freeze a session
        • Unfreeze a session
        • A guest grant
        • The revocation feed
        • The published keyset
        • What this control plane supports
      • Devices
        • Your devices, or a team's
        • Approve a new device's pairing code
        • One device
        • Rename a device
        • Revoke a device
        • Suspend a device
        • Reinstate a suspended device
        • Wipe a device
        • Ask a device to rotate its key
        • The team's members' devices
        • Revoke a member's device
        • An account's devices
        • Revoke an account's device
        • Wipe an account's device
        • The device fleet
      • Access rules and behaviour
        • The access rules that bear on you
        • Add an access rule
        • This request, as the rules see it
        • One access rule
        • Remove an access rule
        • The behaviour profiles that bear on you
        • Add or change a behaviour profile
        • The profile presets
        • Your behaviour findings
        • Acknowledge a finding
        • One behaviour profile
        • Remove a behaviour profile
        • The team's access rules
        • Add a team access rule
        • Remove a team access rule
        • The team's behaviour profiles
        • Add or change a team behaviour profile
        • Remove a team behaviour profile
        • What the team's profiles caught
        • Acknowledge a team finding
      • Recovery and lockdown
        • Ask for a recovery link
        • Recover access with the emailed code
        • Is the account locked down
        • Lock the account down
        • Release a lockdown
        • Lock a member down
        • Release a member's lockdown
      • MCP tokens
        • Make a delegated token for one MCP client
        • The account's delegated tokens
        • Revoke a delegated token
        • What a delegated token may do now
        • Run one MCP tool with a delegated token
        • Run a device-signed MCP tool call relayed by the MCP Worker
      • Kerberos
        • Give the team its own Kerberos realm
        • The team's realm
        • Remove the team's realm
    • Logging and monitoring
      • Audit log
        • What has been done to the account
        • What has happened to this account
        • The account's audit trail
      • Operations
        • Everything that has been asked of this database
        • One, with its steps
        • Stop it
        • Put a failed one back in the queue
        • Every operation, across all accounts
      • Metrics and events
        • Every measurement, for a graph
        • Who did what to it
    • Billing
      • Plans and pricing
        • What the plans allow
        • The rate card
        • What one database would cost
        • What this account is paying for, itemised
      • Subscriptions and invoices
        • What this month's invoice will say
        • What this account is on
        • What it has been charged
        • Start a Stripe Checkout session
        • Open the customer portal
        • Stripe's events
        • Buy more connection pools
      • Team billing
        • The team's billing account
        • What the team has been charged
        • What this month's invoice will say
        • The team's cards
        • Add a card, through a Stripe-hosted setup session
        • Make a card the team's default
        • Remove a card
        • Start the team paying
        • Open the Stripe portal on the team's customer
        • Change the team's plan
        • Who receives the team's bills
        • How the team pays for transfers
    • Support
      • Support chat
        • My conversations
        • Start a conversation
        • Every support conversation, open first
        • A ticket for the socket
        • One conversation, with who is in it and who is connected
        • Rename a group; open or close a support conversation
        • The lines, oldest first
        • Say something
        • Add somebody
        • Mark read
    • Administration
      • Users
        • Every account
        • One account, with everything hanging off it
        • Grant or revoke admin
        • Verify an account by hand
        • Lock or unlock sign-in
        • Allow or hold provisioning
        • Send the verification mail again
        • Set the plan by hand
      • Platform
        • Every database and store, across all accounts
        • The counts worth putting on a dashboard
      • Staff and permissions
        • What kind of admin the caller is, and which scopes they hold
        • The admin presets
        • Every admin, and the scopes each resolves to
        • Make an account an admin of a kind
        • Change an admin's kind, or a custom admin's scopes
        • Revoke admin
      • Team administration
        • Every team, with its owner and size
        • One team in full — members with their accounts, and projects
        • Hand the team to another member
        • Put an account in the team, active at once
        • Change a member's role
        • Remove a member
      • Assumed identities
        • Assume an identity — act as a customer account
        • The identities the caller holds open
        • End an assumed identity
      • Support inbox
        • The platform's addresses
        • Add a platform address
        • One address, with its counts
        • Change an address's label, routing, or whether it accepts mail
        • Remove an address that holds no mail
        • Make, adopt or repair the address's routing rule
        • Every admin's personal address, made where missing
        • File a raw message, as if it had arrived
        • The inbox
        • One message, whole
        • Mark a message
        • Hand a message to an admin, or take it back
        • Leave an internal note
        • Reply, from the address the message came to
        • What is waiting
    • Databases
      • Databases
        • Data stores
          • Every data store on the account
          • Provision a data store
          • Change a data store that is already running
          • One data store
          • Deprovision it
          • Report what it is using
          • How to reach a data store
        • Connections and credentials
          • The api keys on a database
          • Issue a key
          • Revoke a key
          • The engine users we created
          • Create a user inside the database
          • Give a user a new password
          • Drop a user
          • How the database is reached
          • Set how it is reached
          • Revoke every api key on it
          • How to connect to it
          • The ssh keys that open its bastion
          • Add a key, or have one made
          • The private half of a generated key, once
          • Revoke an ssh key
        • Schema and objects
          • The SQL objects we put in it
          • Create one, and apply it
          • What is in the database, as the engine reports it
          • One table
          • What it would run, without running it
          • Redefine an object in place
          • Drop it
          • The extensions installed in it
        • Migrations
          • A plan compiled to statements, recording nothing
          • The migrations recorded for it
          • Record one
          • Apply everything recorded and not yet run
          • Apply everything pending, oldest version first
        • Lifecycle
          • Grow or shrink it
          • Stop it accepting connections
          • Start it again
          • Move it to another region
        • Backups and replicas
          • Take a backup
          • The backups it has
          • Restore one into it
          • What has been restored into it, and from where
          • Its read copies
          • Add one
        • Policies
          • The catalog
          • One policy
          • What this database is held to
          • Switch a policy on, or change its knobs
          • Switch it off
          • Mark the policies reviewed
          • Evaluate the periodic policies now
          • Who is behind a credential
        • Passwordless
          • The database's passwordless sessions
          • Issue a passwordless session (v1)
          • Revoke a passwordless session
          • Bind a session to a device key
          • Issue a v2 session for a host to bind
          • Redeem an enrolment ticket on the host
          • Check a passwordless credential
          • Run one described query under a passwordless credential
        • Database roles
          • The database's roles
          • Define a role
          • One role
          • Change what a role permits
          • Delete a role, and revoke every key granted it
          • Grant a role, which mints a key
          • Who holds a role
          • The api keys bound to a role
        • Pools
          • A database's connection pools, and how busy each is
          • Open another pool
          • Rename a pool or change its size
          • Close a pool
        • Data plane
          • Run a query against a database
          • Several statements, in order, over one connection
          • Is this database answering
          • What the proxy itself is holding
        • Transfers
          • The strategies this account's tier may use
          • Price a transfer without creating it
          • The account's transfer jobs, newest first
          • Create a transfer job
          • One job, and whether its certificate holds
          • Cancel an unfinished job
          • Start a created job, confirming its price
          • Report a job's status (the runner, or the owner's side of a local run)
        • Every database on the account
        • Provision a database
        • Change a database that is already running
        • One database
        • Deprovision it
        • Read or write it
        • Report what it is using
        • The engines this API will provision
        • Where a managed database can be put
    • Teams
      • Teams
        • Members
          • Who is on it
          • Invite somebody
          • Accept an invitation
          • Change somebody's role
          • Remove somebody
        • Projects
          • The projects a team runs
          • Start one
          • Rename a project, or move its environment
          • Delete a project
          • One project, and what is in it
          • Put a database or store in it
          • What is in a project
          • Take one out
        • The teams this account is on
        • Start one
        • The twelve roles, and what each may do
        • Rename a team
        • Close a team
        • One team
        • Hand the team to somebody else
        • Leave a team
    • Schemas
      • Error
      • ValidationErrors
      • Token
      • PolicyCategory
      • PolicySeverity
      • PolicyAction
      • PolicyTrigger
      • PolicyParam
      • Policy
      • PolicyCatalog
      • PolicyAssignment
      • PolicyOutcome
      • PolicyFiring
      • CredentialTrace
      • Relation
      • Restrictions
      • ChildSummary
      • SignupRequest
      • SigninRequest
      • UpdateMeRequest
      • User
      • Address
      • GeoPoint
      • D1Region
      • Usage
      • UsageReport
      • Resource
      • Database
      • DataStore
      • NewDatabase
      • NewDataStore
      • QueryRequest
      • DatabasePrivilege
      • ApiKey
      • DbUser
      • Access
      • AccessRequest
      • DatabaseObject
      • NewDatabaseObject
      • NewMigration
      • SchemaOperation
      • Migration
      • Operation
      • Session
      • AdminPowers
      • Admin
      • AuditEntry
      • TeamRole
      • Conversation
      • ChatMessage
      • TeamSso
      • Team
      • TeamMember
      • SshKey
      • ConnectionInfo
      • InboxAddress
      • InboxMailbox
      • InboxMessageSummary
      • InboxMessage
      • InboxNote
      • Page
      • Quote
      • Plan
      • ApiError
      • CapabilityRefusal
      • KlockErrorCode
      • PageWindow
      • SessionView
      • KlockStepUpRequest
      • KlockOpenRequest
      • KlockOpenedSession
      • UpdateMeResult
      • CapabilitiesView
      • SessionLimits
      • Device
      • DeviceListed
      • DeviceTeamRef
      • DeviceCommand
      • DeviceAuditLine
      • DeviceActionRequest
      • StaffActionRequest
      • ManagedSession
      • SessionRevoked
      • AccessRule
      • AccessRuleRequest
      • BehaviourProfile
      • BehaviourProfileRequest
      • BehaviourProfileSaved
      • BehaviourFinding
      • BehaviourFindingAck
      • StepUpProof
      • Removed
      • MemberLockdownRequest
      • McpClient
      • McpDelegatedView
      • McpToolCall
      • McpForwardRequest
      • PasswordlessScope
      • RoleScope
      • PasswordlessSession
      • PasswordlessTlsMaterial
      • PasswordlessKerberosMaterial
      • ServiceDeviceKeys
      • ServiceSession
      • DatabaseRole
      • TransferEndpoint
      • TransferJobRequest
      • TransferQuote
      • TransferStrategy
      • TransferJob
      • TransferJobAccess
      • Pool
      • PoolAllowance
      • BillingSubscription
      • Invoice
      • BillingContact
      • PaymentMethod
      • UpcomingCharges
      • TeamBilling
      • TeamKerberosRealm
      • TeamSsoDomain
      • TeamSsoResponse
      • StoreConnection
      • BatchFailure
      • ProxyStatus
  1. guides

CLI

The package installs a single binary, available under two names:
It covers three things: local project work (scaffolding, model generation
from a live database, migrations, seeders, diagrams), live-database work
(inspecting a schema, catalog objects, maintenance) and cloud/account work
(logging in, pulling credentials, managing databases, buckets, secrets and
teams). All three are served by dist/cli.js.
prorm with no arguments, or --help, prints the full flag reference.

Local project commands#

CommandDoes
init [--directory <path>]Scaffold config/, models/ and migrations/ with a zero-config SQLite setup.
models pull [--out ./models] [--tables a,b] [--style define|class] [--lang ts|js]Generate model files from a live database, any SQL dialect. model:create is an alias.
migration:create -- --name <name>New migration file.
migration:run [-- --name <migration>]Apply pending migrations.
migration:revert [-- --name <m> | --steps <n>]Roll back.
migration:statusWhat has run and what has not.
seeder:create -- --name <name>New seeder file.
db:seed [-- --name <s>] [--env <e>] [--force]Run pending seeders, or one by name.
db:seed:undo [-- --name <s>]Undo one seeder, or the last one run.
db:seed:undo:allUndo every executed seeder, in reverse order.
db:seed:statusWhat has been seeded.
db:create / db:dropCreate or drop the configured database.
prisma:import --schema ./schema.prismaConvert a Prisma schema into prorm models plus an initial migration.
diagram:<type> --file <spec.json>Render one of 15 diagram types — see Diagrams below.
packagesPick the drivers this project uses and install only those.

packages#

Installing prorm brings a driver for every database it can talk to — Oracle,
DB2, DuckDB, Snowflake, Spanner and the rest, several of them native builds. A
project that uses SQLite pays for all of them. This asks what you actually
connect to and produces the one install command that covers it.
Three questions, in order: the SQL databases, then any non-SQL stores, then
security and tooling. Each list is a checkbox picker — arrow keys to move,
space to tick, and typing filters it, which matters at 37 dialects and 105
stores. Then it asks which package manager you use (npm, pnpm, yarn or bun,
detected from your lockfile) and prints the command:
2 packages for what you picked:
  npm install better-sqlite3 pg
Add --run to execute it rather than print it.
It knows which dialects share a driver, so picking TimescaleDB, CockroachDB and
Postgres installs pg once — and picking Turso installs @libsql/client, not
SQLite's driver. Feature-only packages stay out: Postgres needs pg to connect
and pg-copy-streams only to stream a COPY, so the second is offered under
tooling rather than forced on everyone.
Non-interactively, for a Dockerfile or CI:

What the library does not ship#

Around sixty stores load a driver that has never been a dependency — Cassandra,
Couchbase, Kafka, RabbitMQ, the cloud SDKs. Each throws Cannot find module at
its first call. This lists them with the store that needs each one, so the
failure is something you can look up rather than discover in production.

init#

Defaults to SQLite — matching Prorm's own default — and reads DB_DIALECT
plus the other DB_* environment variables to target something else.

models pull (model:create)#

Reads the database through its own catalog — every SQL dialect prorm speaks —
and writes one model file per table plus an index that registers them and
wires the associations its foreign keys imply. Column types map to the exact
DataTypes call that recreates them; a type with no portable equivalent is
kept native, never degraded. Nothing is overwritten without --force;
--dry-run prints instead of writing. See
Models from a database for every flag, the
output, and per-dialect coverage.

Migrations and seeders#

The -- separator matters: flags after it go to the command rather than to
npm/npx. See Migrations for the file format and the
QueryInterface you write against.
Seeders follow the same shape. db:seed runs everything pending; --name runs
one (add --force to re-run one that has already gone), --env scopes to an
environment, and db:seed:undo:all walks the whole list back in reverse.

prisma:import#

Reads a schema.prisma, writes one prorm model per Prisma model and an initial
migration that creates the tables. Importing a Prisma
schema
covers what maps cleanly and what does not.

Diagrams#

Fifteen generators, all reachable as diagram:<type>. Three of them accept a
purpose-built input; the other twelve read a JSON spec matching the generator's
own options object.
CommandInputDraws
diagram:model--file <model.json>, or --db <url> --table <t>One table in detail: columns, types, keys
diagram:er--file <schema.json>, or --db <url>Every model and the foreign keys between them
diagram:migration--file <migration.json>What a migration does to a table
diagram:class--file <spec.json>Models as UML classes
diagram:relational--file <spec.json>A crow's-foot relational diagram
diagram:chen--file <spec.json>ER in Chen notation: diamonds and ovals
diagram:index--file <spec.json>Which indexes cover which columns
diagram:schemadoc--file <spec.json>A data-dictionary sheet
diagram:sequence--file <spec.json>The call order during a write
diagram:state--file <spec.json>The states an instance moves through
diagram:flow--file <spec.json>A read, from finder to instances
diagram:tree--file <spec.json>A nested include, drawn as a tree
diagram:dependency--file <spec.json>Create order, and its reverse for drops
diagram:package--file <spec.json>How the ORM source is organised
diagram:gantt--file <spec.json>A migration rollout over time
Shared flags:
FlagDoes
--file <path>The JSON spec. Required for every type except when --db is given.
--db <url>Introspect a live database instead (diagram:model, diagram:er).
--out <path>Where to write the SVG. Defaults to ./diagrams/<type>.svg.
--title <text>Diagram title.
--theme <light|dark>Defaults to light.
--color <accent>industry (default), industry-2, blue, violet, emerald, amber, pink, sky, indigo, purple, red, teal, green, stone, or a #hex.
--columns <n>Grid width for diagram:er. Defaults to 3.
--description <text>diagram:migration only.
The spec each type expects is the generator's own options object — the same one
you would pass in TypeScript — so Diagrams doubles as
the schema reference, and every example there is generated from real code.

Working against a live database#

The schema, extension/enum/domain/sequence and maintain groups talk
to your database — the one the project is developed against — rather than to
the cloud service. They are the commands you would otherwise open a psql or
mysql prompt for, except they speak all 27 dialects, so the same invocation
works against SQLite locally and PostgreSQL in staging.

Choosing the database#

Every command in these groups accepts the same connection flags, and falls back
to the environment when they are absent:
FlagEnvironmentDefault
--urlDATABASE_URL, DATABASE—
--dialectDB_DIALECT, DIALECTsqlite
--database, -DDB_NAME, DB_DATABASE (DB_STORAGE for SQLite)database.sqlite
--hostDB_HOSTlocalhost
--portDB_PORTthe dialect's default (5432, 3306, 1433, …)
--username, -UDB_USER, DB_USERNAME—
--passwordDB_PASSWORD, DB_PASS—
A URL supplies every part at once, and an individual flag still wins over it —
so --database shop_replica points an otherwise identical DATABASE_URL at a
second database. For SQLite and DuckDB, --database is the file path, and
file:./dev.db works as a URL.
The connection is opened, used, and closed again for each command, so nothing
is left holding a pool.

--dry-run#

Every command that would write accepts --dry-run: it prints the SQL and never
opens a connection.
$ prorm maintain vacuum users --full --dialect postgres --database shop --dry-run
VACUUM (FULL) "users";
The dry run goes through the same builders the live path uses, so what it
prints is what would run. It is the fastest way to see what a statement looks
like on a dialect you do not have in front of you — and what you paste into a
maintenance window when the change has to be applied by someone else.
Because a dry run never reaches the database, it cannot discover anything from
it: maintain routine --dry-run therefore requires --tables rather than
silently printing nothing.

Inspecting a schema#

CommandDoes
schema tables [--filter <substring>]List tables. Aliased to tables.
schema describe <table>Columns, types, nullability, defaults, keys. Aliased to describe.
schema indexes <table>The table's indexes and their columns.
schema views [--materialized]Views, or materialized views.
schema exists <table>Exit 0 if present, 5 if not. Prints nothing.
schema exists is written for scripts — it answers with its exit code, so it
drops straight into a shell conditional:
Because the exit codes are distinct (see Global flags), a
script can tell "no such table" from "could not connect":

Changing a schema#

CommandDoes
schema rename <from> <to>Rename a table.
schema drop <table> [--if-exists] [--cascade]Drop a table (confirms first; --yes skips).
schema index-add <table> <columns> [--name] [--unique] [--using] [--include]Add an index.
schema index-drop <table> <index>Remove an index.
schema comment <table> [text] [--column <c>]Comment on a table or column; omit the text to clear it.
These are for the one-off changes that do not deserve a migration file. Anything
that has to be reproducible on another machine belongs in a migration — see
Migrations.

Catalog objects#

The named objects that live beside tables. Each maps onto the matching
QueryInterface method, so what the CLI can do here,
a migration can do too.
CommandDoes
extension create <name> [--schema] [--version] [--cascade]Install an extension.
extension drop <name> [--if-exists] [--cascade]Remove one.
enum create <name> <values>Create a named enum type from a comma-separated list.
enum add-value <name> <value> [--before | --after]Add a member, leaving the columns using it alone.
enum rename-value <name> <from> <to>Rename one member.
enum drop <name> [--if-exists] [--cascade]Drop the type.
domain create <name> --as <type> [--check] [--not-null] [--default]Create a domain.
domain drop <name> [--if-exists] [--cascade]Drop it.
sequence create <name> [--start] [--increment] [--cache] [--cycle]Create a sequence.
sequence set <name> <value> [--not-called]Set its current value.
sequence restart <name> [value]Restart it.
sequence next <name>Take the next value, consuming it.
sequence drop <name> [--if-exists]Drop it.
sequence set is the one to remember after a bulk load: rows inserted with
explicit ids leave the sequence behind them, and the next ordinary insert fails
on a duplicate key until it is moved forward.

SQL objects#

The views, functions, triggers and other objects a project declares in code
(see Schema objects for the concept) are planned,
checked and applied as one ordered, hashed set. The objects are loaded from a
module given as --entry <module> (a Prorm instance, or a register(prorm)
function) or from $PRORM_ENTRY. prorm_objects is the bookkeeping table
apply keeps in the database, so list and --prune know what prorm
created.
CommandDoes
objects plan [--dialect <d>] [--sql]The ordered, hashed plan for one dialect (defaults to the connection dialect); --sql prints every statement. Never connects.
objects check --dialect <d>|a,b|allEvery refusal and warning for one dialect, several, or every engine family, at once. Never connects; exits 1 when there are errors.
objects apply [--prune] [--dry-run] [--kind-change refuse|rename|drop] [--force]Create and replace objects so the database matches the plan; --prune drops objects prorm created that are no longer defined; --kind-change says what to do when a model changed from a table to a view (refuse is the default; drop needs --force, the rows are lost). Uses the connection flags.
objects list [--owner <o>]The objects prorm created in this database, from prorm_objects; --owner is app, model:<Model>, a plugin or a pack.

Extensions#

What a database has, what it offers, what a managed service allows — and
reproducing that set elsewhere. See Extensions.
CommandDoes
extensions list [--status] [--catalog]Every extension marked installed, available, not-allowed (the service's allow-list excludes it) or unknown.
extensions import <name…> [--schema] [--version] [--cascade] [--soname] [--dry-run]Install: CREATE EXTENSION IF NOT EXISTS on the PostgreSQL family, INSTALL/LOAD on DuckDB, INSTALL PLUGIN/COMPONENT on MySQL/MariaDB, the driver's loadExtension on SQLite. Refused on engines without extensions.
extensions import --from <manifest> [--source] [--latest]Replay a manifest written by extensions export.
extensions drop <name…> [--if-exists] [--cascade] [--dry-run]Remove: DROP EXTENSION on the PostgreSQL family, UNINSTALL PLUGIN/COMPONENT on MySQL/MariaDB. Refused on DuckDB and SQLite, which have no statement that removes one.
extensions export [--out] [--key]Write name and version per database to prorm.extensions.json.
On RDS/Aurora, Azure and Cloud SQL, an extension the service does not allow is
refused before anything is sent, with the aws, az or gcloud command
(and console path) that allows it.

Local databases in containers#

db up runs a local database in Docker or Podman with a pinned image, generated
credentials in a git-ignored .env, and the requested extensions installed
once it is healthy. See Local databases.
CommandDoes
db up [--dialect] [--ext] [--name] [--port] [--version] [--image]Write docker/local-db/<name>/docker-compose.yml (plus a Dockerfile when an extension needs one) and .env, start it, install extensions. --no-start, --dry-run, --print-run.
db down [name] [--volumes]Stop it; --volumes deletes the data.
db status [name]Engine, image, port, health and redacted URL.
db url [name] [--show-secret]The connection URL, password masked unless --show-secret.

Maintenance#

CommandDoes
maintain analyze [table] [--columns]Refresh the planner's statistics. Aliased to analyze.
maintain vacuum [table] [--full] [--analyze] [--into]Reclaim storage. Aliased to vacuum.
maintain reindex <target> [--scope] [--concurrently]Rebuild indexes. Aliased to reindex.
maintain routine [--tables]Vacuum then analyze every table.
maintain optimize <table>Rebuild and defragment a table (MySQL family).
maintain check <table> [--level]Check for corruption; prints what the engine reports.
maintain repair <table> [--quick] [--extended]In-place repair (MySQL family).
maintain checksum <table>Checksum, for comparing two copies.
maintain cluster <table> [--index]Rewrite in an index's physical order (PostgreSQL).
analyze is the one worth running on a schedule: a planner working from stale
statistics chooses bad plans, and that is the usual reason a query that was fast
last month is not. vacuum --full is the opposite — on PostgreSQL it takes an
ACCESS EXCLUSIVE lock, so the table cannot even be read while it runs. --dry-run
first.
Commands that report rather than act (check, repair, checksum) pass the
engine's own output through instead of normalising it into columns only one
database family would fill in. Add --json when a script has to read it.

The security toolkit#

prorm audit scans a project for patterns that weaken security, probes a
database read-only, evaluates the checklists (SOC 2, ISO/IEC 27001, the HIPAA
Security Rule, PCI DSS, GDPR), records attestations and writes a verifiable
evidence bundle. It needs no sign-in. Its reports record technical evidence
mapped to control references; they are not an audit or an attestation, and
they do not determine compliance. See The security toolkit
and Evidence bundles.
CommandDoes
audit code [path]Static rules (SQL-001 … CFG-002) over TS/JS and committed .env/.klock files; --format table|json|sarif, --fail-on, --write-baseline --reason
audit checklist [framework]The checklists, or one checklist's items; --item <id> for one item and its crosswalk
audit attest <item-id>Record an attestation in .prorm/attestations/ (prompts, or --by --role --statement --expires --evidence)
audit dbRead-only probes: TLS in use, role privileges, password hashing, RLS (--rls-tables), pgaudit, grants
audit klock [path]The klock checks (KLOCK-001 … KLOCK-008, and SEC-003 for .klock files) over the project, your own klock files (~/.prorm/klock/*.klock and the configured one, unless --no-home) and, signed in, the account probes; --offline, --report-days N, --fail-on
audit run [path]Everything, evaluated per checklist, into prorm-evidence-<slug>-<UTC>/; --db, --account (the klock account probes, signed in), --report-days, --sign-key
audit verify <bundle>Recompute every hash and check the ed25519 signature
audit export <bundle>--format evidence-json|evidence-csv for a GRC tool
audit logThe account audit log. Bare audit still shows it for one minor release, with a deprecation note
Exit codes: 6 when an unsuppressed finding is at or above --fail-on
(default high), 7 when nothing failed but some evidence could not be
gathered.
audit klock asks the public /.well-known/klock-configuration whether the
API offers klock v2 (a 404 turns KLOCK-003, the v1-secret check, off), and
its account probes are read-only GETs (/me/capabilities,
/security/devices, /security/behaviour for admins, /security/access-rules).
A probe the API cannot answer is unknown, never a pass, and makes the exit
inconclusive when no finding failed. audit run --account refuses to run
signed out, and adds evidence/klock-account.json to the bundle. The checks
are listed in The security toolkit.

Audit log levels and escalation#

prorm logs reads the ORM's own audit table (AuditLogs by default) over the
usual connection flags, and shows the level ladders and escalation presets
without a connection. See Audit levels and escalation
for what the columns mean.
CommandDoes
logs levels [--ladder <name>]Print a level ladder: names, ranks, syslog severities, aliases. Ladders: default, syslog, itil, sre and any you registered.
logs presets [name] [--keyword <k>]List the escalation presets, filtered by keyword, or print one as JSON to start your own policy from.
logs tail [--level <filter>]The most recent audit entries. --level warning is that level only, warning+ that level and above, info- that level and below, error..alert a range, a,b a list.
logs escalations [--unacked] [--level <min>]Escalated entries; with --unacked, open incidents grouped by key that nobody has acknowledged or resolved.
logs tail and logs escalations share --table (default AuditLogs),
--ladder, --limit (default 50) and --since (a duration: 24h, 7d).

Platform commands#

The second half of the CLI works on your account and its databases through
the control plane at api.prod-orm.io — the same API the console uses. Sign
in once; every command after that speaks as you.
GroupCommands
Accountlogin, logout, whoami, status, step-up, auth token, account, account update, account emails, family, family child
Databasesdb init, db serve, db up, db down, db status, db url, db list, db show, db create, db delete, db use, db update, db connect, db access, db env, db query, db tables, db describe, db push, db logins, db logins create, db logins rotate, db logins drop, db objects, db migrations, db migrate, db pause, db resume, db backup, db backups, db restore, db resize, db move, db replicas, db rotate-credentials, db ops, db events, db metrics, db usage, db open, engines, regions, stores, stores create, stores connect, stores delete
Keyskeys list, keys create, keys revoke, keys trace
Rolesroles list, roles create, roles show, roles update, roles delete, roles grant
Passwordlesspasswordless issue, passwordless list, passwordless revoke, passwordless query, passwordless check, klock show
Transfertransfer strategies, transfer quote, transfer create, transfer start, transfer run, transfer agent, transfer list, transfer show, transfer cancel
Base44base44 generate, base44 sync
Policiespolicies catalog, policies show, policies assigned, policies assign, policies unassign, policies check, policies review, policies firings
Poolspools list, pools create, pools update, pools close, pools buy
Securitysecurity sessions (also sessions list), security revoke (also sessions revoke), sessions show, sessions rotate, sessions freeze, sessions unfreeze, devices list, devices show, devices rename, devices revoke, devices suspend, devices reinstate, devices wipe, devices pair (also devices approve), devices rotate-key, access list, access show, access allow, access block, access add, access rm, access test, behaviour list, behaviour presets, behaviour show, behaviour apply, behaviour set, behaviour rm, behaviour test, behaviour findings, lockdown, lockdown release, security audit (also audit log), security mfa enable, security mfa disable
Klockklock status, klock verify, klock sync, klock rotate, klock upgrade, klock explain, klock cp, klock inbox, klock accept, klock refuse, klock receipt, klock init, klock keygen, klock agent, klock certs list (also klock certs), klock certs request, klock certs renew, klock certs revoke, klock kerberos status (also klock kerberos), klock kerberos issue, klock kerberos rotate, klock kerberos revoke (and klock show, under Passwordless)
Teamsteams list, teams create, teams use, teams members, teams invite, teams role, teams remove, teams accept, teams leave, teams delete, teams roles, teams projects, teams attach
Billingbilling plans, billing estimate, billing quote, billing subscription, billing invoices, billing checkout, billing portal
Configconfig get, config set, config list, config unset, config path
Systemping, open, version, upgrade, completion (and __complete, which its scripts call), mcp, mcp clients list, mcp clients create, mcp clients revoke
Scaffoldnew, add
The live-database groups above (Schema, Catalog, Maintenance) are
registered the same way and appear alongside these in --help — but they
talk to a database you reach yourself and need no sign-in.

Signing in#

No password is typed in the terminal. The CLI listens on 127.0.0.1 on a
port the OS picks, opens the console in your browser with that port and a
one-time code, and waits. The console — where you already are, or where you
sign in — asks whether this CLI on this machine may act as you, and hands the
answer to the listener (or, when the browser will not talk to loopback,
leaves it with the API for the CLI to collect once).
klock v2 (the default when the API offers it). Before opening the
browser the CLI makes this device's key pair — an Ed25519 key that signs
every request and an X25519 key that receives sealed secrets — and keeps it
in the OS keychain (macOS Keychain, libsecret) or, without one, in the 0600
klock file with a warning. It prints a confirm code:
Confirm code: K7QM2D   (the console shows the same code — approve only if they match)
The console shows the same six characters and the machine's name; approve
only when they match. The console then opens a device-bound interactive
session
and hands back its grant: an API-signed statement of your
capabilities, limits and check-in deadline that names this device's key, so
it is useless on any other machine. The CLI verifies it against the pinned
keys, writes it to ~/.prorm/klock/<profile>.klock (0600, atomically), and
proves it with a signed GET /me. From then on every request is signed by
the device key (RFC 9421) instead of carrying a token.
The legacy flow — a thirty-day token for this machine, labelled
prorm-cli (<host>) — is used when the API does not offer klock v2 (its
/.well-known/klock-configuration answers 404, or 503
klock_v2_unavailable), with --legacy, and with --token/PRORM_TOKEN.
After one v2 sign-in the CLI refuses to fall back silently: a later
prorm login against an API that suddenly says "no v2" stops and asks for
--legacy. The legacy token is kept in the OS keychain where there is one,
and it reaches the keychain over stdin (security -i on macOS,
secret-tool store on Linux), never as a command-line argument that ps
could show. PRORM_NO_KEYCHAIN=1 keeps it (and the device key) in the 0600
files instead.
A whole sign-in, from a terminal with a browser beside it:
$ prorm login
Confirm code: K7QM2D   (the console shows the same code — approve only if they match)
Sign in in the browser to connect this CLI:
  https://app.prod-orm.io/#/cli?port=53117&state=…&host=ada-mbp&v=2&dpk=…&spk=…&sb=…&cc=K7QM2D
Waiting for the browser…
✓ Signed in as ada (ada@example.org) on this device (dev_01J9Z3K4M5N6P7Q8R9S0T1V2).
klock v2 interactive session · key in the macOS Keychain · ~/.prorm/klock/default.klock
Check in by 9/27/2026, 9:14:02 AM; the session ends 10/26/2026, 9:14:02 AM.
$ prorm whoami
ada <ada@example.org>
  tier        user
  session     interactive · device-bound · dev_01J9Z3K4M5N6P7Q8R9S0T1V2 · key in the OS keychain
  check in    by 9/27/2026, 9:14:02 AM (in 24h)
  expires     10/26/2026, 9:14:02 AM
  klock       active · ~/.prorm/klock/default.klock
  41 capabilities (grant); --caps lists them
API https://api.prod-orm.io
The times are printed in your locale. klock reads check-in-due once the
deadline has passed (the next command syncs), and expired or idle when the
session has to be signed in again.
What sign-in leaves on disk:
~/.prorm/
  config.json            # API URL, default database, team, klock path; the legacy token when no keychain
  config.<profile>.json  # one per --profile
  klock/
    default.klock        # the v2 session klock: device, grant, check-in (0600)
    <profile>.klock
  cache/
    mcp-docs.json        # prorm mcp's documentation-tool listing (24 h)
The device's private keys are in the OS keychain under the service
prorm-klock when there is one, and in the klock file otherwise.
For a machine with no browser — CI, a server — mint a token under Security →
Sessions → A token for a machine with no browser, and set it as
PRORM_TOKEN (or prorm login --token).
[!WARNING]
A PRORM_TOKEN is a bearer token: whoever holds it acts as you until it
expires or is revoked. Keep it in the CI system's secret store, never in
the repository, and revoke it (prorm sessions revoke <id>) when the
pipeline no longer needs it.
prorm whoami prints the tier (guest, user, team, admin), the team
role in the current team context, the session's kind and binding, its
check-in deadline (checkInBy: after it, only the public commands work
offline until a sync) and expiry, and the klock's state. Admin capabilities
of an interactive session are dormant until a step-up: prorm step-up
asks for an authenticator code (or --totp), and any command that meets
step_up_required offers the same prompt on a terminal and retries once.

Login v2, step by step#

What prorm login does, in order (src/cli/basic/commands/auth.ts):
1.
Pick the path. --token <t> (or --token - to be prompted), or
PRORM_TOKEN in the environment with none of --guest, --device,
--pair or --legacy, stores that legacy token once GET /me accepts
it: no browser. --pair goes straight to pairing (below). With
--no-input (or $CI) anything else stops with prorm login` needs a browser. In a non-interactive environment pass --token. (exit 2),
except --guest and --device --totp <code>. --legacy runs the legacy
browser flow. Two mode flags at once is a usage error.
2.
Ask the API whether it offers v2: GET /.well-known/klock-configuration
at the API host's root. A 404, a 503, or a body without "v2": true means
no. Then --guest and --device say the feature is coming soon (exit 1).
A CLI that has signed in with v2 before (klockV2Seen in its config)
refuses: klock v2 is not available on <api> (…), and this CLI signed in with v2 before. Refusing to fall back silently: run `prorm login --legacy` if you mean it. (exit 3). Any other CLI says why and runs the legacy
flow.
3.
The device key. If the profile's session klock
(~/.prorm/klock/<profile>.klock, or beside --config/$PRORM_CONFIG)
already holds a device whose key loads and whose integrity tag matches,
that key is reused. Otherwise the CLI makes an Ed25519 signing key and an
X25519 sealing key (the seal key signed by the signing key), stored under
the keychain service prorm-klock as pending:<thumbprint> until the
device has an id, or in the 0600 file with a warning.
4.
The confirm code. A random 24-byte state, and the first six
characters of base32(sha256(state ‖ thumbprint)). The CLI prints
Confirm code: K7QM2D (the console shows the same code — approve only if they match).
5.
The listener and the console. A listener on 127.0.0.1, on a port the
OS picks, answering only POST /callback, with CORS for the console's
origin only. The browser opens
<console>/#/cli?port=…&state=…&host=<hostname>&v=2&dpk=<signPub>&spk=<sealPub>&sb=<sealBinding>&cc=<code>,
plus &kind=device with --device and &team=<t> with --team.
--no-browser only prints the link.
6.
Approval in the console. The console shows the host name and the code
and asks whether your terminal shows it. It then calls
POST /security/klock/sessions. The API recomputes the code from state
and the device key and refuses a mismatch (confirm_code_mismatch, which
is audited). A --device session also needs a fresh step-up.
7.
The hand-over. The console POSTs {state, grant, sessionId, deviceId, deviceCert, keyset, v: 2} to the listener. When the browser cannot reach
loopback, it parks the same thing with the API and the CLI collects it
(GET /security/cli-logins/<state>, polled every 2 seconds). The state is
compared in constant time, and a wrong one gets 409. After five minutes
the CLI gives up (exit 1). A parked grant is bound to this device's key,
so nobody else who collected it could use it. If the console hands over a
legacy token instead, a CLI with klockV2Seen refuses it
(klock_downgrade).
8.
Verification, and the file. The CLI loads the keyset: the one handed
over, if the pinned root keys sign it and its seq is not below the cached
one, and otherwise /.well-known/klock-keys.json. It then checks the grant:
its kid, its Ed25519 signature, and that it names this device key's
thumbprint, this session and this device. It also checks that its iss is
the API you signed in to; another origin is refused unless you passed
--api-url. The keychain entries move to the device id, and the klock is
written atomically with mode 0600: version: 2, kind, api, klock,
device, and session (id, grant, grantSeq, syncSeq, syncedAt
and checkInBy). The config records the klock's path and, except for a
guest, klockV2Seen: true. It forgets any legacy token.
9.
Proof. A device-signed GET /me. The CLI prints who you are, the
device id, where the key is kept, and when to check in by.
The variants:
--device on a machine already signed in with v2 skips the browser. It
opens the device session with a signed POST /security/klock/sessions,
asking for an authenticator code when the API wants a step-up (or taking
--totp), then signs out the interactive session it replaces.
--guest is refused on a CLI signed in to an account (prorm logout
first; exit 2). Otherwise it sends a device-signed
POST /security/klock/guest and stores the guest grant.
--pair starts a pairing on the klock service (POST /v2/pair/start,
signed by the new key, 5 per address an hour). It prints a code such as
KXQ7-M2PD, a prorm://pair?… QR link and a console link, for a
signed-in device (prorm devices approve <code>) or the console
(#/pair). It polls GET /v2/pair/<id> until the pairing is approved or
expires (ten minutes), then installs the grant exactly as step 8 does, and
exits 3 if it is not approved. The code is never stored by the service,
only its hash.
The confirm code guards against a crafted sign-in link. If someone sends you
the link their own prorm login printed and you approve it, their machine
gets a session as you. The code is the check: it belongs to their
terminal, so it does not match anything in yours. The risk that remains is
social engineering, so approve only a code you can see in your own terminal.

Working on a database#

Every database command takes --db <id|uuid|name>; set a default once and
stop passing it:
Queries are described, never SQL: select, insert, update, delete
as JSON, compiled by the ORM for whatever engine the database is. That is what
lets an access role check "read on users, columns id and email" before a
connection is used.
Anything that takes longer than a request — backup, restore, resize, move,
migrate, rotate — is queued as an operation. The command prints its id;
--wait follows it to the end; prorm db ops --op <id> --wait follows it
later; --cancel and --retry do what they say.

Managed engines & data stores#

prorm engines lists every engine and whether this deployment can build a
managed one, and where. Beyond Cloudflare D1 (sqlite) and the postgres
tiers, the platform provisions managed services in Google Cloud:
EngineBuilt onReached by
postgres, mysqlCloud SQLa connection string (db connect)
bigqueryBigQuery datasetHTTPS, with an api key
redisMemorystorea private VPC endpoint
firestoreFirestoreHTTPS, with an api key
gcsCloud Storage bucketHTTPS, with an api key
A managed engine is built in the background — the resource is provisioning
until it exists — so --wait is worth passing:
A store type the platform does not build managed is still recorded, so you can
keep the connection details for one you run yourself.

The developer loop#

Three commands connect a project on your machine to a database on the platform.
db push runs each migration file's up() once against a recorder that
stands in for the QueryInterface, writes every call down as a described
operation — createTable, addColumn, addIndex and the rest — and sends
that. Column types travel as the ORM's own (STRING(120), BOOLEAN), and the
API renders them for whichever engine the database is, so sqlite-developed
migrations push to a postgres database unchanged. A file that reaches for
raw SQL or bulkInsert is refused by name before anything is sent, because a
plan cannot carry it. Pushed twice is a no-op. db events --follow tails the
feed while an operation runs.

Installing#

prorm-cli is the CLI on its own: one file, no database drivers.
$ npm i -g prorm-cli
$ prorm login
$ pnpm add -g prorm-cli
$ prorm login
$ yarn global add prorm-cli
$ prorm login
$ bun add -g prorm-cli
$ prorm login
Or run it without installing: npx prorm-cli login.
The project commands — schema, catalog, maintain, db push, migrations,
scaffolding, diagrams — need the ORM and a driver, so the standalone prorm
hands them to the ts-prorm-orm installed in the current project and says
how to install it when there is none. Inside a project that has the ORM, the
two are one command.

Credentials, and doing without them#

Three kinds of thing can reach a database, and the CLI issues all three.
An api key is the credential a program holds: one database, one
privilege, optionally bound to a role.
An access role is a named scope — ops × tables × columns — enforced at
the data plane on every request, with no GRANT on the engine:
A passwordless grant is a role handed to somebody as a .klock — a file
holding a session secret and nothing that could open the database. The holder
signs a 30-second grant for each request; the API checks it against the role
as it stands and runs the query in its own pool.
The .klock is the same file the outh Go tool writes and reads, and the
grant is the same signature, so the two interoperate. The file format, the
signatures and what a copied file can do are in klock; every
error you may see, with its fix, is in the klock error reference.

Connection pools#

The data plane never hands a client a connection; it holds them and runs each
request through a pool — a bounded, long-lived set of them. A client sticks
to one pool (its id is remembered on the key or session), so it uses one
optimised pool rather than fanning out. When a pool fills, another is opened
if the account's allowance permits and the client is moved onto it; over the
allowance, a request is refused with a clear "buy another pool" rather than
queued for ever. The count of in-flight requests is held in Redis, so the
ceiling is true across every instance of the API at once (set REDIS_REST_URL
and REDIS_REST_TOKEN; without them each instance counts on its own and the
listing says so).
A pool has one credential, not rotating keys — it is connections to the same
database as the same engine login. What rotates is the client's credential
(an api key, a passwordless grant), and that is independent of which pool
serves it; the data-plane response carries the pool id it used, so a client
library can hold it.

Base44 datastores#

Declare an entity once — as an ordinary prod-orm model — and take it to a
Base44 app. Base44 keeps entity schemas as
base44/entities/{kebab-case}.jsonc files that npx base44 entities push
deploys, so base44 generate writes exactly those files from your models; the
auto-increment primary key and the ORM timestamps drop out, because Base44 owns
id, created_date and updated_date. base44 sync writes them, shows what
changed against the files already on disk, and then runs the push for you (drop
--plan to apply). It never touches an entity file your code does not define.
base44 entities push is a full-folder sync: it replaces the app's entity
set with what is in base44/entities/, so any entity on the app that is not a
file there — including Base44's built-in User and anything made in the
dashboard — is deleted. sync warns about this (and points out a missing
user entity specifically) and asks before pushing; pass --yes/-y to
confirm non-interactively. Keep entities you rely on as files (add a
user.jsonc if you use Base44 auth).
The push authenticates through Base44's own CLI (device-code login). In code the
same thing is Base44.toBase44Entity(Model) writing through
Base44.fileBase44Client('base44/entities'), or the full
Base44.Base44Connector for the plan/apply loop; Base44.httpBase44Client
(app id + API key) is the hosted path for entity records.

Policies#

The account#

Sessions, devices, access rules, behaviour, lockdown (klock v2)#

Every list and show command takes --json; a list prints an array, a show
an object. These commands call the API's /security/sessions,
/security/devices, /security/access-rules, /security/behaviour and
/security/lockdown routes. Against a server that does not offer one (a
501, 405, a bare 404, or 503 klock_unconfigured when the API has no
klock service bound) the command says the feature is coming soon, changes
nothing, and exits 1.
sessions, sessions list and sessions revoke are the same commands as
security sessions and security revoke. Only a session can rotate its own
key, so sessions rotate takes current (service sessions rotate through
prorm passwordless).
Your own devices need device:read to list and device:manage to
change. Someone else's device answers 404, the same as one that does not
exist.
--team <t> lists or revokes a team member's device, and needs a team
role with device:manage-members (owner, admin, security). Revoking
another person's device needs --reason; the member is emailed, and the
action is audited with you as the actor. Only the team owner may act on the
owner's devices.
revoke ends the device's sessions first, then revokes the device and
its credentials, all onto the revocation feed. suspend stops it
getting a grant at sync and freezes its sessions; reinstate undoes
both. wipe ends its sessions, suspends it and queues an API-signed
wipe command for its next sync: best effort, and only klock material is
removed. A revoked or wiped device answers 409 to a wipe.
Pairing. devices pair without a code starts pairing on the new device,
like prorm login --pair. With a code it approves: --kind device (the
default) or interactive, which must match what the new device asked for
(409 pairing_kind_mismatch). Approving a device needs a fresh step-up
(prorm step-up first). A code works once (409 pairing_used), for ten
minutes (410 pairing_expired), and an unknown code is
404 pairing_not_found. Approvals are limited to 10 an hour.
Access rules allow or block by ip, country, asn, device,
cert, principal, time, client or api-key, on the account (default),
team:<t>, db:<d>, store:<s> or device:<id>. Block beats allow; the
most specific allowlist wins, except team (mandatory) allowlists, which
always apply. Rules are validated locally with the same engine the API uses,
and a rule that would lock you out of your own account is refused before
it is sent
(the API checks again, 409 would_lock_you_out); --force
overrides with a step-up. A new block or account allow starts in report mode
for 24 hours unless --enforce-now (--report-only asks for report mode
explicitly). --mandatory makes an allowlist one that narrower scopes cannot
widen; it is on by default for team rules. --until takes a duration
(24h, 7d) or a date. access test exits 1 when the answer is "denied".
(prorm db access is the database's network firewall, a different layer.)
The full rules of precedence are in
Access rules and behaviour.
Behaviour profiles (alias behavior) describe what normal looks like —
places, hours, devices, clients, rates — and what to do when it is not: log,
alert, step-up, rate-limit, freeze, revoke. Profiles are checked against the
profile schema before they are sent; new ones start in report mode
(--mode enforce to enforce at once). Besides the access-rule scopes, a
profile may be scoped to session:<id> or service:<id>.
Lockdown is the button after a scare: every browser, CLI, interactive,
device, delegated, handoff and assumed session is revoked (this CLI's too;
service sessions only with --include-services), the klock service is asked
to suspend every device, and for 15 minutes every request of the account is
refused with 423 lockdown, even from sessions signed in afterwards, except
a recovery session. It asks you to type LOCKDOWN (or --yes) and a
step-up. lockdown release needs a sign-in from the last 15 minutes plus a
TOTP code; devices stay suspended until prorm devices reinstate. Locked out
by your own rules or a lockdown? Recovery is in the console (#/recover):
an emailed link, then your password and second factor give a one-hour
recovery session. See
Access rules and behaviour.

This device's klock#

When a klock command (or login, whoami, sessions, devices) fails
with a klock error, it ends with the catalog's steps to fix it and a link;
prorm klock explain prints the whole entry. klock status exits 5 when
there is no klock at the path, and klock verify exits 3 when the file
does not verify.
klock upgrade never upgrades silently: it rewrites a version 1 file as
version 2 atomically, keeping every field (the v1 secret too, so outh and
older clients keep working), gives it a device key, and asks the API to bind
the passwordless session to that key. --drop-secret removes the secret
once the session is bound; --no-bind only rewrites the file. When the API
cannot bind yet, the file is still version 2 and keeps working over the v1
secret, and the command says so. The bind is
POST /provision/databases/:id/passwordless/:sid/bind, which the database's
owner (access:write) may call; binding makes the passwordless session a
service session held by this key (see
klock).
A local transfer job can use a device-signed agent too:
prorm transfer create … --local --klock-v2 gives the on-device agent its own
device key and a service session over the job, writes
~/.prorm/transfer-agent/<job>.klock, and prorm transfer agent then signs
every status report whole instead of using the v1 MAC.
File transfer between your devices (or a team member's, or the support
drop) is end to end: the file key is sealed to the recipient's device key,
which the sender checks against the API's signature; the klock service only
ever holds ciphertext, for at most 24 hours.
klock accept shows the sender, their device and every file first, then
writes only inside a sandbox — ~/Downloads/prorm-klock/<id>/ by default, or
--into. It refuses absolute paths, .., names over 255 bytes, symlinks
anywhere on the way, anything but regular files, and totals over
--max-bytes (2 GiB by default); it never replaces an existing file without
--force (and never a symlink or special file even then), checks every
file's SHA-256 before it is renamed into place, and drops the execute bit
unless --keep-mode.
klock inbox --sent lists what this device sent, with each transfer's
status. klock accept --json includes receipt, the relay's signed receipt.
klock refuse <id> declines an offer (the ciphertext is deleted and the
sender's receipt says refused), and klock receipt <id> shows a transfer's
status and, once it has ended, the klock service's receipt, verified against
the keyset's klock key: completed, refused, cancelled or expired, the bytes
and chunks, and the manifest and ciphertext digests. klock cp takes
--reason (required for support:) and --max-bytes. Who may send what,
the limits and the wire are in klock.
While the klock service has no relay bound, the transfer routes answer
503 klock_unconfigured and these commands say file transfer is coming soon,
send or write nothing, and exit 1.

A database host's own klock#

A host that runs queries for a passwordless session keeps its own service
klock, bound to a key made on the host. The owner issues an enrolment ticket
(console, POST …/passwordless/enrol, or the MCP tool
prorm_issue_passwordless_binding); the host redeems it:

Make the key (optional)#

$ prorm klock keygen
✓ Created device key for ./.klock.
  thumbprint  …
  public key  …
  seal key    …
klock keygen [file] [--rotate] writes a v2 kind: "service" klock holding
only the pending key (the file defaults to ./.klock, or $OUTH_KLOCK).
Running it again shows the same key. --rotate replaces it only before
enrolment
; an enrolled key is refused (exit 2) with a pointer to
prorm klock rotate. A version 1 file is refused, with a pointer to
prorm klock upgrade. --json adds sealBinding and
action: created|rotated|existing.

Redeem the ticket#

$ prorm klock init --service --ticket "$ENROL_TICKET"
klock init --service [--ticket <t>] [--database <id|uuid>] [--force] [file]
calls POST /provision/databases/:id/passwordless/redeem with the ticket and
the key's public halves. No account token is sent: the ticket is the
owner's approval. The ticket can also come from $PRORM_ENROL_TICKET, and
--database defaults to the one the ticket names. It writes a v2 service
klock with the enrolled device, the service session and its grant, and no
secret
, then checks the grant against the pinned keys. Without a ticket
it writes the pending key only, and a key from klock keygen is reused. An
existing bound klock is refused without --force (exit 2); a refused
ticket (ticket_invalid, ticket_expired, ticket_used, …) leaves no bound
file behind.

Keep it checked in#

$ prorm klock agent ./.klock
klock agent [file] [--once] [--no-channel] is the long-running daemon. The
klock it runs for is the argument, else $OUTH_KLOCK, else this profile's
session klock, else ./.klock; it must be a version 2 file with an enrolled
device, or the agent exits 3. It syncs on the server's nextSyncAfter
(clamped to 30 s–1 h, always before checkInBy minus 10 minutes), at once on
a push-channel nudge, and backs off 1, 2, 4 … minutes up to an hour after a
failure. When checkInBy is within 10 minutes it checks in
(POST …/security/klock/sessions/:id/refresh), renews provider credentials
that are due, and applies signed commands: logout, wipe and a revocation
stop it with exit 3. It logs one line per event to stderr
([klock agent] <time> …), and SIGTERM or SIGINT stop it cleanly (exit 0).
--once runs one round and exits 0 (all succeeded), 1 (something failed)
or 3 (the session or device is gone), with the round's report under
--json: that suits cron, systemd timers and CI. --no-channel syncs on
schedule only. A systemd unit needs no more than
ExecStart=prorm klock agent /srv/app/.klock and Restart=on-failure.
klock upgrade now stores the bind's answer in the file too: the service
session id, its grant, grantSeq and checkInBy (so klock agent can keep
it checked in), api when the file had none, and the keyset beside it (so
klock status verifies the grant offline). --json gains sessionId and
protocol (v1+v2 or v2). --drop-secret binds with protocol v2 first,
so the server refuses the v1 secret from then on, and only then removes the
secret from the file; on a file already upgraded it binds again with the same
key (200 rebound). If the server cannot take the session as v2 it refuses
(exit 2) and keeps the secret. It cannot be combined with --no-bind.

Certificates and Kerberos for this device#

klock certs list [--file], request [--file],
renew [id] [--if-due] [--file] and revoke [id] [--reason] [--file]
manage short-lived TLS client certificates (12 hours for a person's device,
24 for a service session). The private key is made on this device and
never leaves it: it is kept in the OS keychain (prorm-klock,
tls:<deviceId>), or in the 0600 klock for a device whose key store is
file. renew and revoke default to the installed certificate. They
need klock:sync.
klock kerberos status [--file], issue [--file],
rotate [--file] and revoke <id> [--reason] [--file] manage this
device's Kerberos principal. The keytab stays sealed to the device, and
outh spnego opens it when a ticket is needed. They need
kerberos:manage.
--file points at another klock, for example a host's ./.klock;
otherwise it is this profile's session klock. The klock must hold an
enrolled device key and a session that can sign.
The providers, lifetimes and plans are in klock.

MCP clients#

A delegated token holds only the capabilities named, and lives 1 to 30 days
(--days, default 30). It never holds an admin capability, or
session:manage, device:manage, access-rule:*, behaviour:*,
billing:manage, team:manage-members, device:manage-members,
session:manage-members or mcp:clients (the delegated kind mask); the
CLI refuses those (and *) before it asks the API. The token is shown once. How the
remote MCP server uses it is in MCP.
Plain prorm mcp runs the local MCP server on a narrowed child token holding
your session's non-destructive capabilities (renewed every 20 hours,
revoked when it stops). When the API cannot mint one, it narrows your session
locally to the same set and says so on stderr. prorm mcp --full runs on
your whole session, as before, and destructive tools then ask for a
confirmation token first; --full with --caps is a usage error. Details in
MCP.
prorm mcp --caps a,b [--hours 1–24] runs the local MCP server on a
narrowed child: it mints a delegated token holding only those capabilities
(one day, client name prorm mcp (<host>)), serves every call through it,
stops serving account tools after --hours (default 12), and revokes the
token when the server stops. It never falls back to your own session, and
with an MCP client token as the parent it refuses:
prorm mcp: --caps needs your own session, not an MCP client token. Not started.
The capability list is checked against the same delegated mask as
mcp clients create, which also refuses device:manage-members,
session:manage-members, access-rule:team and behaviour:team. Details
in MCP.

Staff commands: prorm admin#

The staff commands are a namespace of prorm: prorm admin <command>. They
used to be a separate binary, prorm-admin; a namespace rather than bare
names because teams, team and me already mean something in prorm.
They are secret. To anyone who is not an admin they do not exist: they are
left out of --help, "did you mean" suggestions and completion, and
prorm admin users answers exactly as a command that was never registered
would (prorm help admin … too). An admin sees them in --help, grouped
under Admin: …, and prorm admin alone lists the ones they may use.
Every command needs an admin capability, admin:<scope> — one per admin
scope (admin:users, admin:billing, admin:email, …; the table is
src/cli/core/command-needs.ts) — and the API enforces it on every request,
not the admin role alone: a billing admin asking for admin users is refused
with the name of the scope it is missing; admin me lists the ones it holds.
Granting, changing and revoking need grant-admin, which only the general
preset carries, and the last general admin cannot be revoked. With a klock v2
session the admin capabilities are dormant until a step-up: the first staff
command asks for an authenticator code (or run prorm step-up first).

Acting as a customer#

With the assume scope (the support and general presets carry it), an
admin can take on a customer's identity for a while — to see what they see,
and, in write mode, to fix what their own admin scopes cover:
assume hands back a token that is the customer's. Give it to prorm for
one command with PRORM_TOKEN, or with --save as a profile:
The ceiling is fixed by the API, not the CLI. Read mode changes nothing. Write
mode may change the billing area only if the admin holds billing,
provisioning only with resources, the profile, teams and family only with
users — never more than the admin could touch anyway, narrowed to one
account. The identity itself (sessions, MFA, password) is never writable, and
nothing under /admin answers to an assumed token, so assuming an admin
account (which needs grant-admin) borrows none of its powers. The customer
sees the session under Security → Sessions, with who and why, and can end it;
every write is audited against the admin.

The old binaries: prorm-admin and prorm-team#

Both still ship, for one release, as aliases (they are removed in 4.10). Each
prints one line to stderr and runs prorm:
OldNowNotice
prorm-admin usersprorm admin users (every staff command gains admin; login, logout, whoami, status, ping and version pass through, and prorm-admin login still opens the staff console)prorm-admin is now `prorm admin`; this alias is removed in 4.10
prorm-team teams membersprorm teams members (argv unchanged: its commands were always teams …)prorm-team is now `prorm` (its commands are `prorm teams …`); this alias is removed in 4.10
In the standalone package they are 3-line shims that set PRORM_ALIAS and
load bin/prorm.js. prorm-admin can is prorm admin can for now; prorm whoami --caps is its replacement.

What you may run: capabilities and the guard#

Every command declares what it needs, as a capability id from
src/capabilities.ts (the map is src/cli/core/command-needs.ts). Local
commands — the project and live-database groups, scaffolding, config,
sign-in itself, the legacy init/migration:*/db:*/diagram:* — need
local:use, which everyone has: they never ask you to sign in and never wait
on the network. The platform commands need an account capability
(resource:read, key:write, team:manage-members, …), and before one runs
the CLI checks its view of what you hold — the klock v2 grant, verified on the
device; for an older token, /me/capabilities, cached for an hour; signed
out, the guest set:
The view saysWhat happens
allowedIt runs.
sign in (you are signed out)Sign in to use `prorm db list`: prorm login, exit 3.
step up (an admin capability is dormant)On a terminal, it asks for an authenticator code and carries on; with --no-input or no terminal, prorm <cmd>` needs a fresh step-up. Run `prorm step-up` first., exit 3.
sync (the grant is past its check-in time)It syncs with the klock service and looks again; if the sync fails (offline), it runs and the API decides.
deniedExit 3 with what would unlock it: a plan upgrade (prorm billing plans), another team role, or nothing the CLI can offer.
With --json the refusal is JSON on stdout: {"error", "needs", "unlock"}
(unlock is login, upgrade, team-role, step-up, sync or none).
The same applies when the API refuses: step_up_required gets the step-up
offer and one retry, capability_missing prints its unlock hint, and any other
klock or sign-in error prints What to do (<code>) with the fix and a link to
its entry in the klock error reference. The guard is
a courtesy, not the lock — the API enforces every capability on every request,
whatever the CLI believed.
prorm --help lists what you may use. A group you can use none of gets one
line under Locked saying what would unlock it (for example Teams — sign in to unlock (prorm login)); staff groups get nothing.

Output formats#

Every command takes --json, and the framework supports table, json,
csv and tsv, so they compose with jq and friends:
With --json, prose (progress, hints, warnings) goes to stderr and only the
answer is on stdout, so a pipe gets clean JSON. A refusal by the guard is
JSON too:
$ prorm teams members --json          # signed out
{
  "error": "Sign in to use `prorm teams members`: prorm login",
  "needs": "team:read",
  "unlock": "login"
}
$ echo $?
3

Global flags#

These are accepted by every command in the registry — the platform groups and
the live-database groups alike — unless a command declares its own flag of
the same name:
FlagDoes
--jsonRaw JSON on stdout.
--output, -o <fmt>table (default), json, csv, tsv.
--quiet, -qSuppress everything but errors and the actual answer.
--verboseDebug output, including every HTTP request and the SQL the live-database commands run.
--color / --no-colorForce colour on or off; auto-detected otherwise.
--api-url <url>Override the API base URL for one run.
--team <id>Team context for one run.
--yes, -yPre-confirm prompts (db delete, roles delete, …).
--no-inputNever prompt; a prompt resolves to its default. Implied by $CI.
--profile <name>Use a named config profile (~/.prorm/config.<name>.json).
--config <path>Use an alternate config file.
--no-truncate, --wideDo not trim wide table columns to the terminal.
--timeout <sec>HTTP timeout.
Exit codes are distinct so a script can tell classes of failure apart: 0 ok,
1 error, 2 usage (bad flag or missing argument), 3 auth, 4 network,
5 not found, 6 findings (prorm audit), 7 inconclusive evidence
(prorm audit), 124 timeout. schema exists uses 5 deliberately.
<command> --help prints that command's own arguments, flags, defaults,
choices and examples; prorm --help lists every command by group.
A handful of commands answer to a shorter name as well: databases, keys,
roles, policies, sessions, audit, query, trace, pq (for
passwordless query), me, and tables, describe, analyze, vacuum,
reindex for their schema/maintain forms.

Shell completion#

$ prorm completion zsh >> ~/.zshrc          # or: > "${fpath[1]}/_prorm"
$ prorm completion bash >> ~/.bashrc
$ prorm completion fish > ~/.config/fish/completions/prorm.fish
Completion is dynamic: the scripts call prorm __complete -- <words…>, which
prints the next words you may type — only the commands you may use, so it
follows sign-in and sign-out without regenerating anything, and staff
commands appear only for admins. It works offline from the view the CLI
already holds (no network, no keychain). Scripts generated by an older
version keep working with their fixed list; run prorm completion again for
the dynamic ones.

How the commands are organised#

Local commands live in src/cli.ts as a direct dispatch. Everything else is a
module under src/cli/basic/commands, each exporting a commands: Command[] array
registered with the small framework in src/cli/core/framework.ts. The live-database
groups share the connection handling in src/cli/core/connect.ts, which resolves the
flags/environment above, opens and closes the connection, and implements
--dry-run by capturing the statements a query-interface call would have run. An unknown
command falls through from the local dispatcher into that registry, and an
unrecognised name gets a "did you mean" suggestion.
The staff commands, src/cli/admin/commands.ts, are registered in the same
registry as prorm admin …. src/cli/admin/program.ts and
src/cli/team/program.ts are only the one-release aliases: src/admin-cli.ts and
src/team-cli.ts (the ORM's bins) and the standalone package's PRORM_ALIAS
shims print the notice and run prorm. resolveCommand matches the longest
registered name of up to four words (admin inbox addresses add).
What each command needs is src/cli/core/command-needs.ts, applied by
register() when a command does not set needs itself, so command modules
need no edits (a test fails when a registered command has no entry). The
guard is in runRegistered (src/cli/core/framework.ts); the views it reads
come from src/cli/core/capability-view.ts, which both entry points install.
A Command declares its name (which may be namespaced with a space, like
db add), description, group, aliases, positionals, flags — with types,
choices, defaults and required — examples, and a run function. Adding a
command means writing that object and listing its module in
src/cli/basic/commands/index.ts; there is no central switch to edit.
export const commands: Command[] = [{
  name: 'db list',
  group: 'Databases',
  description: 'Your databases.',
  examples: ['prorm db list --json'],
  async run() {
    const { databases } = await api<{ databases: Database[] }>('/provision/databases');
    out.table(databases.map((d) => ({ id: d.id, name: d.name, engine: d.type })));
    return 0;
  },
}];
The platform commands share src/cli/core/cloud.ts: the config file and keychain,
the transport (bearer token, idempotency keys, retries on the idempotent
verbs only, typed errors), and resolveDatabase(), which turns an id, uuid or
name — or the configured default — into the database a command means.

Related reading#

cli — how the CLI was consolidated
Migrations
Diagrams
Defining models
Modified at 2026-10-04 00:40:16
Previous
Capabilities, tiers and team roles
Next
Cloudflare
Built with