NotusBase

Reference

<!-- Generated by notusbase/ops/reference.sh from the code. Do not edit: change the code or the texts in notusbase/reference/ and run the script. -->

Everything that exists, in one page: the commands and their options, what a function can call, the modules, the test kit, notusbase.json and every environment variable. The lists come from the code of this version, and a test keeps the page and the code equal. For how to use each piece, with the reasons, read the guide and the rules.

Commands

notusbase <command> --help prints the same text, and notusbase help <topic> prints a part of this page from inside the binary, with the topics commands, functions, examples, modules, kit, config, variables and rules (the rules). A command that talks to a server (--url, --key, --dir) takes the address from --url, from NOTUSBASE_URL or from notusbase.json, and the deploy key from --key, from NOTUSBASE_DEPLOY_KEY or from the pb_data in --dir.

CommandWhat it does
notusbase backup-checkRestores the backup alongside and compares it with the live database
notusbase backup-s3-statusShows what can be restored from the bucket
notusbase bundleOnly bundles, without deploying
notusbase capacityShows the database size against the limit, the growth, the writer and the next step
notusbase certificateLists the client certificates (name, subject, validity), as JSON, never the key
notusbase certificate removeDeletes a certificate
notusbase certificate setStores a PKCS#12 certificate (A1) for fetchWithCertificate and signXml. The password comes from the terminal or stdin
notusbase checkChecks the database integrity
notusbase codegenGenerates the _generated folder
notusbase copyCopies production from its backup in one step: masked to develop, isolated to reproduce a bug or rehearse a deploy, or as the homologation
notusbase cronLists the crons from crons.ts, with the next run and the last one
notusbase cron runRuns the cron now, without waiting for its time. The next occurrence follows the schedule. Shows the id of the job it created
notusbase deployGenerates types, checks, bundles and deploys the functions; --rollback goes back to a saved deploy
notusbase deploy-keyShows this pb_data's deploy key
notusbase devStarts the server and redeploys the functions on every change
notusbase envLists the variables and the environment of each. The values only come with --values
notusbase env listThe same as notusbase env
notusbase env removeDeletes a variable, in the server's environment or the one in --env
notusbase env setSets a variable, in the server's environment or the one in --env. Without the value on the command line, where it would stay in the shell history, it is asked for in the terminal or read from stdin: notusbase env set KEY < key.txt
notusbase exportSaves tables and files to a zip in the npx convex export format
notusbase importLoads a zip from notusbase export or npx convex export. Without --replace, the tables must be empty
notusbase initCreate a project: schema, a query, a mutation, a test, tsconfig and package.json
notusbase jobLists the scheduled functions (runAfter, runAt, crons), with the id that job run takes
notusbase job runBrings a pending job to now, without waiting for its time. The id comes from notusbase job
notusbase logsFollows the functions live. With --since, shows the past, from pb_data/notusbase.log, and exits; with the local server stopped, reads the file in --dir. Times in UTC
notusbase mcpDevelopment MCP server, for an AI to see and work with the local server: functions, schema, logs, run, SQL, schedule, deploy and rollback. Speaks MCP over stdin and stdout; needs Node
notusbase metrics-keyShows the key that only opens the metrics
notusbase promoteTurns the replica into the new primary and starts the server
notusbase replicaFollows the bucket as a standby server, serving nothing
notusbase replica-statusShows the replica lag
notusbase restoreRebuilds the database, the files, the published code and the deploy key from NOTUSBASE_BACKUP
notusbase restore-checkRestores the newest backup into a new folder, compares it with the live database and starts it isolated
notusbase restore-s3Rebuilds the database and files from the bucket
notusbase runRuns a function, including internal ones, and prints its answer as JSON; if the function fails, exits with 1. A large argument comes from @file or, with -, from stdin. With --as, runs as a user
notusbase serveStarts the web server (default to 127.0.0.1:8090 if no domain is specified)
notusbase sqlReads the database with SQL, read-only and with a time limit; to avoid competing for CPU, use nice
notusbase superuserManage superusers
notusbase superuser createCreates a new superuser
notusbase superuser deleteDeletes an existing superuser
notusbase superuser ipsUpdates the superuser IPs whitelist setting (the IPs/subnets arguments must be space separated; leave empty to clear the whitelist restriction)
notusbase superuser otpCreates a new OTP for the specified superuser
notusbase superuser updateChanges the password of a single superuser
notusbase superuser upsertCreates, or updates if email exists, a single superuser
notusbase userAccounts in the login collection
notusbase user addCreates an account in the login collection, even with signup closed. The password comes from the terminal or stdin
notusbase vacuumCompacts the database, with the server stopped. Search is rebuilt at startup
notusbase versionShows the binary version

Options of every command

OptionDefaultWhat it does
--admin-httpport for the dashboard, deploy and metrics; --http then serves only the apps
--behind-proxyaccepts starting exposed, behind a reverse proxy on this machine that speaks TLS and overwrites X-Forwarded-For, and trusts the IP it reports
--dirpb_datathe data folder (or NOTUSBASE_DIR)
--encryptionEnvthe env variable whose value of 32 characters will be used as encryption key for the app settings (default none)
--envproductionthe environment this server is: production, staging or development (or NOTUSBASE_ENV). Only production sends email and opens fetch; without it, development
--import-limit262144ceiling of the uncompressed import, in MB
--isolatedstarts a copy without crons, jobs, network, _env, backup or alerts (or NOTUSBASE_ISOLATED=1)
--queryTimeout30the default SELECT queries timeout in seconds

notusbase backup-check

Restores the backup alongside and compares it with the live database.

Asks the server that is up to prove its backup: it restores the backup beside the database and compares the copy with the live data, table by table, without stopping. On a large database it takes minutes. The server needs NOTUSBASE_BACKUP.

It prints the result as JSON and exits with an error when the check did not pass or gave up midway. The server also runs it by itself, once a day; --last prints the result of the last one, without running another.

notusbase backup-check [flags]
notusbase backup-check
notusbase backup-check --last
# where the dashboard and deploy are on a port of their own
notusbase backup-check --url http://localhost:8091
OptionDefaultWhat it does
--jsonthe answer as JSON
--keydeploy key
--lastonly shows the last check
--toa destination named in the remotes of notusbase.json, such as production: opens an SSH tunnel to it, reads the deploy key there, uses both and closes. Nothing is stored here
--urlhttp://127.0.0.1:8090address of the server this command talks to

notusbase backup-s3-status

Shows what can be restored from the bucket.

Prints what the bucket holds, without a server: each chain with its segments, the size of its base, and the first and last instant it can restore to. It reads the NOTUSBASE_S3_* variables.

notusbase backup-s3-status
notusbase backup-s3-status

notusbase bundle

Only bundles, without deploying.

Bundles convex/ into one file, as notusbase deploy would send it, and stops there: no _generated, no tsc, no server. It prints nothing when all is well. The tests and the bench serve the file with NOTUSBASE_TEST_BUNDLE.

notusbase bundle [flags]
notusbase bundle
notusbase bundle --output tmp/bundle.js
OptionDefaultWhat it does
--functionsconvexfunctions directory
--outputbundle.jsoutput file

notusbase capacity

Shows the database size against the limit, the growth, the writer and the next step.

Prints how far the server that is up is from its limits: the size of the database against the limit, how much it grows a day and when it reaches the limit, how busy the writer is, the open sessions, the size of each table, and the next step to take, when there is one. The growth needs NOTUSBASE_BACKUP on the server: each daily check of the backup stores one point.

notusbase capacity [flags]
notusbase capacity
# for a script
notusbase capacity --json
OptionDefaultWhat it does
--jsonthe answer as JSON
--keydeploy key
--toa destination named in the remotes of notusbase.json, such as production: opens an SSH tunnel to it, reads the deploy key there, uses both and closes. Nothing is stored here
--urlhttp://127.0.0.1:8090address of the server this command talks to

notusbase certificate

Lists the client certificates (name, subject, validity), as JSON, never the key.

Prints the client certificates stored on the server, as JSON: name, subject, issuer, notBefore, notAfter and sha256 of each. The key and the password never leave the server. notAfter is when to renew.

notusbase certificate [flags]
notusbase certificate
OptionDefaultWhat it does
--jsonthe answer as JSON
--keydeploy key
--toa destination named in the remotes of notusbase.json, such as production: opens an SSH tunnel to it, reads the deploy key there, uses both and closes. Nothing is stored here
--urlhttp://127.0.0.1:8090address of the server this command talks to

notusbase certificate remove

Deletes a certificate.

Deletes the certificate of that name from the server. It prints nothing, also when there was none. A function that asks for it fails from then on, saying how to store it again.

notusbase certificate remove <name>
notusbase certificate remove company
OptionDefaultWhat it does
--keydeploy key
--toa destination named in the remotes of notusbase.json, such as production: opens an SSH tunnel to it, reads the deploy key there, uses both and closes. Nothing is stored here
--urlhttp://127.0.0.1:8090address of the server this command talks to

notusbase certificate set

Stores a PKCS#12 certificate (A1) for fetchWithCertificate and signXml. The password comes from the terminal or stdin.

Stores a client certificate (a PKCS#12 file, .pfx) on the server under a name, for the functions that call fetchWithCertificate and signXml with that name. The same name again replaces the file, and new requests use the new one.

The password of the file is never an argument: in a terminal it is asked for twice, without echo; otherwise it is read from stdin, one line. The server opens the file with it before storing, so a wrong password is refused. The name takes letters, digits, _ and -, and the file up to 64 KB. It prints what was stored, as notusbase certificate does.

notusbase certificate set <name> <file.pfx>
# asks for the password of the file
notusbase certificate set company ./company.pfx
# the password from a file, one line
notusbase certificate set company ./company.pfx < password.txt
OptionDefaultWhat it does
--keydeploy key
--toa destination named in the remotes of notusbase.json, such as production: opens an SSH tunnel to it, reads the deploy key there, uses both and closes. Nothing is stored here
--urlhttp://127.0.0.1:8090address of the server this command talks to

notusbase check

Checks the database integrity.

Checks the integrity of the database file of a pb_data and prints ok, or what it found, exiting with an error. The server may be up. Use it instead of sqlite3, which does not know the functions the indexes of NotusBase use and cannot run the check.

Without --dir or NOTUSBASE_DIR it uses the dir of notusbase.json, in this folder or in one above (saying which file when it is above), or else pb_data here. A folder with no data.db is refused, with its full path: start the server there first (notusbase dev or serve), or pass the project's folder.

notusbase check [flags]
notusbase check
notusbase check --dir /var/lib/notusbase/pb_data

notusbase codegen

Generates the _generated folder.

Writes what the editor, tsc and an AI need to see the project as the binary does, without a server. Run it in the project folder; it prints nothing when all is well. It writes only what changed:

notusbase dev and notusbase deploy do the same before bundling. It refuses components.* with no convex/convex.config.ts to install them, and an import of a module from a path that does not exist, saying the right one.

notusbase codegen [flags]
# after replacing the binary, or after npm install
notusbase codegen
OptionDefaultWhat it does
--functionsconvexfunctions directory

notusbase copy

Copies production from its backup in one step: masked to develop, isolated to reproduce a bug or rehearse a deploy, or as the homologation.

Copies production from its backup (--backup, the folder of the chains, or --s3) into a new folder, in one step, and leaves it fit for one of three uses. Run it in the project folder.

The mask replaces emails, phones, names, CPF and CNPJ by field name (and by the mask key of notusbase.json), empties credentials, sessions and _env, replaces the vectors of a vector index by random ones, and sweeps every other text for addresses, phones, documents and the values it replaced. Ids, dates, amounts and timestamps stay. A name typed in free text that no name field holds stays: the output lists those fields. The fakes reach nobody: @example.test addresses and phones that start with 10. Storage files do not come along in a masked copy.

The copy is marked: it never starts as production, and one with real data never starts as development. It refuses production's outlets in the environment (NOTUSBASE_ENV=production, SMTP, webhooks, alerts) and states the space it needs before it starts.

notusbase copy development|staging|isolated [flags]
# develop with production's data masked, from the bucket
notusbase copy development --s3
# the function that failed, with what the customer sent, as that customer, on a masked copy
notusbase copy isolated --mask --function tasks:create --args @args.json --as k17abc
# rehearse the deploy of this folder on a copy of the backup, deleted at the end
notusbase copy isolated --rehearse --backup /var/backups/notusbase
# the homologation on the production server, renewed over the old one
notusbase copy staging --backup /var/backups/notusbase --folder /srv/notusbase-staging --replace
OptionDefaultWhat it does
--argsfunction arguments: JSON, @file or - for stdin
--asruns the function as a user: the id of the account, the same as in production
--backupfolder of the backup chains (default: NOTUSBASE_BACKUP)
--exitisolated: exits after running the function, instead of leaving the server up
--folderwhere the copy lives (default: ./tmp/copy-<use>-<date>; staging needs one)
--functionisolated: function to run after starting (module:function)
--functionsconvexfunctions directory
--keepisolated: keeps the copy at the end, also with real data
--maskreplaces personal data with fake values (always in development)
--no-deployisolated: keeps the published code of the backup instead of deploying this folder's
--port0port of the copy (default: a free one)
--rehearseisolated: deploy --plan, deploy and rehearse.mjs, a report, and the copy deleted
--replacestaging: renews the copy in --folder, which must be one
--s3downloads from the bucket, with NOTUSBASE_S3_*, instead of the folder
--skip-typecheckdeploys without tsc
--untilinstant in RFC 3339; without it, goes to the end

notusbase cron

Lists the crons from crons.ts, with the next run and the last one.

Lists the crons of crons.ts on the server that is up, one per line: the name, the schedule, the next run and the last one, with the time in UTC. It changes nothing. --json prints the same as a list of objects. To run one now, use notusbase cron run.

notusbase cron [flags]
notusbase cron
# for a script
notusbase cron --json
OptionDefaultWhat it does
--jsonthe answer as JSON
--keydeploy key
--toa destination named in the remotes of notusbase.json, such as production: opens an SSH tunnel to it, reads the deploy key there, uses both and closes. Nothing is stored here
--urlhttp://127.0.0.1:8090address of the server this command talks to

notusbase cron run

Runs the cron now, without waiting for its time. The next occurrence follows the schedule. Shows the id of the job it created.

Runs a cron of crons.ts now, by its name, on the server that is up. It schedules one run for this moment, prints the id of the job, {"id":"..."}, and does not wait for it to end; the cron's next occurrence stays where it was. It is refused while the last run of that cron has not finished, and when the name is not in the deployed crons.ts. notusbase sql "select name, next from _crons" lists the names.

notusbase cron run <name>
notusbase cron run cleanup
OptionDefaultWhat it does
--jsonthe answer as JSON
--keydeploy key
--toa destination named in the remotes of notusbase.json, such as production: opens an SSH tunnel to it, reads the deploy key there, uses both and closes. Nothing is stored here
--urlhttp://127.0.0.1:8090address of the server this command talks to

notusbase deploy

Generates types, checks, bundles and deploys the functions; --rollback goes back to a saved deploy.

Sends the functions of this project to a server that is running. Run it in the project folder. In order:

  1. writes convex/_generated and what follows the binary in the project, as notusbase codegen does;
  2. runs tsc when the project has it in node_modules, and stops on a type error (--skip-typecheck skips it);
  3. bundles convex/;
  4. sends the bundle. The server checks the new schema.ts against the data that exists and refuses the deploy, changing nothing, when a document does not fit. Otherwise it creates tables, columns and indexes and swaps the code with no downtime.

It prints one plan: line for each thing it changed in the database, then version N live, or no change when the bundle is the one already live. A variable the code reads as process.env.NAME that has no value on the server is a warning; set it with notusbase env set.

--plan does steps 1 to 3, then asks the server what the deploy would change and what it would refuse. Nothing is deployed, and the command exits with an error when the deploy would be refused, so a script can test it.

The server keeps the last bundles. --history lists them, newest first, and --rollback deploys the previous one again, or the one whose number is given. A rollback passes through the same checks, gets a new version number, changes no data and does not touch the files in convex/: the next notusbase deploy sends your folder again. --rollback --plan shows what it would do.

The server is the one at --url, NOTUSBASE_URL or the url of notusbase.json, in that order. The key comes from --key or NOTUSBASE_DEPLOY_KEY; without them it is derived from the pb_data at --dir, which works on the server's own machine. A server on another machine has to be reached over https. Every command with these three options finds the server this way.

notusbase deploy [number] [flags]
# to the local server of notusbase.json
notusbase deploy
# what it would change in the database, deploying nothing
notusbase deploy --plan
# to another machine, with the key that `notusbase deploy-key` printed there
NOTUSBASE_DEPLOY_KEY=... notusbase deploy --url https://admin.example.com
# the saved bundles, then back to the previous one, or to one of them
notusbase deploy --history
notusbase deploy --rollback
notusbase deploy --rollback 12 --plan
OptionDefaultWhat it does
--functionsconvexfunctions directory
--historylists the deploys saved on the server, and which ones can be rolled back to
--jsonthe answer as JSON
--keydeploy key
--planshows what the deploy (or the rollback) would change in the database, without deploying anything
--rollbackgoes back to a saved deploy: the one of the number given (notusbase deploy --rollback 7) or, without a number, the previous one
--skip-typecheckdoes not run tsc
--toa destination named in the remotes of notusbase.json, such as production: opens an SSH tunnel to it, reads the deploy key there, uses both and closes. Nothing is stored here
--urlhttp://127.0.0.1:8090address of the server this command talks to
--yeswith --to, deploys or rolls back without asking first: for scripts

notusbase deploy-key

Shows this pb_data's deploy key.

Prints the deploy key of a server, from its pb_data. Run it on the server's machine; no server has to be up, but it must have started once. The key opens everything the commands do: deploy, run, variables, export.

On the server's own machine the commands find the key without this. It is for another machine: put the output in NOTUSBASE_DEPLOY_KEY there, or pass it as --key. The key is derived from the secret in pb_data and does not change while that file stays.

Without --dir or NOTUSBASE_DIR it uses the dir of notusbase.json, in this folder or in one above (saying which file when it is above), or else pb_data here. A folder with no data.db is refused, with its full path: start the server there first (notusbase dev or serve), or pass the project's folder.

notusbase deploy-key [flags]
notusbase deploy-key
notusbase deploy-key --dir /var/lib/notusbase/pb_data

notusbase dev

Starts the server and redeploys the functions on every change.

The development server: run it in the project folder and leave it running. It writes convex/_generated, bundles convex/, starts the server with its data in pb_data and deploys. From then on every file saved in convex/ is deployed again, with the same checks as notusbase deploy, and the result is a line that starts with dev:. tsc runs after each deploy and only reports: a type error does not hold the deploy.

The address is the url of notusbase.json, or --http. The dashboard is at /_/ of the same address.

What it turns on that notusbase serve does not, all of it for development:

What dev deploys lives in memory. A notusbase serve on the same pb_data has no functions until a notusbase deploy.

notusbase dev [flags]
# in the project folder, until Ctrl-C
notusbase dev
# on another port, without changing notusbase.json
notusbase dev --http 127.0.0.1:3490
OptionDefaultWhat it does
--functionsconvexfunctions directory
--http127.0.0.1:8090server address

notusbase env

Lists the variables and the environment of each. The values only come with --values.

Lists the variables on the server, one row per line: the name and the environment it belongs to (production, staging, development, or all for a row every environment sees). They are what the functions read as process.env.NAME. The values only come with --values, as the ones this server uses; mind where the output goes. --env shows the rows of one environment.

env set and env remove take effect on the next call of a function, with no deploy and no restart. The code loads again for it, so a value a file copied from process.env when it loaded is the new one too, and so is the key of a client created at the top of a file. Calls already running finish as they were. The first call in each isolate after the change pays for loading the code.

notusbase env [flags]
notusbase env
notusbase env --env staging
notusbase env --values --url https://admin.example.com
OptionDefaultWhat it does
--jsonthe answer as JSON
--keydeploy key
--toa destination named in the remotes of notusbase.json, such as production: opens an SSH tunnel to it, reads the deploy key there, uses both and closes. Nothing is stored here
--urlhttp://127.0.0.1:8090address of the server this command talks to
--valuesshows the values this server's functions see: NAME=value per line, or the whole object with --json

notusbase env list

The same as notusbase env.

The same as notusbase env.

notusbase env list [flags]
notusbase env list --env production
OptionDefaultWhat it does
--jsonthe answer as JSON
--keydeploy key
--toa destination named in the remotes of notusbase.json, such as production: opens an SSH tunnel to it, reads the deploy key there, uses both and closes. Nothing is stored here
--urlhttp://127.0.0.1:8090address of the server this command talks to
--valuesshows the values this server's functions see

notusbase env remove

Deletes a variable, in the server's environment or the one in --env.

Deletes one variable from the server, in the environment the server runs as, or the one in --env. A function that reads it gets undefined, or the shared row, from the next call on.

notusbase env remove <name>
notusbase env remove STRIPE_SECRET_KEY
notusbase env remove --env all APP_URL
OptionDefaultWhat it does
--keydeploy key
--toa destination named in the remotes of notusbase.json, such as production: opens an SSH tunnel to it, reads the deploy key there, uses both and closes. Nothing is stored here
--urlhttp://127.0.0.1:8090address of the server this command talks to

notusbase env set

Sets a variable, in the server's environment or the one in --env. Without the value on the command line, where it would stay in the shell history, it is asked for in the terminal or read from stdin: notusbase env set KEY < key.txt.

Sets one variable on the server, replacing the value it had, in the environment the server runs as, or the one in --env (all for a row every environment sees). When a name has both, the row of the server's environment wins. The next call of a function already reads it as process.env.NAME: no deploy, no restart.

Without the value on the command line, where it would stay in the shell history, it is asked for in the terminal or read from stdin.

notusbase env set <name> [value]
notusbase env set STRIPE_SECRET_KEY < key.txt
notusbase env set --env staging STRIPE_SECRET_KEY sk_test_123
notusbase env set --env all APP_URL https://app.example.com --url https://admin.example.com
OptionDefaultWhat it does
--keydeploy key
--toa destination named in the remotes of notusbase.json, such as production: opens an SSH tunnel to it, reads the deploy key there, uses both and closes. Nothing is stored here
--urlhttp://127.0.0.1:8090address of the server this command talks to

notusbase export

Saves tables and files to a zip in the npx convex export format.

Saves the data of the server that is up to a zip, in the format of npx convex export: one folder for each table of the deployed schema.ts, with its documents as JSON lines, the stored files, and the tables of each Convex component under _components/<name>/. It is read at one instant, while the server keeps serving. It prints the file and its size.

Left out: the accounts of the login collection, the variables, the certificates and what is scheduled.

The zip goes into another NotusBase with notusbase import, and into Convex with npx convex import.

notusbase export <file.zip> [flags]
notusbase export backup.zip
notusbase export backup.zip --url https://admin.example.com
OptionDefaultWhat it does
--keydeploy key
--toa destination named in the remotes of notusbase.json, such as production: opens an SSH tunnel to it, reads the deploy key there, uses both and closes. Nothing is stored here
--urlhttp://127.0.0.1:8090address of the server this command talks to

notusbase import

Loads a zip from notusbase export or npx convex export. Without --replace, the tables must be empty.

Loads a zip into the server that is up: one from notusbase export, or one from npx convex export, with the same ids, references and files. Deploy first: each table of the zip has to be in the deployed schema.ts. The documents go in one transaction, all or none, and the files after it.

Without --replace each table of the zip has to be empty on the server. With it, the tables that are in the zip are emptied and loaded again; the others are not touched.

The tables of a Convex component (_components/<name>/ in the zip) go into that component's own tables when convex/convex.config.ts installs it, with the same ids; notusbase export writes them back the same way.

What the server does not take is named and nothing is loaded: a table outside schema.ts, a Convex system table, the tables of a component the project does not install. --skip leaves tables and components out.

It prints how many documents went into each table, the files, and what was skipped.

notusbase import <file.zip> [flags]
# into empty tables
notusbase import backup.zip
# over what is there, in the tables of the zip
notusbase import backup.zip --replace
# a Convex export: without the login tables, and without a component the project does not install
notusbase import convex.zip --skip authAccounts,authSessions,_components/resend
OptionDefaultWhat it does
--keydeploy key
--replacereplaces what already exists in the file's tables
--skiptables and components left out, comma-separated (e.g. authAccounts,_components/stripe)
--toa destination named in the remotes of notusbase.json, such as production: opens an SSH tunnel to it, reads the deploy key there, uses both and closes. Nothing is stored here
--urlhttp://127.0.0.1:8090address of the server this command talks to

notusbase init

Create a project: schema, a query, a mutation, a test, tsconfig and package.json.

Writes a project into the folder (the current one, when none is given) and prints the next steps. It installs nothing and starts no server: npm install comes next.

The plain project is package.json, notusbase.json, convex/schema.ts with one table, convex/tasks.ts with a query and a mutation, convex/tsconfig.json, kit.mjs and a test.mjs that npm test runs. It also gets .gitignore, CLAUDE.md (the rules for writing a system, for an AI) and .mcp.json, when the folder does not have them, and convex/_generated.

It overwrites nothing. When one of its files is already in the folder it stops, names them and creates none.

--port is the port of the local server, written as the url of notusbase.json: notusbase dev listens there and the other commands find the server there. Give each project on the same machine its own.

notusbase init [dir] [flags]
# a new project in a new folder
notusbase init my-app
# with its server on another port
notusbase init my-app --port 3490
# in the current folder
notusbase init
OptionDefaultWhat it does
--port8090server port, written to notusbase.json

notusbase job

Lists the scheduled functions (runAfter, runAt, crons), with the id that job run takes.

Lists the scheduled functions on the server that is up (runAfter, runAt and the runs of crons), with the id that notusbase job run takes. --view picks which: scheduled (pending and running, the default), failed, done or canceled; --function keeps the jobs of one function. It changes nothing. --json prints a list of objects.

notusbase job [flags]
notusbase job
# the ones that failed, of one function
notusbase job --view failed --function tasks:create
OptionDefaultWhat it does
--functiononly the jobs of one function
--jsonthe answer as JSON
--keydeploy key
--toa destination named in the remotes of notusbase.json, such as production: opens an SSH tunnel to it, reads the deploy key there, uses both and closes. Nothing is stored here
--urlhttp://127.0.0.1:8090address of the server this command talks to
--viewscheduledwhich jobs: scheduled (pending and running), failed, done or canceled

notusbase job run

Brings a pending job to now, without waiting for its time. The id comes from notusbase job.

Brings one scheduled run (runAfter, runAt) to now, by its id, on the server that is up. Only a pending job moves: one that started, ended or was cancelled is refused. The id is on the dashboard, in Logs → Scheduled, and notusbase cron run prints the one it creates.

notusbase job run <id>
notusbase job run 0193a5c2e4b87d10
OptionDefaultWhat it does
--keydeploy key
--toa destination named in the remotes of notusbase.json, such as production: opens an SSH tunnel to it, reads the deploy key there, uses both and closes. Nothing is stored here
--urlhttp://127.0.0.1:8090address of the server this command talks to

notusbase logs

Follows the functions live. With --since, shows the past, from pb_data/notusbase.log, and exits; with the local server stopped, reads the file in --dir. Times in UTC.

Prints what the functions wrote with console.log and the errors they threw, one line each: the time in UTC, module:function and the text.

Without --since it follows the server live, from now on, until Ctrl-C. With --since it reads the past from the log the server keeps on disk, prints the last --limit lines that match, oldest first, and exits. --since takes 30m, 1h, 2d or a date in RFC 3339.

--function keeps one function (tasks:create) or one module (tasks). --text keeps the lines with that text. An error the client received as [Request ID: ...] Server Error can be pasted whole in --text: the id finds the line with the detail.

notusbase logs [flags]
# live, until Ctrl-C
notusbase logs
# the last hour of one module
notusbase logs --since 1h --function tasks --limit 50
# the detail of an error the client received
notusbase logs --since 1d --text '[Request ID: 3f9c2a7d81e04b56] Server Error'
OptionDefaultWhat it does
--functiononly one function (module:function) or one module
--jsonthe answer as JSON
--keydeploy key
--limit1000with --since, the last N lines
--sinceshows the past: 30m, 1h, 2d or an RFC 3339 date
--textonly the lines with this text; the error pasted as the client received it, [Request ID: …] Server Error, finds its line
--toa destination named in the remotes of notusbase.json, such as production: opens an SSH tunnel to it, reads the deploy key there, uses both and closes. Nothing is stored here
--urlhttp://127.0.0.1:8090address of the server this command talks to

notusbase mcp

Development MCP server, for an AI to see and work with the local server: functions, schema, logs, run, SQL, schedule, deploy and rollback. Speaks MCP over stdin and stdout; needs Node.

Starts the development MCP server, for an AI that writes the system to see the local server and work with it. An MCP client runs this command and speaks to it over stdin and stdout: notusbase init writes the .mcp.json that tells it so. It needs Node 22 or newer.

It only talks to a server on this machine, the one of notusbase.json, and refuses any other address. Its tools list the functions and the schema, read the logs, run a function, read the database with SQL, show the names of the variables, show and run what is scheduled, deploy and roll back, and print these pages (docs). All but docs need notusbase dev up.

notusbase mcp
# what an MCP client runs, in the project folder
notusbase mcp

notusbase metrics-key

Shows the key that only opens the metrics.

Prints the key that opens only the metrics of a server (/api/notusbase/metrics, in the Prometheus format), from its pb_data. It is the one to give to a monitor, which then cannot deploy or read data. Run it on the server's machine, after the server has started once.

Without --dir or NOTUSBASE_DIR it uses the dir of notusbase.json, in this folder or in one above (saying which file when it is above), or else pb_data here. A folder with no data.db is refused, with its full path: start the server there first (notusbase dev or serve), or pass the project's folder.

notusbase metrics-key [flags]
notusbase metrics-key
notusbase metrics-key --dir /var/lib/notusbase/pb_data

notusbase promote

Turns the replica into the new primary and starts the server.

Turns the replica in that folder into the primary. It refuses when the primary still answers, when there is no address to check it (--primary or NOTUSBASE_ALERT_PRIMARY), when the standby's check failed, when what came by the direct copy does not apply, when a stored file a row points to is missing here (--accept-missing-files goes on without them), or when another server writes to the bucket: once the replica stops, promote watches the primary's beat for its write lease plus 2 s (about 32 s). --force skips the watch, not the wait, never the bucket. Of two promotions at once the second refuses; a provider without If-None-Match cannot promote. It stops the replica, takes the bucket from the old primary, applies what is left from the bucket and the direct copy, checks the database and starts notusbase serve with the options after --. The old primary stops on reading the new epoch; until then its client writes are lost. Cut off from the bucket, it stops when its write lease runs out (30 s with a direct standby; promote waits for it), else never. A planned switchover loses nothing: stop the primary cleanly (it sends its last point and files to the bucket, marked final), then --planned refuses unless the standby has that point before the epoch and the primary did not start again since.

There is no way back: promoting by mistake takes a healthy primary down. It needs NOTUSBASE_BACKUP, for the new primary to have a backup from its first minute. --no-serve prepares the folder and stops there. --check quick runs quick_check before serving and integrity_check after. Once the server listens it runs NOTUSBASE_PROMOTE_HOOK, which moves the route; if the hook fails, promote again on the folder redoes it.

notusbase promote <dir> [-- serve options] [flags]
# after --, the options the old primary's serve had
notusbase promote /var/lib/notusbase/replica -- --http 127.0.0.1:8090 --admin-http 127.0.0.1:8091
# only prepare the folder
notusbase promote /var/lib/notusbase/replica --no-serve
OptionDefaultWhat it does
--accept-missing-filespromote with stored files missing
--checkfullfull: integrity_check before serving; quick: quick_check before, integrity_check after
--forcepromote with the primary answering or a failed standby check; never without the bucket
--no-serveonly prepares the directory; running serve is up to you
--plannedafter a clean stop: refuse unless its final point is here
--primarythe primary's address, to check it is down (default NOTUSBASE_ALERT_PRIMARY)

notusbase replica

Follows the bucket as a standby server, serving nothing.

Keeps a standby copy of a server in a folder, fed by the bucket the primary writes to. It runs until it is stopped and serves nothing: no functions, no dashboard, no crons. It reads the same NOTUSBASE_S3_* variables as the primary. The folder is the replica's own and must not hold a database.

With NOTUSBASE_ALERT_PRIMARY set, it also watches the primary and receives from it directly, a fraction of a second behind instead of the bucket's minutes; the bucket still gives it the base of each chain.

notusbase replica-status shows how far behind it is, and notusbase promote turns it into the primary.

notusbase replica <dir>
notusbase replica /var/lib/notusbase/replica

notusbase replica-status

Shows the replica lag.

Prints where the replica is: the last point it applied and how long ago, the last point in the bucket, the last contact with the bucket, its last check, and whether the direct copy was in step at the primary's last answer. It reads only the folder, so it works with the bucket out of reach.

notusbase replica-status <dir>
notusbase replica-status /var/lib/notusbase/replica

notusbase restore

Rebuilds the database, the files, the published code and the deploy key from NOTUSBASE_BACKUP.

Rebuilds a database from the local backup, the folder a server writes with NOTUSBASE_BACKUP. No server is involved: it reads the first folder and writes the second, which must not have a data.db. Without --until it goes to the last point of the backup; with it, to that instant.

It prints the chain it restored from and how many stored files came back, and checks the integrity of the result. The new folder is then a pb_data: start it with notusbase serve --dir. The deployed bundle comes back too. So does .notusbase_secret, from which the deploy key is derived: into a folder with no secret it brings the key of the server the backup came from, and says so with a warning. That is right for the same server coming back and wrong for a copy: a production backup restored into staging would carry the production key. For a copy, --new-secret gives the folder a key of its own and leaves the backup's out. --with-secret matters only when the folder already has a secret, and replaces it.

notusbase restore <backup dir> <new dir> [flags]
notusbase restore /var/backups/notusbase pb_data.new
# a copy of production for staging, with a deploy key of its own
notusbase restore /var/backups/notusbase pb_data.staging --new-secret
# as it was at an instant
notusbase restore /var/backups/notusbase pb_data.new --until 2026-11-06T12:00:00Z
OptionDefaultWhat it does
--new-secretgive <new dir> a deploy key of its own and leave the backup's out: a copy, not the same server
--untilinstant in RFC 3339; without it, goes to the end
--with-secretreplace the secret <new dir> already has with the one in the backup

notusbase restore-check

Restores the newest backup into a new folder, compares it with the live database and starts it isolated.

Asks the server that is up to prove that its backup restores: it runs restore of the newest local chain and, with S3, restore-s3 downloading for real, each into a new folder; compares the copy with the live database the same way as backup-check; checks the _storage files by size and SHA-256; starts the copy in isolated mode until it answers; and deletes the folder. It runs at low priority and needs free disk for a copy of the database (twice that with S3); without it, it does not run and says so. The server needs NOTUSBASE_BACKUP.

It prints the result as JSON and exits with an error when the check did not pass or ended without a verdict. The server also runs it by itself, once a week (NOTUSBASE_RESTORE_CHECK_HOURS); --last prints the last attempt and when one last passed, without running another.

notusbase restore-check [flags]
notusbase restore-check
notusbase restore-check --last
OptionDefaultWhat it does
--jsonthe answer as JSON
--keydeploy key
--lastonly shows the last check and when one last passed
--toa destination named in the remotes of notusbase.json, such as production: opens an SSH tunnel to it, reads the deploy key there, uses both and closes. Nothing is stored here
--urlhttp://127.0.0.1:8090address of the server this command talks to

notusbase restore-s3

Rebuilds the database and files from the bucket.

Rebuilds a server's data from the bucket, on a new machine or after a lost disk: the database, the stored files, the secret and the deployed bundle, so the deploy key stays the same, with a warning saying so. For a copy that is not that server, --new-secret gives the folder a key of its own instead. The folder must not have a database. It reads the same NOTUSBASE_S3_* variables as the server that wrote the backup, the key that seals it (NOTUSBASE_S3_KEY) among them.

Without --until it goes to the last point in the bucket; with it, to that instant. It ends by printing the point it reached. The folder is then a pb_data: start it with notusbase serve --dir.

notusbase restore-s3 <new dir> [flags]
notusbase restore-s3 /var/lib/notusbase/pb_data
notusbase restore-s3 /new/folder --until 2026-11-06T12:00:00Z
OptionDefaultWhat it does
--new-secretgive <new dir> a deploy key of its own and leave the bucket's out: a copy, not the same server
--untilinstant in RFC 3339; without it, goes to the end

notusbase run

Runs a function, including internal ones, and prints its answer as JSON; if the function fails, exits with 1. A large argument comes from @file or, with -, from stdin. With --as, runs as a user.

Runs a deployed function on the server and prints what it answered, as JSON. It runs as the administrator: internal functions too, and ctx.auth has no user. A mutation really writes and an action really runs.

The argument is one JSON object. Without it the function gets {}. @file reads the object from a file, and - from stdin, for one too large for a command line.

It prints {"status":"success","value":...}, or {"status":"error","errorMessage":...} when the function threw; under notusbase dev the message carries the detail and the console.log lines come along. The exit code tells whether the server took the call, not whether the function passed: read status.

notusbase run <module:function> [JSON args | @file | -] [flags]
notusbase run tasks:list
notusbase run tasks:create '{"text": "hi"}'
# a large argument, from a file or from stdin
notusbase run imports:load @args.json
cat args.json | notusbase run imports:load -
OptionDefaultWhat it does
--asruns with the identity of a user, as if their client had called: the id or the email of an account in the login collection or, with the login in the app code, the subject its token carries (a bare id of the users table becomes users:<id>), with the issuer of auth.config.ts. Without it ctx.auth has nobody. The route behind it takes the deploy key or a superuser token, and with a single port it is on the public one. The run is marked in the function's log, where the name of who asked is a note this command sends, not an audit
--keydeploy key
--toa destination named in the remotes of notusbase.json, such as production: opens an SSH tunnel to it, reads the deploy key there, uses both and closes. Nothing is stored here
--urlhttp://127.0.0.1:8090address of the server this command talks to

notusbase serve

Starts the web server (default to 127.0.0.1:8090 if no domain is specified).

The server as it runs in production: it serves the functions that the last notusbase deploy sent and the dashboard at /_/, and never watches a folder. It needs no project folder, only its data, pb_data or --dir. Without --http the address is the url of notusbase.json when there is one, or 127.0.0.1:8090.

On 127.0.0.1 everything is on one port. An address that others reach is refused unless the dashboard, the deploy and the metrics are moved to a port of their own with --admin-http; behind a reverse proxy it also takes --behind-proxy and --origins. The refusal says which option is missing.

Signup is closed on every start, and login only counts for the collection named in NOTUSBASE_AUTH_COLLECTIONS (users). An error reaches the client as [Request ID: ...] Server Error, and the detail goes to notusbase logs. notusbase help variables lists what else the environment sets: backup, limits, alerts.

The first start prints no superuser: notusbase superuser upsert creates the one who opens the dashboard.

notusbase serve [domain(s)]
# on this machine, one port
notusbase serve
# login with the users collection, on a port of its own
NOTUSBASE_AUTH_COLLECTIONS=users notusbase serve --http 127.0.0.1:3490
# behind a reverse proxy on the same machine: the apps on one port, the dashboard and deploy on another
notusbase serve --http 127.0.0.1:8090 --admin-http 127.0.0.1:8091 --behind-proxy --origins https://example.com
OptionDefaultWhat it does
--httpTCP address to listen for the HTTP server (if domain args are specified - default to 0.0.0.0:80, otherwise - default to 127.0.0.1:8090)
--httpsTCP address to listen for the HTTPS server (if domain args are specified - default to 0.0.0.0:443, otherwise - default to empty string, aka. no TLS) The incoming HTTP traffic also will be auto redirected to the HTTPS version
--origins[*]CORS allowed domain origins list

notusbase sql

Reads the database with SQL, read-only and with a time limit; to avoid competing for CPU, use nice.

Reads the database with one SQL statement and prints one JSON object for each row, one per line. It opens the file in pb_data (--dir) itself, read-only, so it works with the server up or stopped, and only on the server's machine. Anything that writes is refused, and so is reading a secret: the values in _env, the password and tokenKey of the login collections, _params.value (settings, SMTP password included), _collections.options (token secrets), _otps.password and the _certificates table. Name the other columns instead of *.

Each table of schema.ts is a table with the same name; a field that is optional, a union or an object is stored as JSON, read with json_extract. The tables of NotusBase start with _: _crons, _jobs, _storage.

A query is cut at --timeout and at --max-rows. A long read on a live database holds back its maintenance: for a heavy report, query a restored copy instead.

Without --dir or NOTUSBASE_DIR it uses the dir of notusbase.json, in this folder or in one above (saying which file when it is above), or else pb_data here. A folder with no data.db is refused, with its full path: start the server there first (notusbase dev or serve), or pass the project's folder.

notusbase sql <query> [flags]
notusbase sql "select count(*) from tasks"
# the crons the server knows, and when each runs next
notusbase sql "select name, next from _crons"
notusbase sql "select * from tasks" --max-rows 100 --timeout 30s
OptionDefaultWhat it does
--max-rows100000maximum rows in the result
--timeout10smaximum query time

notusbase superuser

Manage superusers.

notusbase superuser create

Creates a new superuser.

notusbase superuser create EMAIL [PASSWORD]
# the password is asked for in the terminal, or read from stdin
notusbase superuser create [email protected]

notusbase superuser delete

Deletes an existing superuser.

notusbase superuser delete
notusbase superuser delete [email protected]

notusbase superuser ips

Updates the superuser IPs whitelist setting (the IPs/subnets arguments must be space separated; leave empty to clear the whitelist restriction).

notusbase superuser ips
# only these addresses may sign in as a superuser; with no address, any may
notusbase superuser ips 127.0.0.1 10.0.0.0/24
notusbase superuser ips

notusbase superuser otp

Creates a new OTP for the specified superuser.

notusbase superuser otp
# a one-time code to sign in to the dashboard as that superuser
notusbase superuser otp [email protected]

notusbase superuser update

Changes the password of a single superuser.

notusbase superuser update EMAIL [PASSWORD]
# the password is asked for in the terminal, or read from stdin
notusbase superuser update [email protected]

notusbase superuser upsert

Creates, or updates if email exists, a single superuser.

The superuser is who opens the dashboard at /_/. It goes into the database in pb_data, which the server creates on its first start: run this in the project's folder, or in one under it, or point --dir at the server's pb_data. The other superuser commands find the folder the same way.

Without --dir or NOTUSBASE_DIR it uses the dir of notusbase.json, in this folder or in one above (saying which file when it is above), or else pb_data here. A folder with no data.db is refused, with its full path: start the server there first (notusbase dev or serve), or pass the project's folder.

notusbase superuser upsert EMAIL [PASSWORD]
# creates the owner of the dashboard, or changes the password; it asks for the password
notusbase superuser upsert [email protected]
notusbase superuser upsert [email protected] --dir /var/lib/notusbase/pb_data

notusbase user

Accounts in the login collection.

notusbase user add

Creates an account in the login collection, even with signup closed. The password comes from the terminal or stdin.

Creates an account for someone who signs in to your system, in the login collection (users), already verified. It works with sign-up closed, which is how notusbase serve starts. The server has to be up.

The password is never an argument. In a terminal it is asked for twice, without echo; otherwise it is read from stdin, one line. It prints account EMAIL created in COLLECTION.

It is refused when the server counts no collection as login (start it with NOTUSBASE_AUTH_COLLECTIONS=users; notusbase dev already does) and when the account exists: the password is then changed in the dashboard. The owner of the server is another thing, notusbase superuser.

notusbase user add <email> [flags]
# asks for the password
notusbase user add [email protected]
# the password from a file, one line
notusbase user add [email protected] < password.txt
OptionDefaultWhat it does
--keydeploy key
--toa destination named in the remotes of notusbase.json, such as production: opens an SSH tunnel to it, reads the deploy key there, uses both and closes. Nothing is stored here
--urlhttp://127.0.0.1:8090address of the server this command talks to

notusbase vacuum

Compacts the database, with the server stopped. Search is rebuilt at startup.

Compacts the database of a pb_data; from then on the running server gives deleted space back by itself. Stop the server first. Search is rebuilt when it starts.

Without --dir or NOTUSBASE_DIR it uses the dir of notusbase.json, in this folder or in one above (saying which file when it is above), or else pb_data here. A folder with no data.db is refused, with its full path: start the server there first (notusbase dev or serve), or pass the project's folder.

notusbase vacuum [flags]
notusbase vacuum
notusbase vacuum --dir /var/lib/notusbase/pb_data

notusbase version

Shows the binary version.

Prints one line, notusbase VERSION, PocketBase VERSION. It needs no project and no server.

notusbase version [flags]
notusbase version
OptionDefaultWhat it does
--jsonthe answer as JSON

Functions

What a function receives and what it imports. The API is Convex's: the convex package of the project gives the types and the client, and the server answers the same calls. Each table lists the names the running code has; "In" says which kind of function has it. Where the behavior differs from Convex, Migrating from Convex says how, and Limits per function has the ceilings.

The context: ctx

The first argument of every handler.

NameInWhat it does
authquery, mutation, actionWho is calling. See ctx.auth
dbquery, mutationThe database. Reads in a query, reads and writes in a mutation. An action has no db: it calls runQuery and runMutation
metaquery, mutation, actionAbout the call itself: which function, and how much of each limit it has used. See ctx.meta
runQueryquery, mutation, actionrunQuery(ref, args) calls a query. In a query or a mutation it runs inside the same transaction and its reads count toward the caller's limits. In an action it is a transaction of its own
runMutationmutation, actionrunMutation(ref, args) calls a mutation. In a mutation it runs inside the same transaction: if the outer one fails, nothing of it stays. In an action it is a transaction of its own
runActionactionrunAction(ref, args) calls another action and waits for it
schedulermutation, actionRuns a function later. See ctx.scheduler
storagequery, mutation, actionFiles. See ctx.storage
vectorSearchactionvectorSearch(table, index, { vector, limit, filter }) returns the _id and _score of the nearest documents, 10 by default. It is a linear scan of what passes the filter

ctx.db

Every method that takes an id also takes the table first, as in the newer versions of Convex: get(id) or get("tasks", id). The second form fails when the id is from another table.

NameInWhat it does
getquery, mutationThe document with this id, or null
queryquery, mutationquery(table) starts a read. See ctx.db.query(table)
insertmutationinsert(table, doc) creates the document and returns its id. The document is checked against the schema
patchmutationpatch(id, fields) changes the given fields and keeps the others. A field set to undefined is removed
replacemutationreplace(id, doc) swaps the whole document, keeping _id and _creationTime
deletemutationdelete(id) removes the document
normalizeIdquery, mutationnormalizeId(table, text) returns the id when the text is an id of that table, and null otherwise. It does not say that the document exists
systemquery, mutationThe system tables, read only. See ctx.db.system
tablequery, mutationtable(name) returns the same methods already tied to one table: get(id), query() and, in a mutation, insert(doc), patch(id, fields), replace(id, doc), delete(id)

ctx.db.query(table)

A read is a chain: where to read from (withIndex, withSearchIndex or the whole table), then filter and order, and one method that ends it. Nothing is read until the last one. The chain can also be consumed with for await.

NameWhat it does
withIndexwithIndex(name, (q) => ...) reads through an index, in the index order. Without the second argument it reads the whole index. by_creation_time and by_id always exist
withSearchIndexwithSearchIndex(name, (q) => q.search(field, text)) reads through a search index, best match first
fullTableScanSays that reading the whole table is intended. The same as using no index
filterfilter((q) => ...) drops the documents that do not pass. It does not replace an index: every document is still read
orderorder("asc") or order("desc"). The default is ascending
collectEnds the chain: every document, in an array. On a table that grows, use take or paginate
takeEnds the chain: take(n) returns at most n documents
firstEnds the chain: the first document, or null
uniqueEnds the chain: the only document, or null. Fails when there is more than one
paginateEnds the chain: paginate({ numItems, cursor }) returns { page, isDone, continueCursor }. The first call sends cursor: null
countEnds the chain: how many documents. It reads all of them, so it costs what collect() costs. It is not in the public types of the convex package, so TypeScript asks for a cast

The index range: withIndex(name, (q) => ...)

The q of withIndex. The fields come in the order of the index: zero or more eq, then at most one lower and one upper bound on the next field.

NameWhat it does
eqeq(field, value): the field equals the value
gtgt(field, value): greater than
gtegte(field, value): greater than or equal
ltlt(field, value): less than
ltelte(field, value): less than or equal

The search filter: withSearchIndex(name, (q) => ...)

The q of withSearchIndex. One search, then eq on the index's filterFields. The words are joined by OR, the last one also matches as a prefix, case and accents do not count, and up to 16 words count. As in Convex, a search reads its 1,024 best results, and .filter() runs on those: asking for more outside paginate (a take above what they give, a collect, or a for await that goes past them) fails with "Search query scanned too many documents (fetched 1024)", and the pages end there. A subscription to a search runs again on any write to a row its eq filters select, before or after the write, whether or not the row has the words: wider than Convex, which runs it again only for a row that matches the search.

NameWhat it does
searchsearch(field, text): the words to look for, in the index's searchField
eqeq(field, value): on a field listed in filterFields

The filter: .filter((q) => ...)

The q of filter. Every method takes values or other expressions and returns an expression.

NameWhat it does
fieldfield(name): the value of a field of the document
eqEqual
neqNot equal
gtGreater than
gteGreater than or equal
ltLess than
lteLess than or equal
andEvery argument is true
orSome argument is true
notThe argument is false
addSum
subDifference
mulProduct
divQuotient
modRemainder
negSign changed

ctx.db.system

Two tables: _scheduled_functions, with what ctx.scheduler created, and _storage, with the files. Read only.

NameWhat it does
getThe system document with this id, or null
queryquery("_scheduled_functions") or query("_storage"), with the same chain as ctx.db.query
normalizeIdThe same as ctx.db.normalizeId, for a system table
tabletable(name) returns get(id) and query() tied to one system table

ctx.auth

NameInWhat it does
getUserIdentityquery, mutation, actionThe identity of the caller, or null when the call has no valid token. subject is the id of the account. The fields are in the rules

ctx.scheduler

In a mutation the job is part of the transaction: if the mutation fails, nothing was scheduled.

NameInWhat it does
runAftermutation, actionrunAfter(ms, ref, args) runs the function after that many milliseconds and returns the id of the job. 0 means right after this transaction
runAtmutation, actionrunAt(when, ref, args) runs it at a time, given in milliseconds since 1970 or as a Date
cancelmutation, actioncancel(id) cancels a job that has not started

ctx.storage

NameInWhat it does
generateUploadUrlmutation, actionAn address that accepts one upload by POST and answers { storageId }
getUrlquery, mutation, actiongetUrl(storageId) returns the address that serves the file, or null when the file does not exist
getMetadataquery, mutation, actiongetMetadata(storageId) returns { storageId, sha256, size, contentType }, or null
getmutation, actionget(storageId) returns the file as a Blob, or null
storemutation, actionstore(blob) saves a file and returns its storageId
deletemutation, actiondelete(storageId) removes the file

ctx.meta

NameInWhat it does
getFunctionMetadataquery, mutation, action{ name, type, visibility, componentPath } of the function that is running
getDeploymentMetadataquery, mutation, actionAlways { class: "self-hosted", name: "notusbase", region: null }
getTransactionMetricsquery, mutationHow much of each limit this call has used and how much is left, with Convex's keys. See Limits per function
getSnapshotTsquery, mutationThe time of the snapshot this transaction reads, as a bigint
getRequestMetadatamutation, actionExists for the types to match. Every field comes empty: { ip: null, userAgent: null, requestId: "", scheduledFunctionId: null }

convex/server

What import ... from "convex/server" gives inside convex/. The server answers this import itself. The functions themselves (query, mutation, action, the internal ones and httpAction) are imported from ./_generated/server, which notusbase dev writes with the types of your schema.

NameWhat it does
defineSchemadefineSchema({ table: defineTable(...) }), the default export of convex/schema.ts
defineTabledefineTable({ field: v.string() }). See defineTable(...)
cronJobsStarts the list of convex/crons.ts. See cronJobs()
httpRouterStarts the routes of convex/http.ts. See httpRouter()
HttpRouterThe class httpRouter() returns
ROUTABLE_HTTP_METHODSThe methods a route accepts: GET, POST, PUT, DELETE, OPTIONS, PATCH
queryGenericWhat query of ./_generated/server is, without the types of the schema
mutationGenericThe same, for mutation
actionGenericThe same, for action
internalQueryGenericThe same, for internalQuery: callable from other functions, the scheduler and crons, never from a client
internalMutationGenericThe same, for internalMutation
internalActionGenericThe same, for internalAction
httpActionGenericThe same, for httpAction: (ctx, request) => Response
paginationOptsValidatorThe validator of the argument paginate takes: args: { paginationOpts: paginationOptsValidator }
paginationResultValidatorpaginationResultValidator(item): the validator of what paginate returns, for returns:
docValidatordocValidator(table, definition): the validator of a whole document of a table, with _id and _creationTime
anyApiWhat api and internal of ./_generated/api are: api.file.name is a reference to the function file:name
makeFunctionReferencemakeFunctionReference("file:name"): a reference from the name
getFunctionNameThe file:name of a reference
getFunctionAddress{ name } of a reference, { functionHandle } of a handle
filterApiReturns the api it receives. It only narrows the type
SearchFilterOnly a type name
componentsGenericWhat components in ./_generated/api is: components.<name>.<file>.<function> names a function of a component installed in convex/convex.config.ts
defineAppFor convex/convex.config.ts, which installs components with app.use(...). The deploy reads that file without running it; called from a function it fails with defineApp does not exist in NotusBase
defineComponentFor a component's own convex.config.ts, read in the same way
createFunctionHandleawait createFunctionHandle(ref) gives a string that names the function, to keep in a document or hand to a component. ctx.runQuery, ctx.runMutation, ctx.runAction and ctx.scheduler take it in place of the reference. The string is function:// and the path of the function, so it can be guessed: never run a handle that came from a client. A handle names internal functions too, and from outside the server it names nothing
currentSystemUdfInComponentNot supported, in the same way
getServiceTokenNot supported, in the same way
getServiceUrlNot supported, in the same way
loglog.audit is not supported, in the same way

defineTable(...)

Each method returns the table, so they chain. The server creates tables, columns and indexes at startup and on deploy, and never drops a table or a column.

NameWhat it does
indexindex(name, [fields]): an index on those fields, in that order. A deploy that adds one to a table with rows builds it at the code swap, in a single SQLite statement, and meanwhile writes and subscription updates wait (reads do not): on an Apple M4, about 0.6 s for a million short rows, 7 s for a million rows of 1 KB, 35 s for 10 million short rows and 90 s for 10 million of 1 KB. deploy --plan gives the rows and an estimate at that rate (a slower machine takes longer), and past 5 s it warns that writes stop that long, so the deploy can go at a quiet hour
searchIndexsearchIndex(name, { searchField, filterFields }): a full-text index (SQLite FTS5) on one text field. The deploy builds it from the rows already there, and builds it again when searchField changes under the same name; meanwhile writes wait (about 1.4 s per million short rows on an Apple M4), and deploy --plan gives the rows and an estimate at that rate. A rebuild that fails leaves the old index; a deploy that fails after it rebuilds the old one again, so writes wait twice. The rebuild is one transaction: the backup and the replica see the whole index at once. Each write keeps it in the same transaction; a write that leaves the field alone costs nothing extra. An optional field is indexed by its text, as a required one. An index built by a binary before 0.2 (which indexed the stored JSON of an optional field) is rebuilt once, at the first start of the new binary, before it serves
vectorIndexvectorIndex(name, { vectorField, dimensions, filterFields }): what ctx.vectorSearch reads

cronJobs()

convex/crons.ts exports the list as default. Every method takes a name, unique in the file, the schedule, the function and its arguments. Times are UTC.

NameWhat it does
intervalinterval(name, { seconds | minutes | hours }, ref, args): every so often. The three add up
croncron(name, "0 9 * * 1-5", ref, args): a cron expression of five fields
hourlyhourly(name, { minuteUTC }, ref, args). Here and below minuteUTC is optional: without it the cron gets a minute of its own, always the same, so that crons of the same hour do not fire together
dailydaily(name, { hourUTC, minuteUTC }, ref, args)
weeklyweekly(name, { dayOfWeek, hourUTC, minuteUTC }, ref, args), with the day as "monday"
monthlymonthly(name, { day, hourUTC, minuteUTC }, ref, args)

httpRouter()

convex/http.ts exports the router as default. The routes answer on the public port, at the root.

NameWhat it does
routeroute({ path, method, handler }) for one exact path, or route({ pathPrefix, method, handler }) for everything under a prefix. The handler is an httpAction

convex/values

This import is the convex package of the project, unchanged. The list is from the version the tests use, 1.49.0.

NameWhat it does
vThe validators. See v
ConvexErrorthrow new ConvexError(data): the error the client receives whole, in error.data. Any other error reaches the client as Server Error
convexToJsonA value in the JSON form that travels on the wire
jsonToConvexThe way back
compareValuesCompares two values in the order the database sorts them
getConvexSizeThe size of a value in bytes, as the limits count it
getDocumentSizeThe same, for a whole document
asObjectValidatorTurns { field: v.string() } into v.object({ field: v.string() })
Base64Base64.fromByteArray and Base64.toByteArray
CommitTsPlaceholderThe value that goes with v.commitTs(), which the server does not accept

Validators: v

Used in args:, in returns: and in the fields of defineTable. A value that does not match stops the call with ArgumentValidationError or ReturnsValidationError, and stops a write with the name of the field.

NameWhat it does
stringA string
numberA number, 64-bit floating point
float64The same as v.number()
booleantrue or false
nullnull
int64A bigint, in 64 bits
bigintThe same as v.int64()
bytesAn ArrayBuffer
idv.id("table"): an id of that table. The check is on the form, not on whether the document exists
literalv.literal("open"): exactly that value
arrayv.array(item)
objectv.object({ field: validator }): those fields and no others
recordv.record(keys, values): an object with any keys of one type, all with values of one type
unionv.union(a, b): any one of them
optionalv.optional(x): the field may be missing. Only for a field of an object
nullablev.nullable(x): the same as v.union(x, v.null())
anyAnything
commitTsFrom the convex package. The server has no rule for it: every value is refused
functionFrom the convex package. The server has no rule for it: every value is refused

Examples that run

Each block below is a function from notusbase/reference/examples.ts. The test suite deploys the file on a real engine and, for each example, calls seed and then the function with {}, and compares what comes back with the // => line. An example that stops working fails the suite. All of them use this schema and this beginning of file:

// convex/schema.ts
export default defineSchema({
  things: defineTable({ n: v.number(), text: v.string(), tags: v.optional(v.record(v.string(), v.number())) })
    .index("by_n", ["n"]).searchIndex("by_text", { searchField: "text", filterFields: ["n"] }),
});
// convex/examples.ts
import { paginationOptsValidator } from "convex/server";
import { ConvexError, v } from "convex/values";
import { api, internal } from "./_generated/api";
import { action, internalMutation, mutation, query } from "./_generated/server";

export const seed = mutation({
  args: {},
  handler: async (ctx) => {
    for (const thing of await ctx.db.query("things").collect()) await ctx.db.delete(thing._id);
    await ctx.db.insert("things", { n: 1, text: "a red apple" });
    await ctx.db.insert("things", { n: 2, text: "a green apple", tags: { fresh: 1 } });
    await ctx.db.insert("things", { n: 3, text: "a blue door" });
  },
});

export const sweep = internalMutation({
  args: { below: v.number() },
  handler: async (ctx, { below }) => {
    const old = await ctx.db.query("things").withIndex("by_n", (q) => q.lt("n", below)).take(100);
    for (const thing of old) await ctx.db.delete(thing._id);
    return old.length;
  },
});

Reading through an index

The range goes inside withIndex, so only the documents in it are read. order and take come after.

export const fromTwo = query({
  args: {},
  handler: async (ctx) => {
    const things = await ctx.db.query("things").withIndex("by_n", (q) => q.gte("n", 2)).order("desc").take(10);
    return things.map((thing) => thing.n);
  },
});
// => [3, 2]

first, unique and filter

first and unique return null when nothing matches. filter runs on each document the index gave.

export const one = query({
  args: {},
  handler: async (ctx) => {
    const lowest = await ctx.db.query("things").withIndex("by_n").first();
    const two = await ctx.db.query("things").withIndex("by_n", (q) => q.eq("n", 2)).unique();
    const none = await ctx.db.query("things").withIndex("by_n", (q) => q.eq("n", 9)).unique();
    const doors = await ctx.db.query("things").filter((q) => q.eq(q.field("text"), "a blue door")).collect();
    return { lowest: lowest?.n, two: two?.text, none, doors: doors.length };
  },
});
// => {"lowest": 1, "two": "a green apple", "none": null, "doors": 1}

Pages

One paginate per query. The client sends cursor: null first and then the continueCursor it received; usePaginatedQuery does this by itself.

export const page = query({
  args: { paginationOpts: paginationOptsValidator },
  handler: async (ctx, { paginationOpts }) => {
    return await ctx.db.query("things").withIndex("by_n").paginate(paginationOpts);
  },
});

export const twoPages = action({
  args: {},
  handler: async (ctx): Promise<{ first: number[]; firstIsDone: boolean; second: number[]; secondIsDone: boolean }> => {
    const first = await ctx.runQuery(api.examples.page, { paginationOpts: { numItems: 2, cursor: null } });
    const second = await ctx.runQuery(api.examples.page, { paginationOpts: { numItems: 2, cursor: first.continueCursor } });
    return { first: first.page.map((t) => t.n), firstIsDone: first.isDone, second: second.page.map((t) => t.n), secondIsDone: second.isDone };
  },
});
// => {"first": [1, 2], "firstIsDone": false, "second": [3], "secondIsDone": true}

The index is declared with searchIndex in the schema. The result comes with the best match first.

export const apples = query({
  args: {},
  handler: async (ctx) => {
    const found = await ctx.db.query("things").withSearchIndex("by_text", (q) => q.search("text", "apple")).take(10);
    return found.map((thing) => thing.n).sort();
  },
});
// => [1, 2]

Text search with a filter and a limit

eq narrows to a field listed in filterFields, and take stops at the limit.

export const greenApple = query({
  args: {},
  handler: async (ctx) => {
    const found = await ctx.db.query("things").withSearchIndex("by_text", (q) => q.search("text", "apple").eq("n", 2)).take(1);
    return found.map((thing) => thing.text);
  },
});
// => ["a green apple"]

Ids

An id is a string that starts with the name of its table. normalizeId checks a string that came from outside.

export const ids = query({
  args: {},
  handler: async (ctx) => {
    const thing = (await ctx.db.query("things").first())!;
    return {
      startsWithTable: thing._id.startsWith("things:"),
      same: ctx.db.normalizeId("things", thing._id) === thing._id,
      otherTable: ctx.db.normalizeId("things", "users:abc"),
      byTable: (await ctx.db.get("things", thing._id))?.n,
    };
  },
});
// => {"startsWithTable": true, "same": true, "otherTable": null, "byTable": 1}

patch, v.record and returns

patch keeps the fields it does not name. returns is checked like args: a value outside it fails the call.

export const tag = mutation({
  args: {},
  returns: v.object({ tags: v.record(v.string(), v.number()), text: v.string(), hasN: v.boolean() }),
  handler: async (ctx) => {
    const thing = (await ctx.db.query("things").withIndex("by_n", (q) => q.eq("n", 2)).unique())!;
    await ctx.db.patch(thing._id, { tags: { ...thing.tags, ripe: 2 } });
    const after = (await ctx.db.get(thing._id))!;
    return { tags: after.tags!, text: after.text, hasN: "n" in after };
  },
});
// => {"tags": {"fresh": 1, "ripe": 2}, "text": "a green apple", "hasN": true}

An error the client can read

Only ConvexError carries data to the client. Here the function catches its own error to show what the client gets in error.data.

export const refuse = query({
  args: {},
  handler: async () => {
    try {
      throw new ConvexError({ code: "NOT_ALLOWED", reason: "not a member" });
    } catch (error) {
      return error instanceof ConvexError ? error.data : null;
    }
  },
});
// => {"code": "NOT_ALLOWED", "reason": "not a member"}

Running later

The job is created in the same transaction as the write. ctx.db.system shows it waiting.

export const later = mutation({
  args: {},
  handler: async (ctx): Promise<{ name: string; args: unknown[]; state: string }> => {
    const id = await ctx.scheduler.runAfter(60_000, internal.examples.sweep, { below: 2 });
    const job = (await ctx.db.system.get(id))!;
    return { name: job.name, args: job.args, state: job.state.kind };
  },
});
// => {"name": "examples.js:sweep", "args": [{"below": 2}], "state": "pending"}

Calling one function from another

Inside a mutation, runMutation and runQuery are part of the same transaction.

export const sweepNow = mutation({
  args: {},
  handler: async (ctx) => {
    const removed: number = await ctx.runMutation(internal.examples.sweep, { below: 3 });
    const left: number[] = await ctx.runQuery(api.examples.fromTwo, {});
    return { removed, left };
  },
});
// => {"removed": 2, "left": [3]}

An action

An action has no ctx.db: it reads and writes through other functions, each in a transaction of its own.

export const fromOutside = action({
  args: {},
  handler: async (ctx): Promise<number[]> => {
    await ctx.runMutation(internal.examples.sweep, { below: 2 });
    return await ctx.runQuery(api.examples.fromTwo, {});
  },
});
// => [3, 2]

Crons

A convex/crons.ts with a default export. After the deploy, the test finds both names in the server's cron table.

import { cronJobs } from "convex/server";
import { internal } from "./_generated/api";

const crons = cronJobs();
crons.interval("every five minutes", { minutes: 5 }, internal.examples.sweep, { below: 0 });
crons.cron("weekdays at nine", "0 9 * * 1-5", internal.examples.sweep, { below: 0 });
export default crons;

Modules

Modules ship inside the binary and enter a project in three steps: the tables in convex/schema.ts (import { xTables } from "notusbase-modules/x/schema"), a file convex/x.ts with export * from "notusbase-modules/x", and the route in convex/http.ts when the module has one. A module that is not enabled creates no table and does not enter the bundle.

The lists are the export lines of each module, with the comment the source has above each one. How to use a module, with examples, is in its README.

certificates

Client certificate (A1) for banks and government APIs: fetch with mutual TLS and XML signatures. A library: no tables, no functions. README.

email

Transactional email written in the mutation's transaction and sent through the queue, by the panel's SMTP or Resend; under notusbase dev it is captured in the table. Templates with typed variables, a shared layout and a preview in the panel. README.

queue

Queue with a parallelism limit, retries with growing backoff and a notification on completion. README.

Test kit

kit.mjs is one file of plain Node, copied into the project by notusbase init. It starts a server on a disposable folder, deploys the project and gives the test a few commands. The list is the export lines of the file and what each one returns, with the comments of the source. How to write a test with it is in the kit README.

kitVersion

The notusbase version that wrote this file. Empty inside the notusbase repository, where the kit is used in place. The kit warns once when the binary it runs is another version.

sleep(ms)

Waits ms milliseconds.

waitUntil(check, what, ms = 5000)

Repeats check every 10 ms until it returns true, or fails saying what did not arrive. Use it after waitJobs and for anything a subscription delivers: a client.query right after a job may still answer with the result the client holds.

freePort()

A free port on 127.0.0.1, chosen by the system.

repoRoot(dir)

The root of the repository dir is in (where .git is), or dir itself.

findNotusBase()

The binary: the NOTUSBASE variable, or notusbase on the PATH.

duration(text)

Milliseconds from "2h", "1h30m", "500ms", "3d", "1w" or a number of milliseconds.

serverAt({ url, dataDir, notusbase = findNotusBase(), env = {}, cwd, log } = {})

Commands and routes of a running server, with the deploy key read from pb_data. Also works for a server another script started, such as the one from npm start.

Returns an object with:

start({ notusbase = findNotusBase(), dataDir, port, env = {}, origins = [], log, cwd, timeout = 20000, dev = false, echo = false } = {})

Starts notusbase serve and waits for it to respond. By default with login on the users collection and the WAL sync every 1 s. With dev, starts notusbase dev in the cwd folder: it deploys on start, redeploys on every saved file and accepts any origin. stop() stops the server and waits for it to exit.

Returns an object with:

setup({ project, name, dir, port, notusbase, server = {}, env = {}, user, skipTypecheck = false } = {})

Test environment: a throwaway folder, a server on a free port, the project deployed and a signed-in user.

Returns an object with:

startApp(o)

Development start: server, functions, variables and the Vite screen, until the screen exits or Ctrl-C arrives.

notusbase.json

In the project folder, next to convex/. notusbase init writes it, and every command run from that folder reads it, so that the options are not repeated. An option on the command line wins over the file. The file goes into git, and for that reason it has no deploy key. A key that is not in this table stops the command, naming it.

KeyDefaultWhat it is
urlhttp://127.0.0.1:8090Where notusbase dev and notusbase serve listen, and the server the other commands talk to. Only a local address (127.0.0.1, localhost, ::1): a remote server goes through --url or NOTUSBASE_URL
dirpb_dataThe data folder: where the server keeps the database, and where the other commands find the deploy key. The same as --dir
remotesServers by name, for --to: "remotes": { "production": { "ssh": "[email protected]" } }. The command opens an SSH tunnel to the administration port there, reads the deploy key there, uses it and closes. admin (default 127.0.0.1:8091, local addresses only) and dir (default /var/lib/notusbase/pb_data) are optional
maskWhat the mask of notusbase copy replaces or leaves besides the fields the name catches: "fields": { "table.field": "email" } with the kinds email, phone, name, document (CPF or CNPJ), text and number ("*" for any table), and "ignore": ["table.field"]
tenantThe cross-tenant leak warning of notusbase dev: field (default tenantId), off to turn it off, and allow, the public functions that return several customers on purpose

Environment variables

These are all the environment variables the code reads: the binary, PocketBase, the operations scripts, the CLI and the MCP server. They tell an operator what is in effect without reading Go.

The server reads the environment only at startup: after changing notusbase.env, run systemctl restart notusbase. The file follows the systemd format, not the shell's: one KEY=value per line, spaces inside a value are kept without quotes (NOTUSBASE_ADMIN_HOSTS=a.com, localhost), surrounding quotes are dropped, and nothing is executed. update.sh and doctor.sh read it the same way, so do not load it with source. doctor.sh warns when the running process started with a different notusbase.env.

"Secret: yes" means the value never appears in logs, script output or reports. doctor.sh only says whether it is set.

The process.env seen by your functions is a different thing: it comes from the _env table, edited in the dashboard or with notusbase env, not from these variables.

Starting the server

Set these in the package's notusbase.env, or in the environment of the compose file. The systemd unit passes NOTUSBASE_HTTP and NOTUSBASE_ORIGINS to the command line, and the server reads NOTUSBASE_DIR itself.

VariableDefaultSecretWhat it does
NOTUSBASE_DIRpb_data in the folder where the command runsThe data folder, the default value of --dir: database, published code, files and .notusbase_secret. The server, the commands, the MCP server and the scripts read the same name. --dir on the command line wins.
NOTUSBASE_HTTPnonePublic port, passed as --http.
NOTUSBASE_ADMIN_HTTPempty: one port onlyAdmin port, the default value of --admin-http: dashboard, deploy, metrics and SQL.
NOTUSBASE_FLAGSemptyExtra serve flags in the unit, such as --behind-proxy.
NOTUSBASE_ENVdevelopmentThe server's environment, the same as --env: production, staging or development. The server reads only the _env rows of its environment and the shared ones. Only production sends email and lets fetch go anywhere; the others, and a server without the name, capture email in the log and reach only NOTUSBASE_FETCH_HOSTS, with crons on. Any other name stops every command. Without the name, outside notusbase dev, a server that already has _env rows or SMTP on refuses to start. The first install needs install.sh --env; on an install from before environments, install.sh and update.sh add NOTUSBASE_ENV=production.
NOTUSBASE_ORIGINSnonePassed as --origins: origins accepted for CORS and WebSocket, comma-separated, with scheme.
PB_WAL_SYNC_MS1000WAL sync every N ms; startup logs the value. 0 turns it off, and then the maximum loss of about 1 s no longer holds. A value that is not a number stops startup.
NOTUSBASE_AUTH_COLLECTIONSempty; users in notusbase devPocketBase auth collections whose token counts as an identity. At most one.
NOTUSBASE_SIGNUPclosed; open in notusbase dev1 opens signup in users. In notusbase dev, 0 closes it.
NOTUSBASE_PUBLIC_URLlisten addressThe server's public address behind a proxy: builds the file URLs and is the value of CONVEX_SITE_URL and CONVEX_CLOUD_URL in the functions. It used to be NOTUSBASE_URL on the server, still read for one version, with a warning.
NOTUSBASE_HOSTSempty: any hostHosts accepted on the public port, comma-separated. A host outside the list gets 421. See First server.
NOTUSBASE_ADMIN_HOSTSempty: any hostThe same for the admin port. Include localhost, or the tunnel, doctor.sh, update.sh and backup-check get 421. Both scripts send Host: localhost; without localhost in the list, update.sh stops before replacing the binary.
NOTUSBASE_IP_HEADERX-Forwarded-ForHeader that carries the real IP, used with --behind-proxy.
NOTUSBASE_DOMAINnone: no HTTPS of its ownThe name, or names separated by commas, the server answers to with its own HTTPS: the binary gets and renews the certificate from Let's Encrypt (TLS-ALPN-01 on the HTTPS port, or HTTP-01 on NOTUSBASE_HTTP), with no proxy in front. It becomes the domain arguments of serve and --https. Requires NOTUSBASE_ADMIN_HTTP on loopback and --origins, and refuses --behind-proxy. install.sh asks for it; later the dashboard's Domain screen changes it: it checks the name, writes it to setup.env and restarts. From setup.env the binary brings the ports a domain needs, fixed values only: --http on 0.0.0.0:80, the dashboard on 127.0.0.1:8091 when notusbase.env names none, and no --behind-proxy. A name in setup.env that is not valid never stops the start: the binary comes up without it, with the ports of notusbase.env, and the screen and doctor.sh say why. The certificate's names always pass NOTUSBASE_HOSTS. See First server.
NOTUSBASE_ACME_EMAILemptyThe email Let's Encrypt writes to about the certificate. Optional.
NOTUSBASE_SETUPclosedThe state of the setup page, in setup.env. pending, which install.sh writes there on the first install, opens the page: each start writes a single-use link, valid for 30 minutes, to .setup_link in the data folder (0600) under systemd, or to the log when started by hand. The page writes done when it finishes. Anything else, or nothing, keeps it closed: a server installed before the page has no setup.env. doctor.sh warns while it is open.
NOTUSBASE_DOMAIN_PREVIOUSnone: nothing to go back toThe domain before the last change on the Domain screen, which "Back to the earlier one" uses. Empty is no domain. Only in setup.env, written by the binary.
NOTUSBASE_SETUP_FILEnone: no setup pageThe setup.env the setup page writes, /var/lib/notusbase/setup.env, set by the systemd unit. The binary reads it itself when it starts, over the environment notusbase.env gave, so a name there wins, --origins included; systemd never reads it, so no command of the unit that runs as root gets its lines. It belongs to the notusbase user, mode 0600, and holds only the names the page sets (origins, login collection, signup, S3, NOTUSBASE_SETUP) and those of the Domain screen (NOTUSBASE_DOMAIN, NOTUSBASE_DOMAIN_PREVIOUS, NOTUSBASE_ACME_EMAIL); notusbase.env stays root's. Anything but a regular file owned by the user the binary runs as (a link, a FIFO), or a line outside the shape the page writes (NAME=value, or 'value' with spaces; no double quote, backslash or control character), stops the binary at startup; doctor.sh says which, and install.sh refuses a link.

Local backup

See Backup.

VariableDefaultSecretWhat it does
NOTUSBASE_BACKUPempty: no backupFolder for the chains. On a separate disk, it also goes in ReadWritePaths, in a drop-in (sudo systemctl edit notusbase): every install replaces the unit.
NOTUSBASE_BACKUP_CHECK_HOURS24Interval of the automatic check. 0 takes it off the primary, when the standby does the checking.
NOTUSBASE_BACKUP_CHECK_DIRNOTUSBASE_BACKUPWhere the check copies the whole database, and where restore-check restores. Needs free space equal to the database size (twice that with S3 in restore-check).
NOTUSBASE_RESTORE_CHECK_HOURS168Interval of the automatic restore-check, counted from the last attempt. 0 turns the schedule off; the command and the button still run.
NOTUSBASE_BACKUP_CHECK_MBS64Read rate of the check.
NOTUSBASE_BACKUP_CHECK_WAL_MB1024WAL size at which the check gives up halfway, without failing, so it does not hold back the checkpoint.

S3 and standby

See Backup to S3 and Standby.

VariableDefaultSecretWhat it does
NOTUSBASE_S3emptyoff declares that this server has no S3 on purpose: doctor.sh then warns instead of failing. Only doctor.sh reads it; the server ignores it, and it has no effect when NOTUSBASE_S3_BUCKET is set.
NOTUSBASE_S3_BUCKETempty: no S3Bucket. Turns on the upload, restore-s3 and the standby.
NOTUSBASE_S3_ENDPOINTempty: AWSProvider address, such as R2 or MinIO.
NOTUSBASE_S3_REGIONus-east-1Region.
NOTUSBASE_S3_ACCESS_KEYnoneyesCredential.
NOTUSBASE_S3_SECRET_KEYnoneyesCredential.
NOTUSBASE_S3_KEYrequired with a bucketyes32 bytes in hex or base64, used to encrypt what goes to the bucket. Without it the server does not start; losing it loses the backup.
NOTUSBASE_S3_PREFIXemptyFolder inside the bucket.
NOTUSBASE_S3_PATH_STYLEoff1 uses path-style addressing (MinIO and the like).
NOTUSBASE_S3_MBS64Disk read rate for the upload.

Alerts

The red warnings of the status screen, sent out in a single message. See Alerts without Prometheus.

VariableDefaultSecretWhat it does
NOTUSBASE_ALERT_URLempty: no webhookyesAlert webhook (ntfy, Slack, Telegram or your own receiver). May contain a token: it never goes to the log.
NOTUSBASE_ALERT_FORMATtexttext or json ({"text": ...}, for Slack and Telegram).
NOTUSBASE_ALERT_EMAILempty: no emailDestination of the alert by email, using the SMTP settings and sender from the dashboard.
NOTUSBASE_ALERT_PRIMARYemptyStandby only (notusbase replica): address of the primary, watched at /api/health, with an alert when it disappears and when it comes back. The standby also receives from it directly, and promote refuses while it answers. See Standby.
NOTUSBASE_PROMOTE_HOOKemptyStandby only: command promote runs once the new primary listens, to move the route (DNS, IP, proxy). route-cloudflare.sh in the package does it on Cloudflare. See Standby.
NOTUSBASE_PROMOTED_EPOCHSet by promote for the route hook: the new epoch. Read by the hook, not by the server.
NOTUSBASE_PROMOTED_DIRSet by promote for the route hook: the new primary's data folder. Read by the hook, not by the server.
NOTUSBASE_DB_LIMIT_GB30Agreed database size, in GB. Home → Limits, notusbase capacity, notusbase_db_limit_bytes and the capacity alerts count against it. A demo server (NOTUSBASE_DEMO) starts at 1 and, over it, refuses record writes and mutations until its reset. See Capacity.

Limits per function

Convex's limits, with its messages. The full table is in Limits per function. Only time and memory are set here; the ceilings on reads, writes, scheduling and ArrayBuffer, and the conflict tries, are fixed (item 203).

VariableDefaultSecretWhat it does
NOTUSBASE_FN_MS1000Time for a query and a mutation, and CPU time per slice of an action.
NOTUSBASE_HEAP_MB64V8 heap per isolate. 0 removes the cap.
NOTUSBASE_UPLOAD_MB1024Size of an uploaded file, and of each _storage file on import.

Engine

Concurrency and memory. The numbers are in Limits per function.

VariableDefaultSecretWhat it does
NOTUSBASE_VMScores of the machineQuery and mutation isolates.
NOTUSBASE_ACTION_ISOLATES64Action isolates of their own (module state) alive at once, from every origin. Above it a call waits up to 10 s, then gets 429; scheduled work waits.
NOTUSBASE_JOB_WORKERS64Scheduled jobs running at the same time.
NOTUSBASE_STOP_GRACE_S30On a requested stop (SIGTERM), how long to wait for calls and scheduled actions in flight. New calls get 503 meanwhile; an action keeps reaching its own server. What passes the grace is cut, as in a crash. Above 75 s it warns at start: the systemd unit and the compose file kill at 90 s.
NOTUSBASE_LOG_MEMORY_MB64Cap of that log. 0 turns it off.

Public port

Per-IP caps.

VariableDefaultSecretWhat it does
NOTUSBASE_RATE_API50Requests per second. 0 turns it off.
NOTUSBASE_RATE_SYNC10Sync connections opened per second.
NOTUSBASE_CONNS_PER_IP200Requests in flight, including sync and realtime. 0 turns it off.
NOTUSBASE_MAX_SESSIONSautomaticCap on sync sessions in the process. Automatic is the open-files limit minus a tenth, keeping at least 1,024 in reserve. Above it, the new session gets 503. A value larger than the automatic one falls back to it. 0 turns it off.

"use node" and fetch

See "use node".

VariableDefaultSecretWhat it does
NOTUSBASE_NODEnode from PATHNode 22.13 or newer, outside /home.
NOTUSBASE_NODE_PATHemptynode_modules of the modules with "use node".
NOTUSBASE_NODE_PORTS443Outbound TCP ports. Port 53 is always open; any opens all of them.
NOTUSBASE_NODE_SANDBOXonoff removes the fence and the destination filter. Emergency exit.
NOTUSBASE_FETCH_ALLOWemptyCIDRs that fetch may reach even though they are loopback, a private network or 100.64/10. Also applies to Node.
NOTUSBASE_FETCH_HOSTSempty: no destinationOutside production (NOTUSBASE_ENV), the names fetch reaches, separated by commas; *.example.com covers the subdomains. Also applies to Node and to redirects. Not read in production.
NOTUSBASE_LOCKED_ENVset by the serverInternal: the environment the server passes to the Node process outside production, so the fence filters names as the V8 fetch does. Not set by hand.
NOTUSBASE_INFLIGHTset by the serverInternal: the token the server passes to the Node process, so its fetch to this server still gets in during a requested stop. Not set by hand.
NOTUSBASE_SELFset by the serverInternal: this server's origins, passed to the Node process to know which fetch carries that token. Not set by hand.

Development and diagnostics

Not for production. doctor.sh flags the first three.

VariableDefaultSecretWhat it does
NOTUSBASE_ISOLATEDoff1 is the same as serve --isolated: no crons, jobs, _env, network, S3, backup or alerts, so a copy of production can start with no effect outside the machine. Ignores NOTUSBASE_PUBLIC_URL, NOTUSBASE_BACKUP, NOTUSBASE_S3_*, NOTUSBASE_ALERT_* and NOTUSBASE_FETCH_ALLOW.
NOTUSBASE_DEV_ERRORSoff; on in notusbase devShows the client the internal error message instead of Server Error.
NOTUSBASE_TEST_CLOCKoff1 turns on the test clock: POST /api/notusbase/test/advance adds time to the clock of the scheduler and of the functions' Date.now(). The kit's setup turns it on. The server refuses to start with it outside loopback or with --behind-proxy. Never in production.
NOTUSBASE_DEMOoff1 makes this a demonstration server, where anyone may sign in to the dashboard. _env is not read, and the server does not start as production, isolated, with the setup page pending, or with NOTUSBASE_BACKUP, NOTUSBASE_S3_*, NOTUSBASE_ALERT_*, NOTUSBASE_FETCH_ALLOW, NOTUSBASE_PROMOTE_HOOK or NOTUSBASE_NODE_SANDBOX=off. The server answers only the routes on its demo list (403 for the rest, for every caller), nothing leaves the machine (no fetch, email, alerts or bucket) while crons and jobs still run, and the limits start lower unless set: NOTUSBASE_RATE_API 20, NOTUSBASE_RATE_SYNC 5, NOTUSBASE_CONNS_PER_IP 40, NOTUSBASE_MAX_SESSIONS 200, NOTUSBASE_DB_LIMIT_GB 1, NOTUSBASE_FN_MS 500. doctor.sh says so on its first line.
NOTUSBASE_DEMO_RESET_MINnot setOn a demo server, every how many minutes the data goes back to the seed, counted on the clock (60 is every full hour, up to 1440): the strip on the dashboard then says the time of the next one. It only says it. The reset itself is a timer outside the binary, which has to run on the same period.

CLI and MCP

On the machine of whoever publishes, not on the server.

VariableDefaultSecretWhat it does
NOTUSBASE_DEPLOY_KEYnoneyesDeploy key for notusbase deploy, logs, run and the MCP server. It opens the whole server.
NOTUSBASE_URLthe url in notusbase.json, or http://127.0.0.1:8090The server that the command or the MCP server calls, the default value of --url.
NOTUSBASE_PROJECTcurrent folderProject that the MCP server reads.

npm package

On the machine of whoever installs notusbase from npm, not on the server. doctor.sh does not know them.

VariableDefaultSecretWhat it does
NOTUSBASE_CACHE_DIR.cache inside the packageFolder where the package keeps the downloaded binary.
NOTUSBASE_DEMO_URLemptyAddress of the live demo that the site's "Live demo" buttons open (ops/site.sh). Empty hides them.
NOTUSBASE_DOWNLOAD_BASEhttps://downloads.notusbase.comBase of the download: the file comes from <base>/notusbase/v<version>/. Only https, or http://127.0.0.1 in the local rehearsal.
NOTUSBASE_PLATFORMthe machine'sForces the platform (linux-x64), only to simulate an unsupported one in the rehearsal.

Operations scripts

Read by the scripts that ship with the package, not by the binary.

VariableDefaultSecretWhat it does
NOTUSBASE_CONF/etc/notusbase/notusbase.envWhere update.sh and doctor.sh read notusbase.env from. The unit sets it for doctor.sh run by the setup page.
NOTUSBASE_UNITnotusbaseName of the systemd unit.
NOTUSBASE_SYSTEMCTLsystemctlThe systemd command, such as systemctl --user in a test.
NOTUSBASE_WATCH_S15How long update.sh watches the new version stay up.
NOTUSBASE_FORCEoff1 makes update.sh update even with a late backup.
NOTUSBASE_BUILD_JOBS2Go packages compiled in parallel by package.sh.
NOTUSBASE_DISTtmp/dist of the repositoryOutput of package.sh; release.sh points it at the release folder.
NOTUSBASE_LLVMemptyAn LLVM folder (the one with bin/clang) to build with, when the system Clang is older than 21.
NOTUSBASE_RELEASE_DIRtmp/release of the repositoryWhere release.sh builds <version>/ with the packages, SHA256SUMS and NOTES.md.
NOTUSBASE_RELEASE_DIRTYempty1 accepts a tree with uncommitted changes and a HEAD outside the tag, for rehearsal only.
NOTUSBASE_IMAGEnotusbaseImage used by the compose file.
NOTUSBASE_REHEARSAL_URLset by notusbase copy isolated --rehearseAddress of the isolated copy, passed to the project's rehearse.mjs.
NOTUSBASE_REHEARSAL_DATAset by notusbase copy isolated --rehearsepb_data folder of the copy, passed to the project's rehearse.mjs.

PocketBase

Read by PocketBase itself. NotusBase sets none of them; they are listed so that a version upgrade shows a new one.

VariableDefaultSecretWhat it does
PB_THUMBS_MAX_WORKERScores + 2Thumbnails generated at the same time.
PB_THUMBS_MAX_WAIT60Seconds to wait for a thumbnail slot.
PB_THUMBS_MAX_SOURCE_PIXELS104857600Pixels of the original image above which there is no thumbnail.
PB_FILES_DELETE_MAX_WORKERS2000Files deleted at the same time after a record or collection is deleted.
PB_ID_TOKEN_LEEWAY300Clock leeway, in seconds, when validating the OIDC id_token.

Tests only

Read only in *_test.go files, or by the server as NOTUSBASE_TEST_<name>: a value that is fixed in production and that the tests and the bench still change. The server writes a warning to the log the first time it uses one. None of them is for production. They serve as the list of what the tests accept; Only the bench and the soak set six more: NOTUSBASE_TEST_NODE_MEM_MB and NOTUSBASE_TEST_NODE_STALL_S (the Node process ceilings, 1024 and 60, which the smoke test lowers), NOTUSBASE_TEST_NODE_PERMISSION (off drops --permission so the smoke test tries the kernel fence alone), NOTUSBASE_TEST_BACKUP_HOURS (the age that starts a new backup chain, 24; the soak uses 1), NOTUSBASE_TEST_PPROF (an address for /debug/pprof and /debug/vars, for the long test) and NOTUSBASE_TEST_MALLOC_TRIM_SECONDS (how often the free memory goes back to the system, 60; 0 turns it off).

VariableDefaultSecretWhat it does
NOTUSBASE_TEST_FAKE_SSHFolder of the fake ssh in the --to test: the test binary takes the place of the ssh command.
NOTUSBASE_TEST_SSH_SERVERMakes the test binary run as that fake ssh: it opens the local tunnel and runs the script on the other side.
NOTUSBASE_TEST_MAINoffMakes the test binary run as notusbase, for tests that start a process.
NOTUSBASE_TEST_EVERY_MSUpload interval in the standby test.
NOTUSBASE_TEST_SEAL_MSOpen-segment deadline in the standby test.
NOTUSBASE_TEST_HOLD_MSTurns on the standby deadlines test: wait after the standby sees a new point.
NOTUSBASE_TEST_PAUSE_MSPause between load requests in the standby test.
NOTUSBASE_TEST_LOAD_SECONDSDuration of the load in the standby test.
NOTUSBASE_TEST_SWITCHOVERoffTurns on the switchover rehearsal: kills the primary under load, promotes the standby, and reports downtime and lost writes.
NOTUSBASE_TEST_FILL_MBSize of the database before the switchover rehearsal starts.
NOTUSBASE_TEST_CHECKfullCheck of the promotion in the switchover rehearsal: full or quick.
NOTUSBASE_TEST_LOG_MBThe primary's NOTUSBASE_TEST_BACKUP_LOG_MB in the switchover rehearsal: a small one makes new chains during the load, to measure the standby's wait for the base.
NOTUSBASE_TEST_OUTFolder where the switchover rehearsal keeps each process's whole log (old primary, standby, promote, new server).
NOTUSBASE_TEST_PARTITIONoffTurns on the partition rehearsal: cuts the bucket and the standby off with proxies, freezes the primary, and reports how long it goes on confirming writes.
NOTUSBASE_TEST_PARTITION_EXPECT10 only reports, without checking: for a run on the code from before the write lease.
NOTUSBASE_TEST_LEASE_MSthree beatsThe primary's write lease with a standby, 30 s in production.
NOTUSBASE_TEST_WATCH_MSInterval of the standby's watch in the switchover rehearsal.
NOTUSBASE_TEST_S3_DISKoffKeeps the fake S3's objects on disk in the standby tests, for big databases.
NOTUSBASE_FAKE_S3_DIRKeeps the fake S3's objects in a folder.
NOTUSBASE_FAKE_S3_PORTTurns on TestServeFakeS3: a fake S3 on this port, to prove it against the real binary.
NOTUSBASE_KNOWN_BUGSoffRuns the tests of confirmed, still open defects, which fail; without it they are skipped.
NOTUSBASE_INDEX_ROWSTurns on TestIndexBuildTime: rows in the table whose index build it times.
NOTUSBASE_INDEX_BODY0Bytes of the extra text column in TestIndexBuildTime (0 for a short row).
NOTUSBASE_TEST_BUNDLEthe deployed codeReads the published code from another file instead of pb_data/.notusbase_bundle.js: the tests, the bench and the soak.
NOTUSBASE_TEST_HTTPS0.0.0.0:443The address of the server's own HTTPS with a domain, fixed in production.
NOTUSBASE_TEST_ACME_URLLet's Encrypt productionThe ACME directory of another server: the tests use a fake one.
NOTUSBASE_TEST_HTTP0.0.0.0:80The HTTP address of a domain set in the dashboard, where the check of the name listens; fixed in production.
NOTUSBASE_TEST_RESOLVEthe DNSThe address every name resolves to in the check of the domain.
NOTUSBASE_TEST_ACTION_MS600000 (10 min)The total time of an action, fixed in production.
NOTUSBASE_TEST_MAX_READS32000Documents read per call, fixed in production.
NOTUSBASE_TEST_FN_BYTES_MB1024ArrayBuffer and the wasm memory JS can reach, per query or mutation isolate, fixed in production; 0 removes the ceiling. The larger of the two is also the ceiling of one wasm memory, read at startup.
NOTUSBASE_TEST_ACTION_BYTES_MB1024ArrayBuffer and the wasm memory JS can reach, per action isolate, fixed in production; 0 removes the ceiling.
NOTUSBASE_TEST_ACTION_VMS4Shared isolates that run actions, fixed in production. The tests use 1, so a bad action and its neighbor share one.
NOTUSBASE_TEST_REACT_WORKERS4Isolates that rerun the subscribed queries, fixed in production.
NOTUSBASE_TEST_VM_WAIT_MS5000Wait for a free isolate before the 429, fixed in production.
NOTUSBASE_TEST_BATCH256Mutations per COMMIT, fixed in production.
NOTUSBASE_TEST_DEPLOY_HISTORY10Deploys kept for rollback, fixed in production.
NOTUSBASE_TEST_DEPLOY_HISTORY_MB64Space of the deploys kept for rollback, fixed in production.
NOTUSBASE_TEST_BACKUP_MS200Interval between WAL copies, fixed in production.
NOTUSBASE_TEST_BACKUP_LOG_MB8192Log size that starts a new chain, fixed in production.
NOTUSBASE_TEST_BACKUP_WAL_MAX_MB8192WAL without a chain before the checkpoint without copy, fixed in production.
NOTUSBASE_TEST_BACKUP_BASE_MBS64Pace of the base copy, fixed in production.
NOTUSBASE_TEST_BACKUP_COMPACT_S60Compaction window of closed segments, fixed in production.
NOTUSBASE_TEST_S3_EVERY_MS30000Interval between S3 rounds and, on a standby, between bucket reads; fixed in production.
NOTUSBASE_TEST_S3_SEAL_MS900000Maximum age of the open segment, fixed in production.
NOTUSBASE_TEST_ALERT_NAMEapp name and machineName in the subject and the message of an alert. In production it is the dashboard's application name and the machine (agenda on server1), or the machine and the HTTP port with the default name (server1:8090).
NOTUSBASE_TEST_ALERT_GAP_S300Minimum interval between two alert messages, fixed in production.
NOTUSBASE_TEST_ALERT_REPEAT_MIN60Repeat interval of what stays open, fixed in production.
NOTUSBASE_TEST_ALERT_PRIMARY_MS10000On a standby, interval between probes of the primary, fixed in production.
NOTUSBASE_TEST_LOGIN_MAX2Login attempts per IP every 3 seconds, fixed in production; the kit and the smoke test raise it.
NOTUSBASE_TEST_BACKUP_SAMPLE_MB256Window of each closed segment the primary sends for the standby to compare, always on in production; 0 turns it off in tests.

Not counted

These do not start with NOTUSBASE_ or PB_, or are not read by NotusBase: