---
layout: article
title: Kysely
description: Use Kysely with an Appwrite native MySQL database. Configure MysqlDialect with mysql2, run migrations against the direct connection, and pool runtime traffic from serverless environments.
---

Appwrite's native MySQL database is a standard MySQL engine (default **8.4**), so [Kysely](https://kysely.dev/) works against it with no Appwrite-specific dialect or driver. Point `MysqlDialect` and a `mysql2` pool at the connection string from the [Connections](/docs/products/databases/mysql/connections) page and use the type-safe query builder as you would against any MySQL server.

**Before you start**

You'll need a native MySQL database in a `ready` state and its credentials. See [native MySQL databases](/docs/products/databases/mysql) to create one and [Connections](/docs/products/databases/mysql/connections) to retrieve the connection string. The username is `admin.<hash>`, and the database name is `default`.

# Set the connection string

Fetch the connection details with [`mysql.get()`](/docs/products/databases/mysql/connections#credentials). 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](/docs/products/databases/mysql/connection-pooling). 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 migrations and CLI tools. Connections on Appwrite Cloud are encrypted with TLS. If `mysql2` does not infer TLS from the connection string, pass `ssl` options on the pool and keep certificate verification enabled against Node's standard trust store. See [Network security](/docs/products/databases/mysql/network-security) for TLS and network controls.

# Install and configure Kysely

Install Kysely and `mysql2`:

```bash
npm install kysely mysql2
npm install -D typescript kysely-ctl
```

Create a typed database interface and a `Kysely` instance backed by `MysqlDialect`. Mark database-generated columns with `Generated<T>` so insert-only payloads typecheck:

```ts
import { Generated, Kysely, MysqlDialect } from 'kysely';
import { createPool } from 'mysql2';

interface UsersTable {
  id: Generated<number>;
  email: string;
  created_at: Generated<Date>;
}

interface Database {
  users: UsersTable;
}

const dialect = new MysqlDialect({
  pool: createPool({
    uri: process.env.DATABASE_URL,
    ssl: { rejectUnauthorized: true },
    connectionLimit: 10
  })
});

export const db = new Kysely<Database>({ dialect });
```

`ssl: { rejectUnauthorized: true }` validates the server certificate against Node's built-in CA store. You can also pass discrete `host`, `port`, `user`, `password`, and `database` fields to `createPool` instead of `uri`.

# Run migrations

Prefer [kysely-ctl](https://github.com/kysely-org/kysely-ctl) for the migration happy path. Point it at `DIRECT_URL` on the **direct** engine port (`3306`). Migrations issue DDL that needs a session-level connection rather than the transaction-mode pooler.

Create `kysely.config.ts` (project root or `.config/`):

```ts
import { MysqlDialect } from 'kysely';
import { defineConfig } from 'kysely-ctl';
import { createPool } from 'mysql2';

export default defineConfig({
  dialect: new MysqlDialect({
    pool: createPool({
      uri: process.env.DIRECT_URL,
      ssl: { rejectUnauthorized: true }
    })
  }),
  migrations: {
    migrationFolder: 'migrations'
  }
});
```

Generate and apply migrations:

```bash
npx kysely migrate make create_users
npx kysely migrate latest
```

A migration file that creates the `users` table:

```ts
import { Kysely, sql } from 'kysely';

export async function up(db: Kysely<any>): Promise<void> {
  await db.schema
    .createTable('users')
    .addColumn('id', 'integer', (col) => col.primaryKey().autoIncrement())
    .addColumn('email', 'varchar(255)', (col) => col.notNull().unique())
    .addColumn('created_at', 'timestamp', (col) =>
      col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)
    )
    .execute();
}

export async function down(db: Kysely<any>): Promise<void> {
  await db.schema.dropTable('users').execute();
}
```

After the schema exists, you can optionally generate TypeScript types with [kysely-codegen](https://github.com/RobinBlomberg/kysely-codegen) (`npx kysely-codegen`) instead of hand-writing table interfaces. Keep `Generated<T>` on database-generated columns either way.

### Alternate: Migrator API

If you prefer not to use kysely-ctl, Kysely's built-in `Migrator` works the same way against `DIRECT_URL`:

```ts
import { promises as fs } from 'fs';
import { Kysely, MysqlDialect } from 'kysely';
import { FileMigrationProvider, Migrator } from 'kysely/migration';
import { createPool } from 'mysql2';
import * as path from 'path';

const db = new Kysely({
  dialect: new MysqlDialect({
    pool: createPool({
      uri: process.env.DIRECT_URL,
      ssl: { rejectUnauthorized: true }
    })
  })
});

const migrator = new Migrator({
  db,
  provider: new FileMigrationProvider({
    fs,
    path,
    migrationFolder: path.join(__dirname, 'migrations')
  })
});

const { error, results } = await migrator.migrateToLatest();
await db.destroy();

if (error) {
  throw error;
}
```

The `admin.<hash>` user owns the `default` database and can run schema changes.

# Query with Kysely

Once the schema is migrated, use the query builder. MySQL insert results do not return the full row by default, so read the row back when you need it:

```ts
import { db } from './db';

const insertResult = await db
  .insertInto('users')
  .values({ email: 'ada@example.com' })
  .executeTakeFirstOrThrow();

const user = await db
  .selectFrom('users')
  .selectAll()
  .where('id', '=', Number(insertResult.insertId))
  .executeTakeFirstOrThrow();

const recent = await db
  .selectFrom('users')
  .selectAll()
  .orderBy('created_at', 'desc')
  .limit(10)
  .execute();
```

On long-running servers, create the `Kysely` 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 `db.destroy()` 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](/docs/products/databases/mysql/connection-pooling). The primary `DATABASE_URL` already uses the pooler port (`6033`) on the same hostname.

Keep kysely-ctl (and any `Migrator` script) pointed at `DIRECT_URL` so DDL still runs over a session-level connection.

Size the `mysql2` pool conservatively for serverless (often a `connectionLimit` of `1` or a small single-digit value per isolate). Prefer normal parameterized queries over session-scoped prepared statement workflows when you use transaction pooling. Session-level features such as user variables and temporary tables need the direct port or a session-mode pooler. See the [pooler modes](/docs/products/databases/mysql/connection-pooling#modes) page for the trade-offs. Confirm the pooler port on the [Connections](/docs/products/databases/mysql/connections) page if your project shows a different value.

# Use a branch for previews and CI

[Branches](/docs/products/databases/mysql/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 `kysely migrate latest` (or `migrateToLatest`) 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.

# Related

- [Connections](/docs/products/databases/mysql/connections): Retrieve credentials and rotate the primary password.
- [Connection pooler](/docs/products/databases/mysql/connection-pooling): Pool modes, ports, and read/write splitting for serverless workloads.
- [Branches](/docs/products/databases/mysql/branches): Ephemeral database copies for preview environments and CI.
- [Network security](/docs/products/databases/mysql/network-security): TLS and network controls for native MySQL databases.
