Your own databases: Qubix.db
Qubix.db is the command through which a user script and a website page handler work with your own PostgreSQL, MySQL or MariaDB database: they read rows and write changes. No HTTP middleman is needed for this — the script connects to the database itself with a connection string.
This is not the Qubix database. The statistics Qubix records are read by the sql command — read-only and within the rights of your role (see Tables for sql queries). Qubix.db works with your database with the rights of the database user named in the connection string — it both reads and writes.
Where the command is available:
- in scripts of the Scripts section — both on a schedule and via the Ejecutar button;
- in website page handlers — on every visit and via the Ejecutar button in the Ejecución de prueba panel (see Site backend).
Britva rules do not have Qubix.db.
You can connect only to a host that an administrator has entered in the Database hosts for scripts list (Ajustes de Qubix → JavaScript). While the list is empty, connections to databases are off. More — in the Where you can connect section.
What it looks like
function main() {
const db = Qubix.db.connect('postgres://report:[email protected]:5432/crm')
const rows = db.query('SELECT id, email FROM customers WHERE country = $1 LIMIT 10', ['DE'])
for (const row of rows) console.log(row.id, row.email)
const changed = db.exec('UPDATE customers SET checked_at = now() WHERE country = $1', ['DE'])
console.log('rows changed:', changed)
db.close()
}
All calls are synchronous: the result comes right away, as with sql and ctx.fetch, so await is not needed. With MySQL everything is the same, except that the string starts with mysql:// and a value's place in the query text is marked with ?.
Connection string
Qubix.db.connect(...) takes a string of this form:
postgres://user:password@host:5432/database?sslmode=require
mysql://user:password@host:3306/database?tls=true
| Part | What it means |
|---|---|
| scheme | postgres:// or postgresql:// — PostgreSQL; mysql:// or mariadb:// — MySQL and MariaDB |
user:password@ | the database user and their password. What the queries may do is decided by this user's rights in your database |
host | one host — a name or an address. Several hosts separated by commas are not accepted, and neither is a path to a socket file instead of a host: the connection goes over the network only |
:port | optional; without a port — 5432 for PostgreSQL and 3306 for MySQL |
/database | the database name |
?… | parameters — only from the table below |
The connection string is read by the rules of an address, so characters like #, ?, /, % or a space in the user name and the password must be written in percent-encoding. The easiest way is to build the string with encodeURIComponent:
const url = 'postgres://report:' + encodeURIComponent(password) + '@db.example.com:5432/crm'
String parameters:
| Parameter | Database | Values | If not given |
|---|---|---|---|
sslmode | PostgreSQL | disable, allow, prefer, require, verify-ca, verify-full | prefer: an encrypted channel if the database supports it, otherwise an open one |
channel_binding | PostgreSQL | disable, prefer, require | prefer |
require_auth | PostgreSQL | scram-sha-256 — the password goes only by SCRAM: a server that asks for it in any other way gets a refusal | whatever the server asks for |
application_name | PostgreSQL | any name — the connection shows up under it in the database's list of connections | — |
tls | MySQL, MariaDB | true — encryption with certificate verification; skip-verify — encryption without certificate verification; preferred — encryption without verification if the server supports it, otherwise an open channel; false — no encryption | no encryption |
If an administrator set the server to reach the internet through Tor (the Outbound requests tab), a database on the internet is connected only when the connection checks the database server by its name: add sslmode=verify-full for PostgreSQL and tls=true for MySQL and MariaDB. A Tor exit is somebody else's server: without the check it would receive the database password. sslmode=verify-ca is not enough — without a root certificate of your own it accepts any public certificate without checking the name — and neither is channel_binding=require with require_auth=scram-sha-256: the password does not travel open then, but an exit that accepted the encryption itself gets enough to guess a weak password offline. A database on an internal network from the host list is reached directly, and the rule does not apply to it.
Other parameters are not accepted — the refusal lists the supported ones. Parameters that would make the connection read files on the Qubix server (passfile, sslkey, sslcert, sslrootcert, service, servicefile for PostgreSQL and allowAllFiles for MySQL) are always rejected.
A string without :// is treated as the name of a saved connection, and there are no saved connections: such a call gets a refusal. Pass a connection string.
Queries: query and exec
Qubix.db.connect(...) returns a connection object:
| Method | What it does | What it returns |
|---|---|---|
query(text, params) | runs a query that returns rows | an array of rows; each row is an object whose keys are the column names |
exec(text, params) | runs a changing statement: INSERT, UPDATE, DELETE and others | the number of rows the statement changed |
close() | closes the connection | nothing |
Values are passed separately from the query text — as the second argument, in an array, while the text has $1, $2… in their place for PostgreSQL and ? for MySQL. A value is not pasted into the query text, so you do not need to escape it by hand, and the query cannot be tampered with through it. The second argument can be left out if there are no values.
| Value in JavaScript | How it goes to the database |
|---|---|
a string, a number, true / false, null | as is |
Date | a date and time |
ArrayBuffer | binary data |
| an array, an object | not accepted — a refusal. For a JSON column, pass JSON.stringify(value) |
What comes back in the answer rows:
| Column in the database | Value in the answer row |
|---|---|
| integer | a number. An integer beyond JavaScript's exact integer range (Number.MAX_SAFE_INTEGER) comes as a string, so that no digits are lost |
floating-point (real, double precision, FLOAT, DOUBLE) | a number |
decimal (numeric, DECIMAL) | a string — this way no precision is lost |
| JSON | a string — parse it with JSON.parse |
date (DATE) | a string like 2026-09-27 |
| date and time | a string in ISO format in UTC, for example 2026-09-27T10:00:00Z |
binary (bytea, BLOB) | ArrayBuffer |
boolean (boolean in PostgreSQL) | true / false. In MySQL BOOLEAN is the integer TINYINT, so it comes as the number 0 or 1 |
NULL | null |
| anything else | a string |
Date-and-time columns without a time zone (timestamp in PostgreSQL, DATETIME in MySQL) carry no zone in the value itself: the time comes as it was written, with a Z appended.
Columns with the same name — for example, id from two tables — give one property in the row, and the last one stays. Give them different names with AS.
Closing the connection
close() closes the connection. Calling it again breaks nothing, and a query over a closed connection gets a refusal. A connection you did not close is closed on its own: for a script — right after the run, for a page handler — before the answer to the visitor. Close the connection as soon as you no longer need it — most conveniently in a finally block.
All queries of one connection go in one database session: the connection is not swapped for another one in the middle of a run. Closing does not give back a place in the connection limit — every attempt to connect to a host during a run counts.
Example: a summary from Qubix statistics into your own database
The script counts the deposits of the past day by country with the sql command and writes the result into your PostgreSQL table with a single insert.
function main() {
const rows = sql`
SELECT geo, countIf(event = 'dep') AS deps
FROM qubix_events
WHERE event_time > now() - INTERVAL 1 DAY
AND geo != ''
GROUP BY geo`
if (!rows.length) return
// One insert for all rows: every query and exec call counts toward the query limit.
const values = []
const params = []
for (const r of rows) {
values.push('(now(), $' + (params.length + 1) + ', $' + (params.length + 2) + ')')
params.push(r.geo, r.deps)
}
const db = Qubix.db.connect('postgres://writer:[email protected]:5432/reports')
try {
const inserted = db.exec('INSERT INTO daily_deps (taken_at, country, deps) VALUES ' + values.join(', '), params)
console.log('rows written:', inserted)
} finally {
db.close()
}
}
For MySQL, replace $1, $2… with ?. Split a large volume into several inserts, but not into an insert per row: you will run into the query limit.
Where you can connect
You can connect only to a host from the Database hosts for scripts list. An administrator maintains the list on the JavaScript tab of the Ajustes de Qubix window — the tab itself is described in the System article. There is one list for scripts and website page handlers; while it is empty, connections to databases are off.
Entries are written one per line:
| Entry | What it opens |
|---|---|
a name, for example db.example.com | this host — but only if the name leads to an address on the internet |
an address, for example 10.0.0.5, or a network, for example 10.0.0.0/24 | this address or this network, including on an internal network; after that the database can also be reached by a name that leads to this address |
* | any address on the internet, but not the internal network |
Enter a database on an internal network — yours or the Qubix server's own — as an address or a network: a name alone is not enough for it. An entry that could not be read (for example, a network with a wrong mask) is skipped, and a line about it goes to the server log. A change to the list takes effect from the next run, no restart needed.
Limits
An administrator sets the limits on the same JavaScript tab. For scripts they sit under the host list and are counted per run. Website page handlers have limits of their own — in the Handlers de páginas de sitios web (Locations) block: connections and queries there are counted per page request, while the rows, the answer size and the timeout are labeled the same way as for scripts.
| Limit | What it restricts | What happens beyond it |
|---|---|---|
| Databases: max connections per run (for handlers — Databases: max connections per page request) | the number of connections: every attempt to connect to a host counts, including a failed one; a call rejected earlier — with an empty host list or a list without a single readable entry, with a wrong connection string, a name instead of one, or a value that is not a string — does not count | a refusal |
| Databases: max queries per run (for handlers — Databases: max queries per page request) | the number of query and exec calls across all connections together | a refusal |
| Databases: max rows per query | the number of rows in the answer to one query | the extra rows are not read: the database stops the query, query returns the first rows, and a warning goes to the run log |
| Databases: max response size, bytes | the size of the database's answer to one query. Every byte received counts, the service ones included, so the limit is reached sooner than the sum of the values themselves would reach it | a refusal, and the connection is closed |
| Databases: query timeout, ms | the time of one query and of one connection attempt | a refusal. A PostgreSQL connection stays usable after it, a MySQL connection is closed |
Besides these limits, queries are stopped by the overall deadline: the script's run time, or Máx. tiempo de ejecución del handler, ms for a page handler. A change to the limits takes effect from the next run.
Refusals and what to do about them
A refusal is an ordinary JavaScript error. You can catch it with try…catch and carry on. An uncaught error stops the script's run with an error, and a page handler answers the visitor with an empty 500 response; to see the reason, repeat the request in the Ejecución de prueba panel (see Site backend).
The refusal texts are in English. The tables below leave out the common prefix Qubix.db: that most of them start with. The host list and the limits are named in the texts the way they are labeled in the English interface; N in a text stands for a number.
Connecting and the host list
| Refusal | What happened | What to do |
|---|---|---|
connections to databases are off: the "Database hosts for scripts" list (System → JavaScript) is empty | the host list is empty | ask an administrator to enter the database host |
connections to databases are off: … has no entry that can be read (see the server log) | the list has no entry that could be read | for an administrator — fix the entries; the unread ones are named in the server log |
host "db.example.com" is not in the "Database hosts for scripts" list (System → JavaScript) | the host is not on the list | enter the host: a name — for a database on the internet, an address or a network — for a database on an internal network |
host "db.example.com" is on an internal network: its address or network must be in the "Database hosts for scripts" list (System → JavaScript) | the name is on the list but leads to an internal address | enter the database's address or network in the list |
host "db.example.com" has no address, host "db.example.com" could not be resolved | no address was found for a name from the list | check the host name |
Qubix.db.connect("crm"): saved connections are not available yet; … | a name was passed, not a connection string | pass a connection string |
database type "redis" is not supported: use postgres:// or mysql:// | an unknown scheme | start the string with postgres://, postgresql://, mysql:// or mariadb:// |
one host per connection, the connection string names no host, port "…" is not a number from 1 to 65535, cannot read the connection string: … | the connection string is put together wrong | check the string; write special characters in the password in percent-encoding |
parameter "foo" is not supported (supported: …), sslmode="bogus" is not supported (supported: …), parameter "…" is given more than once | a parameter not from the list, a wrong value or a repeat | keep the parameters from the table above, each one only once |
this server reaches the internet through Tor, and a Tor exit reads a connection that does not check the database server: add … | the server reaches the internet through Tor, and the connection string does not check the database server | add sslmode=verify-full for PostgreSQL, tls=true for MySQL — see above |
parameter "sslrootcert" is not allowed: it makes the driver read files on the server | the parameter would make the connection read files on the Qubix server | remove the parameter |
host "/var/run/postgresql" is a unix socket; only TCP connections are allowed, parameter "host" is not allowed: … | instead of a host, the address has a socket path, or the host is given in the parameters | give the host and the port in the address itself |
Limits and timeout
For a page handler, the text says per page request instead of per run.
| Text | What happened | What to do |
|---|---|---|
connection cap exceeded: N per run (limit "Databases: max connections per run") | the connection limit is used up | open one connection and run all queries through it |
query cap exceeded: N per run (limit "Databases: max queries per run") | the query limit is used up | combine queries: many rows — in one insert, many reads — in one query |
the answer is larger than N bytes (limit "Databases: max response size, bytes") | the answer is larger than the limit; the connection is closed | select fewer columns and rows; open a new connection for the next queries |
the database sent more than N bytes while connecting (limit "Databases: max response size, bytes") | the database sent too much already while logging in | check that it is really a database that answers at this address and port, and that the answer size limit is not too small |
query stopped after N ms (limit "Databases: query timeout, ms") | the query or the connection attempt did not fit into the time limit | speed up the query or ask an administrator to raise the limit |
this connection was closed after an answer exceeded the response cap, this connection was closed after a query ran out of time | the connection was closed after one of the two refusals above | open a new connection |
the run is out of time; the query was stopped | the run time of the script or the handler is over | cut down the work that one run does |
result truncated to N rows (limit "Databases: max rows per query") | this is not a refusal but a warning in the run log: there are more rows than the limit, the first ones came back | narrow the selection: a WHERE condition, LIMIT, aggregation in the query |
Values and calls
| Text | What happened |
|---|---|
this connection is closed | a query over a connection already closed with close() |
parameter #2 is an array; pass a string, number, boolean, null, Date or ArrayBuffer | the value is an array |
parameter #2 is an object; pass JSON.stringify(value) for a JSON column | the value is an object |
query(text, params): params must be an array, e.g. [42, "text"] | the values were not passed as an array |
Qubix.db.connect(connection): pass a connection string such as postgres://user:password@host:5432/database | something other than a string was passed to Qubix.db.connect |
Answers from the database itself
Everything else is your database's answer: a wrong password, no such table, an error in the query text. It follows Qubix.db: together with the explanations the connection adds: for example, with a wrong password in PostgreSQL the text contains failed SASL auth: FATAL: password authentication failed for user "report". The password from the connection string does not get into the text, and the addresses the host leads to are replaced with its name.
Script and website page handler: the differences
| Script | Website page handler | |
|---|---|---|
| When it runs | on a schedule and via the Ejecutar button | on every visit and via the Ejecutar button in the Ejecución de prueba panel |
| Limits | counted per run; the fields under the host list | counted per page request; the Handlers de páginas de sitios web (Locations) block |
| Refusal on a limit | … per run | … per page request |
| Warning about truncated rows | in the run console | only in the test run console: the log of a live visit is not shown anywhere |
| Overall deadline | the script's run time | Máx. tiempo de ejecución del handler, ms |
| Unclosed connections | are closed right after the run | are closed before the answer to the visitor |
| Connection reuse | no: every run opens its own connections | no: every visit opens its own connections, and the database login repeats on every page view |
| Uncaught refusal | the run ends with an error | the visitor gets an empty 500 response |
| The visitor left without waiting for the answer | — | the handler is interrupted, and the running database query is canceled: the change it was making may or may not get applied |
Your own Qubix name at the top level of the code | the save is refused: the name is taken by the platform | the handler works, but Qubix.db is unavailable to it |
If the handler has not returned even after its deadline, the visitor gets a 503 response, and the connections of this run are closed when the handler finally finishes.
Every page view logs in to the database anew, so keep handler queries short and do not open more than one connection where one is enough.
Both the Ejecutar button of a script and the Ejecutar button of a handler run real queries against your database: changes made through exec are applied at once. Test writes on a test table or a test database.