> ## Documentation Index
> Fetch the complete documentation index at: https://clickhouse.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Use Laravel with ClickHouse Managed Postgres

> Connect a Laravel app to ClickHouse Managed Postgres through PgBouncer, run migrations, and query with Eloquent over verified TLS

export const BetaBadge = ({link, galaxyTrack, galaxyEvent}) => {
  if (link) {
    return <a href={link} target="_blank" rel="noopener noreferrer" className="betaBadge" onClick={galaxyTrack && galaxyEvent ? galaxyOnClick(galaxyEvent) : undefined}>
                <span>Beta</span>
            </a>;
  }
  return <a href="https://clickhouse.com/docs/reference/settings/beta-and-experimental-features#beta-features" className="betaBadge">
            <span>Beta feature</span>
        </a>;
};

<BetaBadge link="https://clickhouse.com/cloud/postgres" galaxyTrack={true} galaxyEvent="docs.managed-postgres.guides-laravel-beta" />

[Laravel](https://laravel.com/) 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.

<h2 id="prerequisites">
  Prerequisites
</h2>

* 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](https://getcomposer.org/)
* A ClickHouse Cloud account
* [`psql`](https://www.postgresql.org/download/), to create the database. You can also run the `CREATE DATABASE` statement in the [SQL console](/docs/integrations/connectors/sql-clients/sql-console).

<h2 id="create-service">
  Create a ClickHouse Managed Postgres service
</h2>

In the ClickHouse Cloud console, click **New service** and select **Postgres**. The instance is ready in a few minutes. See the [quickstart](/docs/products/managed-postgres/quickstart) for a walkthrough.

<h2 id="connection-details">
  Get your connection details
</h2>

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](/docs/products/managed-postgres/connection#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.

<h2 id="project-setup">
  Set up the project
</h2>

Create a Laravel project:

```bash theme={null}
composer create-project laravel/laravel laravel-managed-postgres
cd laravel-managed-postgres
```

<Tip>
  **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](#configure), 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.
</Tip>

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:

```bash theme={null}
psql "postgresql://postgres:<PASSWORD>@your-instance.pg.clickhouse.cloud:5432/postgres?sslmode=verify-full&sslrootcert=ca-certificate.pem" -c "CREATE DATABASE guide_laravel;"
```

```text theme={null}
CREATE DATABASE
```

<h2 id="configure">
  Configure the connection
</h2>

Laravel uses SQLite by default. Open `.env` and replace the `DB_CONNECTION=sqlite` line and the commented-out `DB_` lines below it with:

```bash title=".env" theme={null}
DB_CONNECTION=pgsql
DB_HOST=your-instance.pg.clickhouse.cloud
DB_PORT=6432
DB_DATABASE=guide_laravel
DB_USERNAME=postgres
DB_PASSWORD=<PASSWORD>
DB_SSLMODE=verify-full
DB_SSLROOTCERT=/absolute/path/to/your-project/ca-certificate.pem
```

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:

```php title="config/database.php" theme={null}
        'pgsql' => [
            'driver' => 'pgsql',
            'url' => env('DB_URL'),
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '5432'),
            'database' => env('DB_DATABASE', 'laravel'),
            'username' => env('DB_USERNAME', 'root'),
            'password' => env('DB_PASSWORD', ''),
            'charset' => env('DB_CHARSET', 'utf8'),
            'prefix' => '',
            'prefix_indexes' => true,
            'search_path' => 'public',
            'sslmode' => env('DB_SSLMODE', 'prefer'),
            'sslrootcert' => env('DB_SSLROOTCERT'),
            'options' => [
                PDO::ATTR_EMULATE_PREPARES => true,
            ],
        ],
```

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:

```bash theme={null}
php artisan db:show
```

```text theme={null}
  PostgreSQL ................................ 18.6 (Ubuntu 18.6-1.pgdg22.04+2)  
  Connection ........................................................... pgsql  
  Database ..................................................... guide_laravel  
  Host ..................................... your-instance.pg.clickhouse.cloud  
  Port .................................................................. 6432  
  Username .......................................................... postgres  
  URL ........................................................................  
  Open Connections ........................................................ 44  
  Tables ................................................................... 0  
```

<Note>
  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`.
</Note>

<h2 id="migrations">
  Create a model and migration
</h2>

Generate an Eloquent model and a migration for a `tasks` table:

```bash theme={null}
php artisan make:model Task -m
```

Open the new migration in `database/migrations/` (its name ends in `_create_tasks_table.php`) and add the `title` and `done` columns to `up`:

```php title="database/migrations/xxxx_xx_xx_xxxxxx_create_tasks_table.php" theme={null}
    public function up(): void
    {
        Schema::create('tasks', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->boolean('done')->default(false);
            $table->timestamps();
        });
    }
```

Replace `app/Models/Task.php` so that the columns can be mass-assigned and `done` is returned as a boolean:

```php title="app/Models/Task.php" theme={null}
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Model;

#[Fillable(['title', 'done'])]
class Task extends Model
{
    protected function casts(): array
    {
        return [
            'done' => 'boolean',
        ];
    }
}
```

Run the migrations over the direct connection. An environment variable overrides the value in `.env`, so you only need to change the port:

```bash theme={null}
DB_PORT=5432 php artisan migrate
```

```text theme={null}
   INFO  Preparing database.  

  Creating migration table ..................................... 289.95ms DONE

   INFO  Running migrations.  

  0001_01_01_000000_create_users_table ............................... 1s DONE
  0001_01_01_000001_create_cache_table ......................... 949.69ms DONE
  0001_01_01_000002_create_jobs_table ................................ 1s DONE
  2026_09_30_165735_create_tasks_table ......................... 240.59ms DONE
```

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.

<h2 id="query">
  Query the database
</h2>

Add a few rows with Eloquent from `artisan tinker`:

```bash theme={null}
php artisan tinker --execute="App\Models\Task::create(['title' => 'Create a Managed Postgres service', 'done' => true]); App\Models\Task::create(['title' => 'Connect Laravel with verify-full TLS', 'done' => true]); App\Models\Task::create(['title' => 'Ship the app']); echo App\Models\Task::count();"
```

```text theme={null}
3
```

Add a route that returns all tasks as JSON to the end of `routes/web.php`:

```php title="routes/web.php" theme={null}
Route::get('/tasks', function () {
    return App\Models\Task::orderBy('id')->get();
});
```

<Note>
  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](/docs/products/managed-postgres/faq#prepared-statement-errors).
</Note>

<h2 id="verify">
  Run and verify
</h2>

Start the development server:

```bash theme={null}
php artisan serve --port=3104
```

```text theme={null}
   INFO  Server running on [http://127.0.0.1:3104].  

  Press Ctrl+C to stop the server
```

Open `http://localhost:3104/tasks` in your browser. The response looks like this:

```json theme={null}
[
  {
    "id": 1,
    "title": "Create a Managed Postgres service",
    "done": true,
    "created_at": "2026-09-30T17:10:28.000000Z",
    "updated_at": "2026-09-30T17:10:28.000000Z"
  },
  {
    "id": 2,
    "title": "Connect Laravel with verify-full TLS",
    "done": true,
    "created_at": "2026-09-30T17:10:29.000000Z",
    "updated_at": "2026-09-30T17:10:29.000000Z"
  },
  {
    "id": 3,
    "title": "Ship the app",
    "done": false,
    "created_at": "2026-09-30T17:10:29.000000Z",
    "updated_at": "2026-09-30T17:10:29.000000Z"
  }
]
```

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`:

| id | title | done | created\_at | updated\_at |
| - | - | - | - | - |
| 1 | Create a Managed Postgres service | true | 2026-09-30 17:10:28 | 2026-09-30 17:10:28 |
| 2 | Connect Laravel with verify-full TLS | true | 2026-09-30 17:10:29 | 2026-09-30 17:10:29 |
| 3 | Ship the app | false | 2026-09-30 17:10:29 | 2026-09-30 17:10:29 |

<h2 id="next-steps">
  Next steps
</h2>

* [Connection](/docs/products/managed-postgres/connection): connection strings, PgBouncer, and TLS
* [Settings](/docs/products/managed-postgres/settings): Postgres and PgBouncer parameters, such as `max_connections`
* [Read replicas](/docs/products/managed-postgres/read-replicas): send reads to a replica with Laravel's [read and write connections](https://laravel.com/docs/database#read-and-write-connections)
* [Security](/docs/products/managed-postgres/security): IP access lists and private networking
