Skip to main content
Laravel is a PHP web framework with a built-in ORM (Eloquent), a migration system, and the artisan command line tool. In this guide, you point a new Laravel app at ClickHouse Managed Postgres, create a tasks table with a migration, and serve its rows from a route. Every connection uses TLS with full certificate verification.

Prerequisites

  • PHP 8.3 or later, as required by Laravel 13, with the pdo_pgsql extension. Run php -m and check that pdo_pgsql is in the list.
  • Composer
  • A ClickHouse Cloud account
  • psql, to create the database. You can also run the CREATE DATABASE statement in the SQL console.

Create a ClickHouse Managed Postgres service

In the ClickHouse Cloud console, click New service and select Postgres. The instance is ready in a few minutes. See the quickstart for a walkthrough.

Get your connection details

Click Connect in the left sidebar of your service. The modal shows your username, password, server, and port, and it has a Directly / via PgBouncer toggle. A PHP app doesn’t keep a connection pool between requests. By default, every request opens a new Postgres connection and closes it when the request ends. This guide connects the app through the bundled PgBouncer on port 6432, so those short-lived connections share a small set of Postgres backends. Migrations connect directly to Postgres on port 5432, because they run DDL and can hold long transactions. Select via PgBouncer to see the pooled connection details. The port changes to 6432, and the connection strings include sslmode=verify-full. In the same modal, click Download CA certificate. You can also download it from Settings → CA Certificate. The certificate is unique to your instance, so the driver can use it to verify that it’s talking to your server.

Set up the project

Create a Laravel project:
Adding to an existing app?Skip composer create-project. In your project, check that php -m lists pdo_pgsql, then follow the rest of this section and continue with Configure the connection, which switches the app from SQLite to Postgres. php artisan migrate then creates your existing tables in Postgres, but it doesn’t copy data from SQLite.
Move the CA certificate you downloaded into the project directory and rename it to ca-certificate.pem. Create a database for the app. Replace <PASSWORD> and the host with the values from the Connect modal:

Configure the connection

Laravel uses SQLite by default. Open .env and replace the DB_CONNECTION=sqlite line and the commented-out DB_ lines below it with:
.env
Use the absolute path to ca-certificate.pem. libpq resolves a relative path against the working directory of the PHP process, which depends on how PHP runs your app. If your password contains spaces or #, wrap it in double quotes. Keep .env out of version control. The default pgsql connection in config/database.php reads DB_SSLMODE, but not the CA certificate path. Open config/database.php and add the sslrootcert and options entries to the pgsql connection:
config/database.php
Laravel appends sslmode and sslrootcert to the PDO DSN, and the pdo_pgsql extension passes them to libpq, which checks the certificate chain and the hostname. PDO::ATTR_EMULATE_PREPARES makes PDO send each query with its values already bound instead of creating a server-side prepared statement. Set it when you connect through PgBouncer. When pdo_pgsql is built against libpq 16 or older, for example on Debian 12, PHP frees each prepared statement with a SQL DEALLOCATE statement. PgBouncer renames prepared statements on the server, so the DEALLOCATE fails. Inside a transaction, that aborts the transaction, and the next query fails with current transaction is aborted. With libpq 17 or later, the default settings also work. Run php -i | grep libpq to see your version. Check the connection:
Without the sslrootcert line, verify-full fails with root certificate file ".../.postgresql/root.crt" does not exist, because libpq falls back to its default CA location. With the wrong CA, the connection fails with SSL error: certificate verify failed.

Create a model and migration

Generate an Eloquent model and a migration for a tasks table:
Open the new migration in database/migrations/ (its name ends in _create_tasks_table.php) and add the title and done columns to up:
database/migrations/xxxx_xx_xx_xxxxxx_create_tasks_table.php
Replace app/Models/Task.php so that the columns can be mass-assigned and done is returned as a boolean:
app/Models/Task.php
Run the migrations over the direct connection. An environment variable overrides the value in .env, so you only need to change the port:
Besides tasks, this creates the tables for the users, sessions, cache, and jobs that a new Laravel app uses by default. Laravel records applied migrations in the migrations table and only runs new ones on later calls.

Query the database

Add a few rows with Eloquent from artisan tinker:
Add a route that returns all tasks as JSON to the end of routes/web.php:
routes/web.php
Laravel applies the search_path, timezone, and isolation_level connection options with session-level SET statements when it connects, and those don’t carry over between transactions through PgBouncer. Keep these options at their defaults, or connect directly on port 5432 if you need them. Session-level features such as LISTEN and session advisory locks also don’t work through PgBouncer. See the FAQ.

Run and verify

Start the development server:
Open http://localhost:3104/tasks in your browser. The response looks like this:
To see the data in the console, open SQL console in the left sidebar of your service, expand guide_laravel and then public, and click the tasks table. The public schema also lists the default Laravel tables, such as users, sessions, and migrations:

Next steps

Last modified on September 30, 2026