Docs
Skip to content

MySQL

Laravel_

Use Laravel and Eloquent with an Appwrite native MySQL database. Configure the connection, run migrations against the direct port, and pool serverless traffic through the connection pooler.

4 min read

Raw

A native MySQL database is a standard MySQL engine, so Laravel works against it with no Appwrite-specific configuration. Point the mysql connection in config/database.php at the credentials from the Connections page, then use Eloquent, the query builder, migrations, and queues as you would against any MySQL server.

Configure the connection

Laravel reads database credentials from .env. Fetch them with mysql.get(), then set the matching connection. Never commit .env:

.env
DB_CONNECTION=mysql
DB_HOST=db-<hash>.<region>.appwrite.center
DB_PORT=3306
DB_DATABASE=<database>
DB_USERNAME=admin
DB_PASSWORD=<password>
DB_CHARSET=utf8mb4
DB_COLLATION=utf8mb4_unicode_ci

The scaffolded config/database.php wires these variables into the mysql connection:

PHP
'mysql' => [
'driver' => 'mysql',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
Mysql::ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],

Laravel imports Pdo\Mysql at the top of this file in new projects. Appwrite Cloud requires TLS on the public hostname. If your PHP runtime needs an explicit CA bundle for MySQL TLS verification, set MYSQL_ATTR_SSL_CA to a trusted root store path and keep the options entry in the mysql connection:

PHP
'options' => extension_loaded('pdo_mysql') ? array_filter([
Mysql::ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],

Run migrations

Define your schema with a migration:

PHP
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('body');
$table->timestamps();
});

Apply migrations from your machine or a deploy step:

Bash
php artisan migrate
# non-interactive, for CI and production deploys
php artisan migrate --force

Run migrate against the direct MySQL port (3306), not the pooler. Migrations issue DDL and schema inspection queries that should use a stable backend connection. The primary admin user owns the database and can run schema changes.

Query with Eloquent

Once the schema is migrated, use Eloquent models and the query builder as usual:

PHP
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
protected $fillable = [
'title',
'body',
];
}

Create and query posts through the model:

PHP
use App\Models\Post;
$post = Post::create([
'title' => 'Hello from a native MySQL database',
'body' => 'Stored in a native MySQL database.',
]);
$recent = Post::query()
->orderByDesc('created_at')
->limit(10)
->get();

Nothing about the native MySQL database changes how Eloquent, relationships, transactions, or the query builder behave. It is a standard MySQL server behind a TLS connection.

Pool connections from serverless

The right port depends on how your app runs.

A long-running PHP process, traditional PHP-FPM with persistent connections, Laravel Octane, or a queue worker, holds its own backend connection for its lifetime. Point these at the direct MySQL port (3306), or at the connection pooler in session mode. Don't put a long-lived process behind the transaction-mode pooler.

A serverless or per-request deployment (Vercel, AWS Lambda, Cloud Run) opens a fresh connection on every invocation and can fan out into more backend connections than the engine allows. Route runtime traffic through the pooler's transaction-mode port (6033) on the same hostname, and keep a named direct connection for migrations and other schema operations:

.env
# Runtime: pooled, transaction mode
DB_HOST=db-<hash>.<region>.appwrite.center
DB_PORT=6033
DB_DATABASE=<database>
DB_USERNAME=admin
DB_PASSWORD=<password>
DB_CHARSET=utf8mb4
DB_COLLATION=utf8mb4_unicode_ci
# Migrations and schema operations: direct MySQL port
DB_DIRECT_HOST=db-<hash>.<region>.appwrite.center
DB_DIRECT_PORT=3306
DB_DIRECT_DATABASE=<database>
DB_DIRECT_USERNAME=admin
DB_DIRECT_PASSWORD=<password>

Add a second MySQL connection in config/database.php for direct schema work:

PHP
'mysql_direct' => [
'driver' => 'mysql',
'url' => env('DB_DIRECT_URL'),
'host' => env('DB_DIRECT_HOST', env('DB_HOST', '127.0.0.1')),
'port' => env('DB_DIRECT_PORT', env('DB_PORT', '3306')),
'database' => env('DB_DIRECT_DATABASE', env('DB_DATABASE', 'laravel')),
'username' => env('DB_DIRECT_USERNAME', env('DB_USERNAME', 'root')),
'password' => env('DB_DIRECT_PASSWORD', env('DB_PASSWORD', '')),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
Mysql::ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],

Run schema commands against that connection:

Bash
php artisan migrate --database=mysql_direct --force

For manual schema work, call DB::connection('mysql_direct'). The transaction-mode pooler does not keep session state across transactions, so user variables, temporary tables, and server-side prepared statements should use the direct port or session mode. See the pooler page for the trade-offs.

Queues and Horizon

A queue worker is a long-running process. php artisan queue:work boots once and processes jobs for its whole lifetime, holding a persistent database connection the entire time. The same applies to every worker that Laravel Horizon supervises. Treat workers like any other long-lived process:

  • Connect them to the direct MySQL port (3306) or the session-mode pooler, never the transaction-mode pooler.
  • Restart workers periodically with --max-time or --max-jobs so a fresh process reclaims memory and reopens its connection. Supervisor or Horizon restarts them automatically.
Bash
php artisan queue:work --max-time=3600 --max-jobs=500

Each worker counts as one backend connection, so size your worker pool (and Horizon's maxProcesses) against the connection budget of your specification. Horizon itself requires Redis for the queue backend; only your application's data connection touches the native MySQL database.

Use a branch for previews and CI

MySQL branches are instant, isolated copies of a database with their own hostname. Create them from the API 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 connection details.
  2. Export them as the DB_* variables for the job.
  3. Run php artisan migrate --force 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 and tests run against realistic 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.