Skip to content

MySQL

Sequelize_

Use Sequelize with an Appwrite native MySQL database. Configure the mysql dialect, run migrations against the direct connection, and pool runtime traffic from serverless environments.

4 min read

Raw

Appwrite's native MySQL database is a standard MySQL engine (default 8.4), so Sequelize works against it with no Appwrite-specific configuration. Point Sequelize's mysql dialect and the mysql2 driver at the connection string from the Connections page and use models, the query interface, and migrations as you would against any MySQL server.

Set the connection string

Fetch the connection details with mysql.get(). That call returns one connectionString on the engine port (3306). Use it as DIRECT_URL, copy it to DATABASE_URL, and change only the port to 6033 for the connection pooler. Never commit them:

.env
DATABASE_URL="mysql://admin.<hash>:<password>@db-<hash>.<region>.appwrite.center:6033/default"
DIRECT_URL="mysql://admin.<hash>:<password>@db-<hash>.<region>.appwrite.center:3306/default"

Use the pooled DATABASE_URL (port 6033) for runtime traffic and DIRECT_URL (port 3306) for Sequelize CLI migrations. Connections on Appwrite Cloud are encrypted with TLS. Configure dialectOptions.ssl when Sequelize or mysql2 does not infer TLS from the URL, and keep certificate verification enabled against Node's standard trust store. See Network security for TLS and network controls.

Install and configure Sequelize

Install Sequelize and mysql2:

Bash
npm install sequelize mysql2
npm install -D sequelize-cli typescript @types/node

Create a Sequelize instance for runtime traffic. Prefer the URL form and set SSL with verification:

TypeScript
import { Sequelize, DataTypes } from 'sequelize';
export const sequelize = new Sequelize(process.env.DATABASE_URL!, {
dialect: 'mysql',
dialectOptions: {
ssl: {
require: true,
rejectUnauthorized: true
}
},
logging: false,
pool: {
max: 10,
min: 0,
idle: 10000
}
});
export const User = sequelize.define(
'User',
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
email: {
type: DataTypes.STRING(255),
allowNull: false,
unique: true
},
createdAt: {
type: DataTypes.DATE,
allowNull: false,
field: 'created_at'
}
},
{
tableName: 'users',
updatedAt: false
}
);

ssl: { require: true, rejectUnauthorized: true } validates the server certificate against Node's built-in CA store. You can also pass discrete host, port, database, username, and password options instead of a URL; keep dialect: 'mysql' and the same dialectOptions.ssl settings.

Run migrations

Use Sequelize CLI migrations against the direct engine port (3306). DDL needs a session-level connection, so do not point the CLI at the transaction-mode pooler.

sequelize-cli init generates localhost defaults. Replace them with a concrete .sequelizerc and config that read DIRECT_URL.

Create .sequelizerc at the project root:

JavaScript
const path = require('path');
module.exports = {
config: path.resolve('config', 'config.js'),
'models-path': path.resolve('models'),
'seeders-path': path.resolve('seeders'),
'migrations-path': path.resolve('migrations')
};

Create config/config.js so every CLI environment uses DIRECT_URL:

JavaScript
require('dotenv').config();
const shared = {
url: process.env.DIRECT_URL,
dialect: 'mysql',
dialectOptions: {
ssl: {
require: true,
rejectUnauthorized: true
}
},
logging: false
};
module.exports = {
development: shared,
test: shared,
production: shared
};

Then generate and apply migrations:

Bash
npx sequelize-cli migration:generate --name create-users
npx sequelize-cli db:migrate

Example migration:

JavaScript
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('users', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
email: {
type: Sequelize.STRING(255),
allowNull: false,
unique: true
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
}
});
},
async down(queryInterface) {
await queryInterface.dropTable('users');
}
};

Prefer migrations over sync({ alter: true }) in shared and production environments. The admin.<hash> user owns the default database and can run schema changes.

Query with Sequelize

Once the schema is migrated, authenticate and use models:

TypeScript
import { sequelize, User } from './db';
await sequelize.authenticate();
const user = await User.create({ email: 'ada@example.com' });
const recent = await User.findAll({
order: [['createdAt', 'DESC']],
limit: 10
});

On long-running servers, create the Sequelize instance once at module scope and reuse it. On serverless, keep a single instance per module scope so warm invocations reuse the pool, and use the Appwrite pooler for runtime traffic. Call sequelize.close() on shutdown.

Pool connections from serverless

Each running instance opens its own connections to the engine. On serverless and edge platforms such as Vercel, Netlify, and Cloudflare, short-lived instances can fan out into more backend connections than the engine allows. Route runtime traffic through the connection pooler. The primary DATABASE_URL already uses the pooler port (6033) on the same hostname.

Keep Sequelize CLI migrations pointed at DIRECT_URL.

Keep the Sequelize pool.max small on serverless. Session-level features such as user variables and temporary tables need the direct port or a session-mode pooler. See the pooler modes page for the trade-offs. Confirm the pooler port on the Connections page if your project shows a different value.

Use a branch for previews and CI

Branches are isolated copies of a database with their own hostname and connection string. They're ideal for running migrations against throwaway data in a pull-request preview or an integration-test job:

  1. Create a branch from the API and read its connectionString.
  2. Export it as DIRECT_URL and DATABASE_URL for the preview or CI job.
  3. Run sequelize-cli db:migrate and your test suite against the branch.
  4. Delete the branch when the job finishes.

Because a branch starts from a storage snapshot, the schema and data match the source database at branch time, so migrations run against representative data without touching production.

Was this page helpful?

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