The command line
Every argument the binary takes, how a script is run from a file or a pipe, and the one thing worth knowing before you run a migration: a script is not a transaction.
The binary is given one store and one thing to do with it. Everything below follows from that sentence, including most of what it refuses.
Everything it takes
This is the binary's own usage, with the column a usage line has no room for.
| written | short | what it does | where it is explained |
|---|---|---|---|
<path> | a store on disk — omitted, the store is in memory and is lost when the process ends | Your first store | |
--at <host:port> | a running node, instead of a store in this process | Connecting | |
--user <name> | sign in as this user; the password comes from the environment | below | |
--serve <host:port> | serve this store over the wire protocol until stopped | Serving | |
--http <host:port> | serve it over HTTP; may accompany --serve, and one process then holds both | Serving | |
--param <name>=<value> | bind $name for the script that follows; repeatable | below | |
--execute <script> | -e | run this and exit | below |
--file <path> | -f | run this file and exit | below |
--backup <file> | write the store's current state (a snapshot) to a file and exit; with --from, its log instead; with --at, a running node's snapshot, streamed (from 0.34.0-beta) | Backup and restore | |
--snapshot <file> | write the store's current state to a file and exit | Backup and restore | |
--dump <file> | write the store's current state as TessariQL to a file and exit | Backup and restore | |
--restore <file> | replay a file into an empty store and exit | Backup and restore | |
--verify <file> | read a backup and say what it holds, changing nothing | Backup and restore | |
--from <n> | with --backup: write the log from sequence n (1 for the whole log) | Backup and restore | |
--upto <n> | with --restore: stop replaying after sequence n | Backup and restore | |
--backup-dir <folder> | where a serving node writes BACKUP … TO '<name>'; without it every TO is refused | Backup and restore | |
--unseal-for <duration> | how long an unseal lasts before the store seals itself (10m, 1h); default TESSARIDB_UNSEAL_FOR or 10m | Vaults | |
--tls-cert <file> | serve clients over TLS 1.3 with this certificate chain, PEM; default TESSARIDB_TLS_CERT | Encryption in transit | |
--tls-key <file> | its private key, PEM, readable by its owner alone (mode 600, or 640 when root owns it; refused otherwise); default TESSARIDB_TLS_KEY | Encryption in transit | |
--require-client-tls | refuse to start unless --tls-cert and --tls-key are given, for a deployment that forbids the clear; or TESSARIDB_REQUIRE_CLIENT_TLS=1; off by default | Encryption in transit | |
--client-plaintext | retired: the clear is the default without a certificate; refused from 0.24.0-beta | Encryption in transit | |
--tls-authority <file> | with --at: speak TLS and trust the node by these certificates, PEM; default TESSARIDB_TLS_AUTHORITY | Encryption in transit | |
--encryption-key-file <file> | encrypt the store's files and seal every backup it writes under the 32 bytes in the file (mode 600); default TESSARIDB_ENCRYPTION_KEY_FILE | Encryption at rest | |
--backup-key-file <file> | with --restore or --verify: the key a sealed backup was sealed under, when it is not the store's | Encryption at rest | |
--health | say whether the store is well, and exit non-zero if it is not | Serving | |
--version | -V | say which build this is, and exit | Install |
--help | -h | print the table above, without the last column | it is the table above |
--cluster-credential <file> | this node's peer credential, PEM, with --serve | The cluster | |
--cluster-key <file> | its private key, PEM, readable by its owner alone (mode 600, or 640 when root owns it; refused otherwise) | The cluster | |
--cluster-authority <file> | the one certificate this cluster trusts, PEM | The cluster | |
--cluster-address <host:port> | where this node's own peer door binds | The cluster | |
--seed <node-id>@<host:port> | a node to reach the cluster through; repeatable | The cluster | |
--join-token <token> | a token from CREATE JOIN TOKEN, offered to the seeds until a row names this node; default TESSARIDB_JOIN_TOKEN | Adding a node |
Two of these need no store at all — --version and --verify — and that is
deliberate. The first thing anyone does with a binary they have just been handed
is ask what it is, and a backup that can only be checked by restoring it is a
backup nobody checks.
An unknown option is an error, not something ignored:
$ tessaridb --nonsense
unknown option "--nonsense"
A session opened with a misspelled flag that quietly used the default is how somebody writes to the wrong store. The same reasoning produces the refusals further down.
Running a script
Three ways in, and the store cannot tell them apart:
$ tessaridb ./data -e 'USE NAMESPACE prod; USE DATABASE orders; SELECT name FROM users;'
$ tessaridb ./data -f migrate.tessariql
$ cat migrate.tessariql | tessaridb ./data
.tessariql is the conventional extension. Nothing reads it — -f takes any
path. The same three, worked through from an empty store, are
Run a file instead of typing.
With neither -e nor -f, statements are read from standard input: a prompt
when that is a terminal, a script when it is a pipe. The pipe gets no prompt
and no greeting, so what comes back is the script's output rather than a
transcript of a session:
$ tessaridb ./data -f first.tessariql
ok
ok
ok
ok
ok
ok
1: { name: 'ada' }
(1 record(s), via scan)
One ok per statement that answered nothing. A statement that answers with
records prints them, then how many there were and how they were reached.
A script is not a transaction
This is the part worth reading before a migration.
A script runs statement by statement and stops at the first refusal, with
exit 1. What ran before the refusal has already happened, and nothing undoes
it. The boundary is not the file — it is wherever you put BEGIN.
Four scripts, each run against its own empty store on disk, and each store read back afterwards:
| the script | the run | what the store held after |
|---|---|---|
| a file, and a statement in the middle is refused | stops there, exit 1 | everything before it — kept; everything after it never ran |
| a file with a syntax error on the last line | the earlier lines ran, exit 1 | everything before it — kept |
the same refusal inside BEGIN … COMMIT | the transaction aborts, exit 1 | nothing |
a one-line -e with a syntax error anywhere in it | nothing ran at all | nothing — not even the DEFINE NAMESPACE in front of it |
The last row differs from the second because of where the statements were cut.
A file is read a statement at a time, so the lines before the bad one have
already gone to the store. A whole -e string on one line is submitted as one
piece, so it is parsed before any of it runs, and one bad token anywhere means
none of it does. Both are defensible; they are not the same, and a migration
that assumes one and gets the other lands halfway.
So the way to say all of it or none of it is to say so:
DEFINE COLLECTION staff;
BEGIN;
CREATE staff = { name: 'ada' };
CREATE staff = { name: 'grace' };
COMMIT;
SELECT name FROM staff;
A transaction spanning several lines of a file is held together as one — the
; after BEGIN does not end it. And a statement outside BEGIN is its own
transaction, which is why the first row of that table leaves a record behind
rather than nothing: see Transactions.
The password is not an argument
There is no --password, and that is the feature:
$ tessaridb --at 127.0.0.1:7654 --user editor --password hunter2
unknown option "--password"
An argument is readable by anyone who can list processes on the machine, and it
outlives the session in the shell history. The password is read from
TESSARIDB_PASSWORD instead, and --user says so when it is missing:
$ tessaridb --at 127.0.0.1:7654 --user editor
tessaridb: --user needs the password in TESSARIDB_PASSWORD, and it is not set
Who the users are and what each may do is Users.
Values that come from outside the script
--param binds one parameter, and repeats:
$ tessaridb ./data --param "who='ada'" --param 'limit=10' -f report.tessariql
The value is written as TessariQL, not JSON, so dec 12.34, 2s and
datetime '…' all say what they are — and what an answer prints pastes straight
back into the next parameter. It is read in isolation, as a value and
nothing else:
$ tessaridb --param 'x=1; DROP TABLE users' -e 'SELECT * FROM users;'
--param x: "1; DROP TABLE users" is not a value TessariQL can read on its own
That reader is shared with the parameters field of an HTTP request body, so
the two surfaces cannot drift into reading a supplied value differently. Why
nothing a caller supplies can become syntax — bound after the statement is
read, rather than escaped into it — is Parameters.
What a running node reports
TESSARIDB_LOG sets how much a node says on standard error — one line per
event, with a UTC timestamp, the level, the source, the spans it happened inside
and a sentence, followed by its values as named fields:
2026-10-04T14:32:45.138963Z INFO tessaridb::serving::announcing: serving the wire protocol address=127.0.0.1:9080
2026-10-04T14:33:02.511207Z WARN connection{connection=7}: tessari_session::session::signing: sign-in refused user=ada
A value is never spliced into the sentence, so user=ada or connection=7 can
be searched for, and every line a connection produced carries its number.
| value | what you get |
|---|---|
off | nothing |
error | refusals the node could not handle |
warn | those, and what it recovered from |
info | the default — and connections accepted, sign-ins refused |
debug | those, and what the node did internally |
trace | everything |
info is the default rather than warn because a node that reports only its
problems cannot answer was it even reached — which is the first question when
a client sees nothing.
Besides one level, TESSARIDB_LOG takes per-module directives —
info,tessari_wire=debug reports the wire protocol in detail and everything else
at info. A value it cannot read is reported once and info is used.
TESSARIDB_LOG_FORMAT chooses the shape: text (the default) for a person, or
json for a collector — one object per line, the event's fields flattened beside
timestamp, level, target and message, and the spans it happened in under
spans. Colour is used only when standard error is a terminal and NO_COLOR is
not set.
A value it cannot read is not an error. It reports at the default and starts normally; refusing to open a database because a log level was misspelled would be the worse of the two outcomes.
The node's own timers
What a node does on a timer — housekeeping, the cluster's greeting, collection,
campaign, settling and balancing rounds, the credential watch — runs one pass at
a time, and every line a pass produces carries cadence{cadence=…}. Each pass
ends one of three ways: it completes (said at debug, because it happens every
period), it fails — pass failed, with what it could not do and why — or it
overruns its period — pass overran, said once; the next pass waits for it
rather than running beside it. A failed pass does not stop the timer.
2026-10-05T03:12:09.402117Z WARN cadence{cadence="collection"}: tessari_wire::driver::cadence: pass failed elapsed_ms=3 error=the collection pass failed: this node cannot say who its peers are: …
Arguments that are refused together
Each of these could have been resolved by ignoring something. None of them is, because a value silently dropped is a value somebody believes was used — and here what they believe is that a port is open, or that a backup was bounded.
| asked | answered |
|---|---|
/tmp/a /tmp/b | only one store may be opened, and "/tmp/b" is a second |
--at … --backup f --from 5 | --from works on a store this process opened, and --at names one it did not |
--param x=1 --backup f | --param binds a value in a script, and --backup runs none |
--from 5 -e '…' | --from bounds a --backup and --upto bounds a --restore |
--serve … --health | an address to serve on and something else to do are two programs |
The five cluster options are given together or not at all. Told some of them, a node refuses to start rather than serving with credentials nobody checked; told none of them, it is the single node it is today. There is no default peer address, because there is no port this engine claims and a defaulted listener is a door the operator did not know they opened.
--version and --help are the exception: they name this binary rather than
the node at --at, so an address alongside them is neither refused nor consulted.
Exit codes
| code | means |
|---|---|
0 | it did what was asked |
1 | it did not, and the reason was printed |
Two of them are meant to be read by a script rather than a person.
--health exits non-zero when the store is unwell, so a cron line needs no
output parsing:
$ tessaridb ./data --health
well — committed to sequence 0
And --verify exits non-zero on a backup that was cut short, which is the
condition you are verifying for — the warning is on the error stream, so a
script can act on the status without reading either:
$ tessaridb --verify ./monday.tessarilog
0 record(s), sequences 1..=5, good through 0
warning: ./monday.tessarilog was cut short — it says it holds through 5 and reads through 0
$ echo $?
1
Where in a backup routine to put that call — and why it is before the file is moved into place — is Checking one.