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.
| Command | What it does |
|---|---|
notusbase backup-check | Restores the backup alongside and compares it with the live database |
notusbase backup-s3-status | Shows what can be restored from the bucket |
notusbase bundle | Only bundles, without deploying |
notusbase capacity | Shows the database size against the limit, the growth, the writer and the next step |
notusbase certificate | Lists the client certificates (name, subject, validity), as JSON, never the key |
notusbase certificate remove | Deletes a certificate |
notusbase certificate set | Stores a PKCS#12 certificate (A1) for fetchWithCertificate and signXml. The password comes from the terminal or stdin |
notusbase check | Checks the database integrity |
notusbase codegen | Generates the _generated folder |
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 |
notusbase cron | Lists the crons from crons.ts, with the next run and the last one |
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 |
notusbase deploy | Generates types, checks, bundles and deploys the functions; --rollback goes back to a saved deploy |
notusbase deploy-key | Shows this pb_data's deploy key |
notusbase dev | Starts the server and redeploys the functions on every change |
notusbase env | Lists the variables and the environment of each. The values only come with --values |
notusbase env list | The same as notusbase env |
notusbase env remove | Deletes a variable, in the server's environment or the one in --env |
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 |
notusbase export | Saves tables and files to a zip in the npx convex export format |
notusbase import | Loads a zip from notusbase export or npx convex export. Without --replace, the tables must be empty |
notusbase init | Create a project: schema, a query, a mutation, a test, tsconfig and package.json |
notusbase job | Lists the scheduled functions (runAfter, runAt, crons), with the id that job run takes |
notusbase job run | Brings a pending job to now, without waiting for its time. The id comes from notusbase job |
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 |
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 |
notusbase metrics-key | Shows the key that only opens the metrics |
notusbase promote | Turns the replica into the new primary and starts the server |
notusbase replica | Follows the bucket as a standby server, serving nothing |
notusbase replica-status | Shows the replica lag |
notusbase restore | Rebuilds the database, the files, the published code and the deploy key from NOTUSBASE_BACKUP |
notusbase restore-check | Restores the newest backup into a new folder, compares it with the live database and starts it isolated |
notusbase restore-s3 | Rebuilds the database and files from the bucket |
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 |
notusbase serve | Starts the web server (default to 127.0.0.1:8090 if no domain is specified) |
notusbase sql | Reads the database with SQL, read-only and with a time limit; to avoid competing for CPU, use nice |
notusbase superuser | Manage superusers |
notusbase superuser create | Creates a new superuser |
notusbase superuser delete | Deletes an existing superuser |
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 otp | Creates a new OTP for the specified superuser |
notusbase superuser update | Changes the password of a single superuser |
notusbase superuser upsert | Creates, or updates if email exists, a single superuser |
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 |
notusbase vacuum | Compacts the database, with the server stopped. Search is rebuilt at startup |
notusbase version | Shows the binary version |
Options of every command
| Option | Default | What it does |
|---|---|---|
--admin-http | port for the dashboard, deploy and metrics; --http then serves only the apps | |
--behind-proxy | accepts starting exposed, behind a reverse proxy on this machine that speaks TLS and overwrites X-Forwarded-For, and trusts the IP it reports | |
--dir | pb_data | the data folder (or NOTUSBASE_DIR) |
--encryptionEnv | the env variable whose value of 32 characters will be used as encryption key for the app settings (default none) | |
--env | production | the environment this server is: production, staging or development (or NOTUSBASE_ENV). Only production sends email and opens fetch; without it, development |
--import-limit | 262144 | ceiling of the uncompressed import, in MB |
--isolated | starts a copy without crons, jobs, network, _env, backup or alerts (or NOTUSBASE_ISOLATED=1) | |
--queryTimeout | 30 | the 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
| Option | Default | What it does |
|---|---|---|
--json | the answer as JSON | |
--key | deploy key | |
--last | only shows the last check | |
--to | a 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 | |
--url | http://127.0.0.1:8090 | address 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
| Option | Default | What it does |
|---|---|---|
--functions | convex | functions directory |
--output | bundle.js | output 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
| Option | Default | What it does |
|---|---|---|
--json | the answer as JSON | |
--key | deploy key | |
--to | a 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 | |
--url | http://127.0.0.1:8090 | address 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
| Option | Default | What it does |
|---|---|---|
--json | the answer as JSON | |
--key | deploy key | |
--to | a 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 | |
--url | http://127.0.0.1:8090 | address 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
| Option | Default | What it does |
|---|---|---|
--key | deploy key | |
--to | a 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 | |
--url | http://127.0.0.1:8090 | address 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
| Option | Default | What it does |
|---|---|---|
--key | deploy key | |
--to | a 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 | |
--url | http://127.0.0.1:8090 | address 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:
convex/_generated, the types ofapi,internaland the data model, from the modules andschema.ts;node_modules/notusbase-modules, the modules inside the binary with their pages.npm installdeletes that folder, which is why thepostinstallof a project runs this command;kit.mjs, when a notusbase wrote it and nobody edited it; the rules between the marks ofCLAUDE.md; andconvex/tsconfig.jsonwhen it is missing.
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
| Option | Default | What it does |
|---|---|---|
--functions | convex | functions 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.
development: to develop with data that looks real. The personal data is always masked, and the copy starts with the engine ofnotusbase dev(redeploys on save), with email captured and fetch only to the names inNOTUSBASE_FETCH_HOSTS. It lives in./tmp/copy-development-<date>;notusbase dev --dirstarts it again.isolated: no crons, jobs, network,_env, backup or alerts. Real data unless--mask.--functionruns the function that failed, with--argsand--as(the id of the account).--rehearserunsdeploy --plan, the deploy of this folder and the project'srehearse.mjs(withNOTUSBASE_REHEARSAL_URLandNOTUSBASE_REHEARSAL_DATA), prints a report and deletes the copy, also on failure. Real data loses exactly what staging loses (below), and is deleted at the end unless--keep.staging: the homologation, with real data unless--mask. It goes without the production rows of_env, the client certificates, the file and backup S3, SMTP and the OAuth2 client secrets, and with new token secrets; the shared rows of_envstay and are listed. It is prepared in--folder, not started: it runs as its own service withserve --env staging.--replacerenews it, building the new copy beside the old one.
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
| Option | Default | What it does |
|---|---|---|
--args | function arguments: JSON, @file or - for stdin | |
--as | runs the function as a user: the id of the account, the same as in production | |
--backup | folder of the backup chains (default: NOTUSBASE_BACKUP) | |
--exit | isolated: exits after running the function, instead of leaving the server up | |
--folder | where the copy lives (default: ./tmp/copy-<use>-<date>; staging needs one) | |
--function | isolated: function to run after starting (module:function) | |
--functions | convex | functions directory |
--keep | isolated: keeps the copy at the end, also with real data | |
--mask | replaces personal data with fake values (always in development) | |
--no-deploy | isolated: keeps the published code of the backup instead of deploying this folder's | |
--port | 0 | port of the copy (default: a free one) |
--rehearse | isolated: deploy --plan, deploy and rehearse.mjs, a report, and the copy deleted | |
--replace | staging: renews the copy in --folder, which must be one | |
--s3 | downloads from the bucket, with NOTUSBASE_S3_*, instead of the folder | |
--skip-typecheck | deploys without tsc | |
--until | instant 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
| Option | Default | What it does |
|---|---|---|
--json | the answer as JSON | |
--key | deploy key | |
--to | a 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 | |
--url | http://127.0.0.1:8090 | address 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
| Option | Default | What it does |
|---|---|---|
--json | the answer as JSON | |
--key | deploy key | |
--to | a 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 | |
--url | http://127.0.0.1:8090 | address 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:
- writes
convex/_generatedand what follows the binary in the project, asnotusbase codegendoes; - runs
tscwhen the project has it innode_modules, and stops on a type error (--skip-typecheckskips it); - bundles
convex/; - sends the bundle. The server checks the new
schema.tsagainst 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
| Option | Default | What it does |
|---|---|---|
--functions | convex | functions directory |
--history | lists the deploys saved on the server, and which ones can be rolled back to | |
--json | the answer as JSON | |
--key | deploy key | |
--plan | shows what the deploy (or the rollback) would change in the database, without deploying anything | |
--rollback | goes back to a saved deploy: the one of the number given (notusbase deploy --rollback 7) or, without a number, the previous one | |
--skip-typecheck | does not run tsc | |
--to | a 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 | |
--url | http://127.0.0.1:8090 | address of the server this command talks to |
--yes | with --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:
- the client receives the message and the stack of an error, and the
console.loglines of the call; - login with the
userscollection and open sign-up, unlessNOTUSBASE_AUTH_COLLECTIONSis set (NOTUSBASE_SIGNUP=0closes signup); - actions may
fetchthis machine (127.0.0.1 and ::1), and any origin may call the server; - no email is sent.
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
| Option | Default | What it does |
|---|---|---|
--functions | convex | functions directory |
--http | 127.0.0.1:8090 | server 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
| Option | Default | What it does |
|---|---|---|
--json | the answer as JSON | |
--key | deploy key | |
--to | a 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 | |
--url | http://127.0.0.1:8090 | address of the server this command talks to |
--values | shows 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
| Option | Default | What it does |
|---|---|---|
--json | the answer as JSON | |
--key | deploy key | |
--to | a 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 | |
--url | http://127.0.0.1:8090 | address of the server this command talks to |
--values | shows 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
| Option | Default | What it does |
|---|---|---|
--key | deploy key | |
--to | a 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 | |
--url | http://127.0.0.1:8090 | address 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
| Option | Default | What it does |
|---|---|---|
--key | deploy key | |
--to | a 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 | |
--url | http://127.0.0.1:8090 | address 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
| Option | Default | What it does |
|---|---|---|
--key | deploy key | |
--to | a 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 | |
--url | http://127.0.0.1:8090 | address 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
| Option | Default | What it does |
|---|---|---|
--key | deploy key | |
--replace | replaces what already exists in the file's tables | |
--skip | tables and components left out, comma-separated (e.g. authAccounts,_components/stripe) | |
--to | a 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 | |
--url | http://127.0.0.1:8090 | address 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
| Option | Default | What it does |
|---|---|---|
--port | 8090 | server 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
| Option | Default | What it does |
|---|---|---|
--function | only the jobs of one function | |
--json | the answer as JSON | |
--key | deploy key | |
--to | a 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 | |
--url | http://127.0.0.1:8090 | address of the server this command talks to |
--view | scheduled | which 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
| Option | Default | What it does |
|---|---|---|
--key | deploy key | |
--to | a 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 | |
--url | http://127.0.0.1:8090 | address 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'
| Option | Default | What it does |
|---|---|---|
--function | only one function (module:function) or one module | |
--json | the answer as JSON | |
--key | deploy key | |
--limit | 1000 | with --since, the last N lines |
--since | shows the past: 30m, 1h, 2d or an RFC 3339 date | |
--text | only the lines with this text; the error pasted as the client received it, [Request ID: …] Server Error, finds its line | |
--to | a 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 | |
--url | http://127.0.0.1:8090 | address 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
| Option | Default | What it does |
|---|---|---|
--accept-missing-files | promote with stored files missing | |
--check | full | full: integrity_check before serving; quick: quick_check before, integrity_check after |
--force | promote with the primary answering or a failed standby check; never without the bucket | |
--no-serve | only prepares the directory; running serve is up to you | |
--planned | after a clean stop: refuse unless its final point is here | |
--primary | the 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
| Option | Default | What it does |
|---|---|---|
--new-secret | give <new dir> a deploy key of its own and leave the backup's out: a copy, not the same server | |
--until | instant in RFC 3339; without it, goes to the end | |
--with-secret | replace 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
| Option | Default | What it does |
|---|---|---|
--json | the answer as JSON | |
--key | deploy key | |
--last | only shows the last check and when one last passed | |
--to | a 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 | |
--url | http://127.0.0.1:8090 | address 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
| Option | Default | What it does |
|---|---|---|
--new-secret | give <new dir> a deploy key of its own and leave the bucket's out: a copy, not the same server | |
--until | instant 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 -
| Option | Default | What it does |
|---|---|---|
--as | runs 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 | |
--key | deploy key | |
--to | a 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 | |
--url | http://127.0.0.1:8090 | address 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
| Option | Default | What it does |
|---|---|---|
--http | TCP 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) | |
--https | TCP 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
| Option | Default | What it does |
|---|---|---|
--max-rows | 100000 | maximum rows in the result |
--timeout | 10s | maximum 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
| Option | Default | What it does |
|---|---|---|
--key | deploy key | |
--to | a 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 | |
--url | http://127.0.0.1:8090 | address 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
| Option | Default | What it does |
|---|---|---|
--json | the 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.
| Name | In | What it does |
|---|---|---|
auth | query, mutation, action | Who is calling. See ctx.auth |
db | query, mutation | The database. Reads in a query, reads and writes in a mutation. An action has no db: it calls runQuery and runMutation |
meta | query, mutation, action | About the call itself: which function, and how much of each limit it has used. See ctx.meta |
runQuery | query, mutation, action | runQuery(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 |
runMutation | mutation, action | runMutation(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 |
runAction | action | runAction(ref, args) calls another action and waits for it |
scheduler | mutation, action | Runs a function later. See ctx.scheduler |
storage | query, mutation, action | Files. See ctx.storage |
vectorSearch | action | vectorSearch(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.
| Name | In | What it does |
|---|---|---|
get | query, mutation | The document with this id, or null |
query | query, mutation | query(table) starts a read. See ctx.db.query(table) |
insert | mutation | insert(table, doc) creates the document and returns its id. The document is checked against the schema |
patch | mutation | patch(id, fields) changes the given fields and keeps the others. A field set to undefined is removed |
replace | mutation | replace(id, doc) swaps the whole document, keeping _id and _creationTime |
delete | mutation | delete(id) removes the document |
normalizeId | query, mutation | normalizeId(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 |
system | query, mutation | The system tables, read only. See ctx.db.system |
table | query, mutation | table(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.
| Name | What it does |
|---|---|
withIndex | withIndex(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 |
withSearchIndex | withSearchIndex(name, (q) => q.search(field, text)) reads through a search index, best match first |
fullTableScan | Says that reading the whole table is intended. The same as using no index |
filter | filter((q) => ...) drops the documents that do not pass. It does not replace an index: every document is still read |
order | order("asc") or order("desc"). The default is ascending |
collect | Ends the chain: every document, in an array. On a table that grows, use take or paginate |
take | Ends the chain: take(n) returns at most n documents |
first | Ends the chain: the first document, or null |
unique | Ends the chain: the only document, or null. Fails when there is more than one |
paginate | Ends the chain: paginate({ numItems, cursor }) returns { page, isDone, continueCursor }. The first call sends cursor: null |
count | Ends 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.
| Name | What it does |
|---|---|
eq | eq(field, value): the field equals the value |
gt | gt(field, value): greater than |
gte | gte(field, value): greater than or equal |
lt | lt(field, value): less than |
lte | lte(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.
| Name | What it does |
|---|---|
search | search(field, text): the words to look for, in the index's searchField |
eq | eq(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.
| Name | What it does |
|---|---|
field | field(name): the value of a field of the document |
eq | Equal |
neq | Not equal |
gt | Greater than |
gte | Greater than or equal |
lt | Less than |
lte | Less than or equal |
and | Every argument is true |
or | Some argument is true |
not | The argument is false |
add | Sum |
sub | Difference |
mul | Product |
div | Quotient |
mod | Remainder |
neg | Sign changed |
ctx.db.system
Two tables: _scheduled_functions, with what ctx.scheduler created, and _storage, with the files. Read only.
| Name | What it does |
|---|---|
get | The system document with this id, or null |
query | query("_scheduled_functions") or query("_storage"), with the same chain as ctx.db.query |
normalizeId | The same as ctx.db.normalizeId, for a system table |
table | table(name) returns get(id) and query() tied to one system table |
ctx.auth
| Name | In | What it does |
|---|---|---|
getUserIdentity | query, mutation, action | The 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.
| Name | In | What it does |
|---|---|---|
runAfter | mutation, action | runAfter(ms, ref, args) runs the function after that many milliseconds and returns the id of the job. 0 means right after this transaction |
runAt | mutation, action | runAt(when, ref, args) runs it at a time, given in milliseconds since 1970 or as a Date |
cancel | mutation, action | cancel(id) cancels a job that has not started |
ctx.storage
| Name | In | What it does |
|---|---|---|
generateUploadUrl | mutation, action | An address that accepts one upload by POST and answers { storageId } |
getUrl | query, mutation, action | getUrl(storageId) returns the address that serves the file, or null when the file does not exist |
getMetadata | query, mutation, action | getMetadata(storageId) returns { storageId, sha256, size, contentType }, or null |
get | mutation, action | get(storageId) returns the file as a Blob, or null |
store | mutation, action | store(blob) saves a file and returns its storageId |
delete | mutation, action | delete(storageId) removes the file |
ctx.meta
| Name | In | What it does |
|---|---|---|
getFunctionMetadata | query, mutation, action | { name, type, visibility, componentPath } of the function that is running |
getDeploymentMetadata | query, mutation, action | Always { class: "self-hosted", name: "notusbase", region: null } |
getTransactionMetrics | query, mutation | How much of each limit this call has used and how much is left, with Convex's keys. See Limits per function |
getSnapshotTs | query, mutation | The time of the snapshot this transaction reads, as a bigint |
getRequestMetadata | mutation, action | Exists 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.
| Name | What it does |
|---|---|
defineSchema | defineSchema({ table: defineTable(...) }), the default export of convex/schema.ts |
defineTable | defineTable({ field: v.string() }). See defineTable(...) |
cronJobs | Starts the list of convex/crons.ts. See cronJobs() |
httpRouter | Starts the routes of convex/http.ts. See httpRouter() |
HttpRouter | The class httpRouter() returns |
ROUTABLE_HTTP_METHODS | The methods a route accepts: GET, POST, PUT, DELETE, OPTIONS, PATCH |
queryGeneric | What query of ./_generated/server is, without the types of the schema |
mutationGeneric | The same, for mutation |
actionGeneric | The same, for action |
internalQueryGeneric | The same, for internalQuery: callable from other functions, the scheduler and crons, never from a client |
internalMutationGeneric | The same, for internalMutation |
internalActionGeneric | The same, for internalAction |
httpActionGeneric | The same, for httpAction: (ctx, request) => Response |
paginationOptsValidator | The validator of the argument paginate takes: args: { paginationOpts: paginationOptsValidator } |
paginationResultValidator | paginationResultValidator(item): the validator of what paginate returns, for returns: |
docValidator | docValidator(table, definition): the validator of a whole document of a table, with _id and _creationTime |
anyApi | What api and internal of ./_generated/api are: api.file.name is a reference to the function file:name |
makeFunctionReference | makeFunctionReference("file:name"): a reference from the name |
getFunctionName | The file:name of a reference |
getFunctionAddress | { name } of a reference, { functionHandle } of a handle |
filterApi | Returns the api it receives. It only narrows the type |
SearchFilter | Only a type name |
componentsGeneric | What components in ./_generated/api is: components.<name>.<file>.<function> names a function of a component installed in convex/convex.config.ts |
defineApp | For 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 |
defineComponent | For a component's own convex.config.ts, read in the same way |
createFunctionHandle | await 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 |
currentSystemUdfInComponent | Not supported, in the same way |
getServiceToken | Not supported, in the same way |
getServiceUrl | Not supported, in the same way |
log | log.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.
| Name | What it does |
|---|---|
index | index(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 |
searchIndex | searchIndex(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 |
vectorIndex | vectorIndex(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.
| Name | What it does |
|---|---|
interval | interval(name, { seconds | minutes | hours }, ref, args): every so often. The three add up |
cron | cron(name, "0 9 * * 1-5", ref, args): a cron expression of five fields |
hourly | hourly(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 |
daily | daily(name, { hourUTC, minuteUTC }, ref, args) |
weekly | weekly(name, { dayOfWeek, hourUTC, minuteUTC }, ref, args), with the day as "monday" |
monthly | monthly(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.
| Name | What it does |
|---|---|
route | route({ 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.
| Name | What it does |
|---|---|
v | The validators. See v |
ConvexError | throw new ConvexError(data): the error the client receives whole, in error.data. Any other error reaches the client as Server Error |
convexToJson | A value in the JSON form that travels on the wire |
jsonToConvex | The way back |
compareValues | Compares two values in the order the database sorts them |
getConvexSize | The size of a value in bytes, as the limits count it |
getDocumentSize | The same, for a whole document |
asObjectValidator | Turns { field: v.string() } into v.object({ field: v.string() }) |
Base64 | Base64.fromByteArray and Base64.toByteArray |
CommitTsPlaceholder | The 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.
| Name | What it does |
|---|---|
string | A string |
number | A number, 64-bit floating point |
float64 | The same as v.number() |
boolean | true or false |
null | null |
int64 | A bigint, in 64 bits |
bigint | The same as v.int64() |
bytes | An ArrayBuffer |
id | v.id("table"): an id of that table. The check is on the form, not on whether the document exists |
literal | v.literal("open"): exactly that value |
array | v.array(item) |
object | v.object({ field: validator }): those fields and no others |
record | v.record(keys, values): an object with any keys of one type, all with values of one type |
union | v.union(a, b): any one of them |
optional | v.optional(x): the field may be missing. Only for a field of an object |
nullable | v.nullable(x): the same as v.union(x, v.null()) |
any | Anything |
commitTs | From the convex package. The server has no rule for it: every value is refused |
function | From 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}
Text search
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.
fetchWithCertificate():fetchthat presents a stored client certificate in the TLS handshake (mutual TLS). Same arguments and result asfetch. Actions only.signXml(): Signs an XML document with a stored certificate: enveloped XMLDSig, RSA-SHA256, C14N 1.0, the certificate inKeyInfo,Signaturewithout a prefix. Returns the signed document. Actions only.
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.
new Html(readonly value: string): HTML that is already safe: whathtml,rawandbuttonreturn. Anything else interpolated inhtmlis escaped..toString(): The HTML text, so anHtmlcan go wherever a string goes.
escapeHtml(): Escapes& < > " 'so a value can go into HTML text or a quoted attribute.html(): Tagged template for email HTML. Every interpolated value is escaped, except what is alreadyHtml(anotherhtml,raw,button). Arrays are joined, andnull,undefinedandfalseprint nothing, so a list mapped tohtmlpieces and a piece behindcond &&both work.raw(): Marks a text as HTML that goes in as it is, without escaping. Only for HTML you wrote, never for a variable.button(): A link that looks like a button in every email client (a table with inline styles). The URL must start withhttp://,https://ormailto:; anything else throws, so ajavascript:from a variable never becomes a link.layout(): The default layout: a centered card 560 px wide, withnameat the top andfooterat the bottom, inline styles only (email clients drop<style>), and the hidden preheader that inboxes show next to the subject.template(): Declares a template. Keep it in any file and list it innew Email({ templates: { name: template } }).new Email(private options: EmailOptions<T> = {}): Transactional email.sendwrites the request toemail_messagesin the caller's transaction and queues the delivery, with retries, through SMTP (the panel's) or Resend, chosen by theEMAIL_PROVIDERapp variable. Undernotusbase dev, and on a server outside production (--env), nothing goes out: the email stayscapturedin the table. Enabling: it needs the queue module too. SpreadqueueTablesandemailTables(fromnotusbase-modules/queue/schemaandnotusbase-modules/email/schema) intodefineSchema, and createconvex/queue.ts(export * from "notusbase-modules/queue";) andconvex/email.ts(export * from "notusbase-modules/email";). No http route..send(): Writes the request and joins the queue. Runs in a mutation or action. In a mutation it is the mutation's transaction: if the mutation fails, nothing goes out. In an action it is a mutation of its own. Returns the document id inemail_messages. Withtemplate, renders that template withdatain the layout first;datais required here, theexampleonly servesrenderand the preview. Throws on an invalid address, subject, sender, size (512 KiB) or template variable, or if the queue module is not enabled..render(): Renders a template without sending:{ subject, html, text?, preheader? }.datadefaults to the template'sexample. Runs anywhere, also in a test. Throws on an unknown name or a variable that does not matchargs..previews(): The two internal queries the dashboard's Email screen calls:templateslists the names, variables and examples, andpreviewrenders one. Export them fromconvex/email.ts, next to the module..status(): Returns theEmailStatusof a message, ornullif it does not exist. Runs in a query, mutation or action.
- Values:
passwordReset,accountInvite - Functions the module registers, called through the exports above and with
notusbase run, never by a client:email:enqueue,email:get,email:status,email:list,email:attempt,email:finish,email:completed,email:reportFailure,email:deliver
queue
Queue with a parallelism limit, retries with growing backoff and a notification on completion. README.
vOnCompleteArgs(): Argument validators for theonCompletemutation:{ workId, context, result }. Pass the validator of thecontextyou gave toenqueue*.new Queue(...): Work queue: limits how many tasks run together, retries with growing backoff and tells you when a task ends. Enqueueing is part of the caller's transaction: if the mutation rolls back, the task never existed. Enabling: spreadqueueTables(fromnotusbase-modules/queue/schema) intodefineSchema, and createconvex/queue.tswithexport * from "notusbase-modules/queue";. No http route..enqueueAction(): Enqueues an action and returns the task id. Runs in a mutation or action. The function is stored by name. Options:retry(overrides the queue's),onComplete(a mutation that receives{ workId, context, result }),context(passed toonComplete),runAfter(ms from now) orrunAt(timestamp). Withretry, the action runs at least once, so it has to be idempotent..enqueueMutation(): Enqueues a mutation and returns the task id. Same options asenqueueAction. The mutation runs exactly once, in the same transaction that marks the task done; a failure undoes everything it did..cancel(): Cancels a task. A pending one leaves at once; one that already started finishes, with no retry. Runs in a mutation or action. Returns nothing..cancelAll(): Cancels the queue's pending tasks. Runs in a mutation or action. Tasks already running are not touched..status(): Returns the task'sTaskStatus(pending,running,success,failedorcanceled, withattempts), ornullif it no longer exists. Runs in a query, mutation or action.
- Values:
vResult - Functions the module registers, called through the exports above and with
notusbase run, never by a client:queue:reportFailure,queue:dispatch,queue:runMutation,queue:load,queue:finish,queue:runAction,queue:enqueue,queue:cancel,queue:status,queue:cancelAll
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:
url: The server address, such as http://127.0.0.1:53211.dataDir: The server's pb_data.notusbase: The binary in use.environment: The environment the kit gives the binary.cli: Runsnotusbase <args>in the project folder and returns its output; a failure becomes an exception with what the command said.t.cli("sql", "--dir", t.dataDir, "select 1").admin: Calls/api/notusbase/<path>with the deploy key, as the CLI does. The exception carriesstatus.signIn: Signs in as an existing user of theuserscollection.idis thesubjectin the functions.deploy(project, { skipTypecheck = false } = {}): Generates the types, runs tsc and deploys the project's convex/ folder.envSet(name, value): Sets a process.env variable on the server.nulldeletes it.run(fn, args = {}): Runs a function, internal ones included, likenotusbase run. Returns the value; an error becomes an exception, withdatawhen the function threw a ConvexError.waitJobs({ function: fn, ms = 10000 } = {}): Waits for the jobs that are already due (runAfter, runAt and the crons' own) to finish. A job set for after the deadline does not count. Returns how many jobs there are in each state.advance(time, { wait = true, ms = 10000 } = {}): Advances the server clock (the scheduler and the functions'Date.now()) and waits for what came due to finish:runAfter/runAtjobs and due crons, which run once each. Only on a server the kit started with the test clock (NOTUSBASE_TEST_CLOCK=1).wait: falseonly advances. Returns how many jobs are in each state.now(): The server's "now", in ms: the real one plus whatadvanceadded.runCron(name, { ms = 10000 } = {}): Fires a cron now, without waiting for its time or touching its next occurrence, and waits for its job to finish. Jobs the cron function schedules are left forwaitJobs.user(email = `test-${randomBytes(4).toString("hex")}@kit.local`, password = randomBytes(18).toString("base64url")): Creates a user in theuserscollection through a test superuser, whether signup is open or closed, and signs in.log(n = 40): The last lines of the server log, when the kit started it.
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:
- everything
serverAt()returns port: The port the server listens on.child: The server process.stop(): Stops the server and waits for it to exit; after 10 s, kills it.
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:
- everything
start()returns dir: The throwaway folder, withpb_dataandnotusbase.log.project: The project folder, as an absolute path.deployOutput: Whatnotusbase deployprinted: the deployed line and, with a schema change, the plan.user: Creates a user in theuserscollection through a test superuser, whether signup is open or closed, and signs in.client: TheConvexClientof theconvexpackage, signed in as the default user:t.client.query(api.x.y, args),.mutation,.action,.onUpdate. It does not print to the console: a refused call arrives as the exception, withe.datawhen the function threw a ConvexError.addUser: Creates another user and returns a client signed in as them.anonymous(): A client with no login.emails({ status = "captured", ms } = {}): The emails theemailmodule captured, oldest first:{ id, to, from?, subject, html, text?, template?, redact?, status, attempts, queuedAt, doneAt }. Waits for the due jobs first, so the send has gone through the queue.statuschanges the status read (sent,failed,queued, ornullfor all).close: Closes the clients and stops the server. Always call it, infinally.
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.
| Key | Default | What it is |
|---|---|---|
url | http://127.0.0.1:8090 | Where 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 |
dir | pb_data | The data folder: where the server keeps the database, and where the other commands find the deploy key. The same as --dir |
remotes | Servers 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 | |
mask | What 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"] | |
tenant | The 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.
| Variable | Default | Secret | What it does |
|---|---|---|---|
NOTUSBASE_DIR | pb_data in the folder where the command runs | The 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_HTTP | none | Public port, passed as --http. | |
NOTUSBASE_ADMIN_HTTP | empty: one port only | Admin port, the default value of --admin-http: dashboard, deploy, metrics and SQL. | |
NOTUSBASE_FLAGS | empty | Extra serve flags in the unit, such as --behind-proxy. | |
NOTUSBASE_ENV | development | The 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_ORIGINS | none | Passed as --origins: origins accepted for CORS and WebSocket, comma-separated, with scheme. | |
PB_WAL_SYNC_MS | 1000 | WAL 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_COLLECTIONS | empty; users in notusbase dev | PocketBase auth collections whose token counts as an identity. At most one. | |
NOTUSBASE_SIGNUP | closed; open in notusbase dev | 1 opens signup in users. In notusbase dev, 0 closes it. | |
NOTUSBASE_PUBLIC_URL | listen address | The 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_HOSTS | empty: any host | Hosts accepted on the public port, comma-separated. A host outside the list gets 421. See First server. | |
NOTUSBASE_ADMIN_HOSTS | empty: any host | The 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_HEADER | X-Forwarded-For | Header that carries the real IP, used with --behind-proxy. | |
NOTUSBASE_DOMAIN | none: no HTTPS of its own | The 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_EMAIL | empty | The email Let's Encrypt writes to about the certificate. Optional. | |
NOTUSBASE_SETUP | closed | The 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_PREVIOUS | none: nothing to go back to | The 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_FILE | none: no setup page | The 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.
| Variable | Default | Secret | What it does |
|---|---|---|---|
NOTUSBASE_BACKUP | empty: no backup | Folder 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_HOURS | 24 | Interval of the automatic check. 0 takes it off the primary, when the standby does the checking. | |
NOTUSBASE_BACKUP_CHECK_DIR | NOTUSBASE_BACKUP | Where 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_HOURS | 168 | Interval 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_MBS | 64 | Read rate of the check. | |
NOTUSBASE_BACKUP_CHECK_WAL_MB | 1024 | WAL 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.
| Variable | Default | Secret | What it does |
|---|---|---|---|
NOTUSBASE_S3 | empty | off 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_BUCKET | empty: no S3 | Bucket. Turns on the upload, restore-s3 and the standby. | |
NOTUSBASE_S3_ENDPOINT | empty: AWS | Provider address, such as R2 or MinIO. | |
NOTUSBASE_S3_REGION | us-east-1 | Region. | |
NOTUSBASE_S3_ACCESS_KEY | none | yes | Credential. |
NOTUSBASE_S3_SECRET_KEY | none | yes | Credential. |
NOTUSBASE_S3_KEY | required with a bucket | yes | 32 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_PREFIX | empty | Folder inside the bucket. | |
NOTUSBASE_S3_PATH_STYLE | off | 1 uses path-style addressing (MinIO and the like). | |
NOTUSBASE_S3_MBS | 64 | Disk read rate for the upload. |
Alerts
The red warnings of the status screen, sent out in a single message. See Alerts without Prometheus.
| Variable | Default | Secret | What it does |
|---|---|---|---|
NOTUSBASE_ALERT_URL | empty: no webhook | yes | Alert webhook (ntfy, Slack, Telegram or your own receiver). May contain a token: it never goes to the log. |
NOTUSBASE_ALERT_FORMAT | text | text or json ({"text": ...}, for Slack and Telegram). | |
NOTUSBASE_ALERT_EMAIL | empty: no email | Destination of the alert by email, using the SMTP settings and sender from the dashboard. | |
NOTUSBASE_ALERT_PRIMARY | empty | Standby 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_HOOK | empty | Standby 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_EPOCH | Set by promote for the route hook: the new epoch. Read by the hook, not by the server. | ||
NOTUSBASE_PROMOTED_DIR | Set by promote for the route hook: the new primary's data folder. Read by the hook, not by the server. | ||
NOTUSBASE_DB_LIMIT_GB | 30 | Agreed 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).
| Variable | Default | Secret | What it does |
|---|---|---|---|
NOTUSBASE_FN_MS | 1000 | Time for a query and a mutation, and CPU time per slice of an action. | |
NOTUSBASE_HEAP_MB | 64 | V8 heap per isolate. 0 removes the cap. | |
NOTUSBASE_UPLOAD_MB | 1024 | Size of an uploaded file, and of each _storage file on import. |
Engine
Concurrency and memory. The numbers are in Limits per function.
| Variable | Default | Secret | What it does |
|---|---|---|---|
NOTUSBASE_VMS | cores of the machine | Query and mutation isolates. | |
NOTUSBASE_ACTION_ISOLATES | 64 | Action 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_WORKERS | 64 | Scheduled jobs running at the same time. | |
NOTUSBASE_STOP_GRACE_S | 30 | On 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_MB | 64 | Cap of that log. 0 turns it off. |
Public port
Per-IP caps.
| Variable | Default | Secret | What it does |
|---|---|---|---|
NOTUSBASE_RATE_API | 50 | Requests per second. 0 turns it off. | |
NOTUSBASE_RATE_SYNC | 10 | Sync connections opened per second. | |
NOTUSBASE_CONNS_PER_IP | 200 | Requests in flight, including sync and realtime. 0 turns it off. | |
NOTUSBASE_MAX_SESSIONS | automatic | Cap 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".
| Variable | Default | Secret | What it does |
|---|---|---|---|
NOTUSBASE_NODE | node from PATH | Node 22.13 or newer, outside /home. | |
NOTUSBASE_NODE_PATH | empty | node_modules of the modules with "use node". | |
NOTUSBASE_NODE_PORTS | 443 | Outbound TCP ports. Port 53 is always open; any opens all of them. | |
NOTUSBASE_NODE_SANDBOX | on | off removes the fence and the destination filter. Emergency exit. | |
NOTUSBASE_FETCH_ALLOW | empty | CIDRs that fetch may reach even though they are loopback, a private network or 100.64/10. Also applies to Node. | |
NOTUSBASE_FETCH_HOSTS | empty: no destination | Outside 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_ENV | set by the server | Internal: 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_INFLIGHT | set by the server | Internal: 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_SELF | set by the server | Internal: 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.
| Variable | Default | Secret | What it does |
|---|---|---|---|
NOTUSBASE_ISOLATED | off | 1 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_ERRORS | off; on in notusbase dev | Shows the client the internal error message instead of Server Error. | |
NOTUSBASE_TEST_CLOCK | off | 1 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_DEMO | off | 1 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_MIN | not set | On 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.
| Variable | Default | Secret | What it does |
|---|---|---|---|
NOTUSBASE_DEPLOY_KEY | none | yes | Deploy key for notusbase deploy, logs, run and the MCP server. It opens the whole server. |
NOTUSBASE_URL | the url in notusbase.json, or http://127.0.0.1:8090 | The server that the command or the MCP server calls, the default value of --url. | |
NOTUSBASE_PROJECT | current folder | Project 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.
| Variable | Default | Secret | What it does |
|---|---|---|---|
NOTUSBASE_CACHE_DIR | .cache inside the package | Folder where the package keeps the downloaded binary. | |
NOTUSBASE_DEMO_URL | empty | Address of the live demo that the site's "Live demo" buttons open (ops/site.sh). Empty hides them. | |
NOTUSBASE_DOWNLOAD_BASE | https://downloads.notusbase.com | Base of the download: the file comes from <base>/notusbase/v<version>/. Only https, or http://127.0.0.1 in the local rehearsal. | |
NOTUSBASE_PLATFORM | the machine's | Forces 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.
| Variable | Default | Secret | What it does |
|---|---|---|---|
NOTUSBASE_CONF | /etc/notusbase/notusbase.env | Where update.sh and doctor.sh read notusbase.env from. The unit sets it for doctor.sh run by the setup page. | |
NOTUSBASE_UNIT | notusbase | Name of the systemd unit. | |
NOTUSBASE_SYSTEMCTL | systemctl | The systemd command, such as systemctl --user in a test. | |
NOTUSBASE_WATCH_S | 15 | How long update.sh watches the new version stay up. | |
NOTUSBASE_FORCE | off | 1 makes update.sh update even with a late backup. | |
NOTUSBASE_BUILD_JOBS | 2 | Go packages compiled in parallel by package.sh. | |
NOTUSBASE_DIST | tmp/dist of the repository | Output of package.sh; release.sh points it at the release folder. | |
NOTUSBASE_LLVM | empty | An LLVM folder (the one with bin/clang) to build with, when the system Clang is older than 21. | |
NOTUSBASE_RELEASE_DIR | tmp/release of the repository | Where release.sh builds <version>/ with the packages, SHA256SUMS and NOTES.md. | |
NOTUSBASE_RELEASE_DIRTY | empty | 1 accepts a tree with uncommitted changes and a HEAD outside the tag, for rehearsal only. | |
NOTUSBASE_IMAGE | notusbase | Image used by the compose file. | |
NOTUSBASE_REHEARSAL_URL | set by notusbase copy isolated --rehearse | Address of the isolated copy, passed to the project's rehearse.mjs. | |
NOTUSBASE_REHEARSAL_DATA | set by notusbase copy isolated --rehearse | pb_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.
| Variable | Default | Secret | What it does |
|---|---|---|---|
PB_THUMBS_MAX_WORKERS | cores + 2 | Thumbnails generated at the same time. | |
PB_THUMBS_MAX_WAIT | 60 | Seconds to wait for a thumbnail slot. | |
PB_THUMBS_MAX_SOURCE_PIXELS | 104857600 | Pixels of the original image above which there is no thumbnail. | |
PB_FILES_DELETE_MAX_WORKERS | 2000 | Files deleted at the same time after a record or collection is deleted. | |
PB_ID_TOKEN_LEEWAY | 300 | Clock 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).
| Variable | Default | Secret | What it does |
|---|---|---|---|
NOTUSBASE_TEST_FAKE_SSH | Folder of the fake ssh in the --to test: the test binary takes the place of the ssh command. | ||
NOTUSBASE_TEST_SSH_SERVER | Makes the test binary run as that fake ssh: it opens the local tunnel and runs the script on the other side. | ||
NOTUSBASE_TEST_MAIN | off | Makes the test binary run as notusbase, for tests that start a process. | |
NOTUSBASE_TEST_EVERY_MS | Upload interval in the standby test. | ||
NOTUSBASE_TEST_SEAL_MS | Open-segment deadline in the standby test. | ||
NOTUSBASE_TEST_HOLD_MS | Turns on the standby deadlines test: wait after the standby sees a new point. | ||
NOTUSBASE_TEST_PAUSE_MS | Pause between load requests in the standby test. | ||
NOTUSBASE_TEST_LOAD_SECONDS | Duration of the load in the standby test. | ||
NOTUSBASE_TEST_SWITCHOVER | off | Turns on the switchover rehearsal: kills the primary under load, promotes the standby, and reports downtime and lost writes. | |
NOTUSBASE_TEST_FILL_MB | Size of the database before the switchover rehearsal starts. | ||
NOTUSBASE_TEST_CHECK | full | Check of the promotion in the switchover rehearsal: full or quick. | |
NOTUSBASE_TEST_LOG_MB | The 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_OUT | Folder where the switchover rehearsal keeps each process's whole log (old primary, standby, promote, new server). | ||
NOTUSBASE_TEST_PARTITION | off | Turns 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_EXPECT | 1 | 0 only reports, without checking: for a run on the code from before the write lease. | |
NOTUSBASE_TEST_LEASE_MS | three beats | The primary's write lease with a standby, 30 s in production. | |
NOTUSBASE_TEST_WATCH_MS | Interval of the standby's watch in the switchover rehearsal. | ||
NOTUSBASE_TEST_S3_DISK | off | Keeps the fake S3's objects on disk in the standby tests, for big databases. | |
NOTUSBASE_FAKE_S3_DIR | Keeps the fake S3's objects in a folder. | ||
NOTUSBASE_FAKE_S3_PORT | Turns on TestServeFakeS3: a fake S3 on this port, to prove it against the real binary. | ||
NOTUSBASE_KNOWN_BUGS | off | Runs the tests of confirmed, still open defects, which fail; without it they are skipped. | |
NOTUSBASE_INDEX_ROWS | Turns on TestIndexBuildTime: rows in the table whose index build it times. | ||
NOTUSBASE_INDEX_BODY | 0 | Bytes of the extra text column in TestIndexBuildTime (0 for a short row). | |
NOTUSBASE_TEST_BUNDLE | the deployed code | Reads the published code from another file instead of pb_data/.notusbase_bundle.js: the tests, the bench and the soak. | |
NOTUSBASE_TEST_HTTPS | 0.0.0.0:443 | The address of the server's own HTTPS with a domain, fixed in production. | |
NOTUSBASE_TEST_ACME_URL | Let's Encrypt production | The ACME directory of another server: the tests use a fake one. | |
NOTUSBASE_TEST_HTTP | 0.0.0.0:80 | The HTTP address of a domain set in the dashboard, where the check of the name listens; fixed in production. | |
NOTUSBASE_TEST_RESOLVE | the DNS | The address every name resolves to in the check of the domain. | |
NOTUSBASE_TEST_ACTION_MS | 600000 (10 min) | The total time of an action, fixed in production. | |
NOTUSBASE_TEST_MAX_READS | 32000 | Documents read per call, fixed in production. | |
NOTUSBASE_TEST_FN_BYTES_MB | 1024 | ArrayBuffer 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_MB | 1024 | ArrayBuffer and the wasm memory JS can reach, per action isolate, fixed in production; 0 removes the ceiling. | |
NOTUSBASE_TEST_ACTION_VMS | 4 | Shared isolates that run actions, fixed in production. The tests use 1, so a bad action and its neighbor share one. | |
NOTUSBASE_TEST_REACT_WORKERS | 4 | Isolates that rerun the subscribed queries, fixed in production. | |
NOTUSBASE_TEST_VM_WAIT_MS | 5000 | Wait for a free isolate before the 429, fixed in production. | |
NOTUSBASE_TEST_BATCH | 256 | Mutations per COMMIT, fixed in production. | |
NOTUSBASE_TEST_DEPLOY_HISTORY | 10 | Deploys kept for rollback, fixed in production. | |
NOTUSBASE_TEST_DEPLOY_HISTORY_MB | 64 | Space of the deploys kept for rollback, fixed in production. | |
NOTUSBASE_TEST_BACKUP_MS | 200 | Interval between WAL copies, fixed in production. | |
NOTUSBASE_TEST_BACKUP_LOG_MB | 8192 | Log size that starts a new chain, fixed in production. | |
NOTUSBASE_TEST_BACKUP_WAL_MAX_MB | 8192 | WAL without a chain before the checkpoint without copy, fixed in production. | |
NOTUSBASE_TEST_BACKUP_BASE_MBS | 64 | Pace of the base copy, fixed in production. | |
NOTUSBASE_TEST_BACKUP_COMPACT_S | 60 | Compaction window of closed segments, fixed in production. | |
NOTUSBASE_TEST_S3_EVERY_MS | 30000 | Interval between S3 rounds and, on a standby, between bucket reads; fixed in production. | |
NOTUSBASE_TEST_S3_SEAL_MS | 900000 | Maximum age of the open segment, fixed in production. | |
NOTUSBASE_TEST_ALERT_NAME | app name and machine | Name 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_S | 300 | Minimum interval between two alert messages, fixed in production. | |
NOTUSBASE_TEST_ALERT_REPEAT_MIN | 60 | Repeat interval of what stays open, fixed in production. | |
NOTUSBASE_TEST_ALERT_PRIMARY_MS | 10000 | On a standby, interval between probes of the primary, fixed in production. | |
NOTUSBASE_TEST_LOGIN_MAX | 2 | Login attempts per IP every 3 seconds, fixed in production; the kit and the smoke test raise it. | |
NOTUSBASE_TEST_BACKUP_SAMPLE_MB | 256 | Window 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:
MALLOC_ARENA_MAX=2, in the systemd unit: with 2 arenas, the free memory that glibc holds on to stops growing. It costs throughput: on 20 CPUs, 23% fewer reads than the glibc default. With the 60 smalloc_trim, it is the combination that uses the least memory.doctor.shchecks it.NOTUSBASE, in the MCP server: the path to the binary,notusbasefromPATHby default.- Variables of the bench and the long test, read by the project's own scripts and not by the server:
NOTUSBASE_PORT,NOTUSBASE_RUN,NOTUSBASE_TOOLS,NOTUSBASE_BIN,NOTUSBASE_TEST_DIR,PB_TUNEDandPB_HOOKS_DIR. Each one is documented in the script that reads it. - PocketBase's
--encryptionEnv, which encrypts the settings with a variable of any name: NotusBase does not use it.