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
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.
You'll need a native MySQL database in a ready state and its credentials. See MySQL databases to create one. To retrieve credentials, call mysql.get() and use the returned hostname, port, username, password, and database name. The primary username is admin, and the database name is generated per database.
Configure the connection
Laravel reads database credentials from .env. Fetch them with mysql.get(), then set the matching connection. Never commit .env:
DB_CONNECTION=mysqlDB_HOST=db-<hash>.<region>.appwrite.centerDB_PORT=3306DB_DATABASE=<database>DB_USERNAME=adminDB_PASSWORD=<password>DB_CHARSET=utf8mb4DB_COLLATION=utf8mb4_unicode_ciThe scaffolded config/database.php wires these variables into the mysql connection:
'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:
'options' => extension_loaded('pdo_mysql') ? array_filter([ Mysql::ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),]) : [],Run migrations
Define your schema with a migration:
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:
php artisan migrate
# non-interactive, for CI and production deploysphp artisan migrate --forceRun 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:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model{ protected $fillable = [ 'title', 'body', ];}Create and query posts through the model:
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:
# Runtime: pooled, transaction modeDB_HOST=db-<hash>.<region>.appwrite.centerDB_PORT=6033DB_DATABASE=<database>DB_USERNAME=adminDB_PASSWORD=<password>DB_CHARSET=utf8mb4DB_COLLATION=utf8mb4_unicode_ci
# Migrations and schema operations: direct MySQL portDB_DIRECT_HOST=db-<hash>.<region>.appwrite.centerDB_DIRECT_PORT=3306DB_DIRECT_DATABASE=<database>DB_DIRECT_USERNAME=adminDB_DIRECT_PASSWORD=<password>Add a second MySQL connection in config/database.php for direct schema work:
'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:
php artisan migrate --database=mysql_direct --forceFor 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-timeor--max-jobsso a fresh process reclaims memory and reopens its connection. Supervisor or Horizon restarts them automatically.
php artisan queue:work --max-time=3600 --max-jobs=500Each 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:
- Create a branch from the API and read its connection details.
- Export them as the
DB_*variables for the job. - Run
php artisan migrate --forceand your test suite against the branch. - 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.
Related
Connect
Retrieve credentials, rotate the password, and create scoped connection users.
Connection pooler
Pool modes, ports, and read/write splitting for serverless workloads.
Branches
Ephemeral database copies for preview environments and CI.
Network
TLS modes, certificate verification, mTLS, and IP allowlists.
Was this page helpful?
Share what worked or what we should fix. Once approved, our agents automatically apply suggested updates to the docs.