Docs
Skip to content

MySQL

Node.js drivers_

Connect to an Appwrite native MySQL database from Node.js with mysql2 or MariaDB Connector/Node.js. Configure pools, TLS verification, serverless connection management, and troubleshooting.

5 min read

Raw

A native MySQL database works with standard Node.js drivers that speak the MySQL wire protocol. The Connections page shows the smallest query. This guide covers driver pools, TLS verification, serverless connection management, and common connection errors.

Raw driver, ORM, or SQL API?

There are three common ways to query a native MySQL database from Node.js. Pick by workload:

ApproachUse it when
Raw driver (this page)You want a connection pool you control, hot-path queries, or a thin data layer with little overhead.
ORM (Prisma, Drizzle)You want migrations, a typed schema, and query building. The ORM still uses a MySQL driver underneath.
SQL APIYou're on a runtime that cannot open TCP sockets, or you're scripting a query over HTTPS.

The rest of this page is for long-running or serverless Node.js servers that can open TCP connections.

mysql2 promise API

Install mysql2 with npm install mysql2. For a long-running server, create one promise pool at module scope and reuse it for every request. The pool opens connections lazily up to connectionLimit.

JavaScript
import mysql from 'mysql2/promise';
import process from 'node:process';
const pool = mysql.createPool({
host: 'db-<hash>.<region>.appwrite.center',
port: 3306,
user: 'admin',
password: process.env.DB_PASSWORD,
database: '<database>',
ssl: { rejectUnauthorized: true },
waitForConnections: true,
connectionLimit: 10,
idleTimeout: 30000,
enableKeepAlive: true,
});
await pool.query(`
CREATE TABLE IF NOT EXISTS drivers_events (
id INT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
const [result] = await pool.execute(
'INSERT INTO drivers_events (email) VALUES (?)',
['ada@example.com']
);
const [rows] = await pool.execute(
'SELECT id, email FROM drivers_events WHERE id = ?',
[result.insertId]
);
console.log(rows[0]);
await pool.end();

Use execute for parameterized statements on the direct MySQL port. mysql2 prepares these statements on the server and caches them per connection, which is a good fit for a stable application pool.

mysql2 callback API

Use the callback API when your application already follows callback patterns. Keep the pool shared, and close it only during process shutdown.

JavaScript
import mysql from 'mysql2';
import process from 'node:process';
const pool = mysql.createPool({
host: 'db-<hash>.<region>.appwrite.center',
port: 3306,
user: 'admin',
password: process.env.DB_PASSWORD,
database: '<database>',
ssl: { rejectUnauthorized: true },
waitForConnections: true,
connectionLimit: 10,
idleTimeout: 30000,
enableKeepAlive: true,
});
pool.query(`
CREATE TABLE IF NOT EXISTS drivers_events_callback (
id INT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`, (createErr) => {
if (createErr) throw createErr;
pool.execute(
'INSERT INTO drivers_events_callback (email) VALUES (?)',
['grace@example.com'],
(insertErr, result) => {
if (insertErr) throw insertErr;
pool.execute(
'SELECT id, email FROM drivers_events_callback WHERE id = ?',
[result.insertId],
(selectErr, rows) => {
if (selectErr) throw selectErr;
console.log(rows[0]);
pool.end();
}
);
}
);
});

MariaDB Connector/Node.js

MariaDB Connector/Node.js is a maintained JavaScript driver for MariaDB and MySQL databases. Install it with npm install mariadb. Its default API is promise-based and it exposes a pool with connectionLimit.

JavaScript
import mariadb from 'mariadb';
import process from 'node:process';
const pool = mariadb.createPool({
host: 'db-<hash>.<region>.appwrite.center',
port: 3306,
user: 'admin',
password: process.env.DB_PASSWORD,
database: '<database>',
ssl: true,
connectionLimit: 10,
connectTimeout: 5000,
});
let connection;
try {
connection = await pool.getConnection();
await connection.query(`
CREATE TABLE IF NOT EXISTS drivers_events_mariadb (
id INT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
const result = await connection.query(
'INSERT INTO drivers_events_mariadb (email) VALUES (?)',
['lin@example.com']
);
const rows = await connection.query(
'SELECT id, email FROM drivers_events_mariadb WHERE id = ?',
[result.insertId]
);
console.log(rows[0]);
} finally {
if (connection) connection.release();
await pool.end();
}

The connector uses Node.js trusted root CAs when ssl: true is enabled. If your runtime image does not ship with a current trust store, pass a CA bundle with ssl: { ca: fs.readFileSync('./ca-bundle.crt') }.

TLS and CA verification

Connections on Appwrite Cloud are encrypted with TLS, terminated at the edge and forwarded to your database over the internal network. Use certificate verification in production:

DriverTLS option
mysql2ssl: { rejectUnauthorized: true }
mariadbssl: true
Custom CA bundleAdd ca inside the driver's ssl object

The database hostname uses a certificate signed by a public CA, so Node.js can verify it with the built-in trust store in standard runtimes. Use a custom CA bundle only for images that do not include one, or when your organization requires a pinned bundle.

For mTLS, where the client also presents a certificate, see the Network security page. Add the client key and cert alongside ca in the same ssl object.

Pool sizing

Pool sizing is a connection budget. Each connection in connectionLimit, across every running instance, counts against your specification's connection cap. A single app server with connectionLimit: 10 is usually modest. Ten replicas with connectionLimit: 50 each can open 500 connections and exhaust smaller specifications.

Size each driver pool to the connection cap divided by replica count, then leave headroom for migrations, background workers, and admin tools. If you need more client concurrency than the engine can accept directly, route runtime traffic through the connection pooler.

Serverless connection management

On Lambda, Cloud Run, Vercel, and similar platforms, every cold start is a fresh instance with its own pool. Keep the pool at module scope so warm invocations reuse it, and keep each pool small, usually connectionLimit: 1 or 2.

Appwrite's MySQL pooler is ProxySQL on port 6033. It is designed for high fan-out runtime traffic and defaults to transaction mode. In transaction mode, use text queries with placeholders through query, and keep server-side prepared statements, temporary tables, user variables, and named locks on the direct port (3306) or a session-mode pooler.

Run migrations, schema changes, and dump or restore tools on the direct port. These workflows rely on a stable session and DDL privileges.

Troubleshooting

SymptomCause and fix
self-signed certificate / unable to verify the first certificateTLS is reaching the wrong host, or the runtime has a stale or incomplete CA bundle. Verify the database hostname and supply a current public CA bundle only when the runtime lacks one.
ER_CON_COUNT_ERROR / connection attempts rejected at the proxyAggregate pool size across all instances exceeds the database specification's connection cap. Lower each pool's connectionLimit, or use the connection pooler on port 6033 with small per-instance pools.
Unknown prepared statement handler or prepared statement errors on port 6033A prepared statement cache is being used through the transaction-mode pooler. Use query with placeholders for pooled runtime traffic, or use the direct port or session mode.
ETIMEDOUT / connect ETIMEDOUTThe database may be cold-starting on smaller specifications or blocked by an IP allowlist. Confirm your egress IP is allowed and raise connectTimeout to absorb cold starts.
Connections drop after a period of inactivityThe proxy closes idle connections after its network idle timeout. Keep mysql2 idleTimeout below that window, and enable keep-alive where the driver supports it.

Was this page helpful?

Share what worked or what we should fix. Once approved, our agents automatically apply suggested updates to the docs.