Docs
Skip to content

PostgreSQL

Rails_

Use Ruby on Rails and ActiveRecord with an Appwrite native PostgreSQL database. Configure database.yml, run migrations against the direct database port, and size the ActiveRecord pool.

5 min read

Raw

A native PostgreSQL database is a standard PostgreSQL engine, so Ruby on Rails works against it through ActiveRecord with no Appwrite-specific configuration. Point config/database.yml at the connection details from the Connections page and use ActiveRecord, migrations, and the rest of the Rails toolchain exactly as you would against any PostgreSQL server.

Install the database driver

ActiveRecord talks to PostgreSQL through the pg driver gem. Add it to your Gemfile:

Ruby
# Gemfile
gem 'pg'

The pg gem builds against libpq, so the PostgreSQL client headers must be available at install time.

Then install:

Bash
bundle install

Set the connection string

Copy the connection string from the Console Credentials dialog, or fetch it with the API. Keep it in an environment variable and do not commit it:

.env
DATABASE_URL="postgresql://admin:<password>@db-<hash>.<region>.appwrite.center:5432/<database>?sslmode=require"

The TLS parameter (sslmode=require) is already part of the string Appwrite returns. Appwrite Cloud terminates TLS at the edge, so no extra certificate configuration is needed. For full certificate verification (verify-full) or mTLS, see the Network page.

Configure database.yml

Rails reads DATABASE_URL automatically. The simplest configuration points the url at the environment variable and lets ActiveRecord parse the host, port, database, and credentials out of it:

YAML
# config/database.yml
production:
adapter: postgresql
url: <%= ENV["DATABASE_URL"] %>
pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %>

If you'd rather set the fields explicitly, the discrete keys map one-to-one to the values from the Connections response. Use ERB to read each value from the environment so no secret lands in source control:

YAML
# config/database.yml
production:
adapter: postgresql
host: <%= ENV["DB_HOST"] %> # db-<hash>.<region>.appwrite.center
port: <%= ENV.fetch("DB_PORT", 5432) %>
database: <%= ENV["DB_NAME"] %> # <database>
username: <%= ENV.fetch("DB_USER", "admin") %>
password: <%= ENV["DB_PASSWORD"] %>
sslmode: require
pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %>

When both DATABASE_URL and explicit keys are present, Rails merges them. sslmode and pool can still be set in database.yml, so a url-based config can carry the TLS parameter in the string and override pool in the YAML.

Size the connection pool

ActiveRecord manages a per-process connection pool. The pool: value caps how many backend connections a single Rails process holds, and it defaults to 5. It must be large enough for every thread that checks out a connection, your Puma worker threads plus any background job threads in the same process.

YAML
production:
adapter: postgresql
url: <%= ENV["DATABASE_URL"] %>
pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %>

Tying pool to RAILS_MAX_THREADS keeps it aligned with Puma's thread count. Each Puma worker is a separate process with its own pool, so the backend connection count is roughly pool × workers × server instances. Keep that product within your specification's maxConnections, see specifications.

Run migrations

Generate and apply migrations the usual way:

Bash
bin/rails db:migrate

Migrations issue DDL and need a session-level connection, so run them against the direct PostgreSQL port 5432, not the transaction-mode pooler. Point DATABASE_URL (or a separate migration URL) at the direct port when you run db:migrate. The primary admin user owns the database and can run schema changes. Narrower database roles should only receive the privileges your application needs.

Use ActiveRecord

Once database.yml is configured, models work with no further setup. Define a migration and model, then query through ActiveRecord:

Ruby
# db/migrate/20240101000000_create_users.rb
class CreateUsers < ActiveRecord::Migration[7.1]
def change
create_table :users do |t|
t.string :email, null: false
t.timestamps
end
add_index :users, :email, unique: true
end
end

Define the matching model:

Ruby
# app/models/user.rb
class User < ApplicationRecord
validates :email, presence: true, uniqueness: true
end

Create and query records as usual:

Ruby
User.create!(email: 'ada@example.com')
recent = User.order(created_at: :desc).limit(10)

ActiveRecord opens connections lazily and reuses them from the pool, so a long-running Puma server keeps a small, stable set of backend connections rather than opening one per request.

Pooling for a long-running server

A Rails app under Puma is a long-running process: it holds an ActiveRecord pool for its lifetime. That pairs naturally with the direct PostgreSQL port 5432, sized so pool × workers stays within your connection budget. This is the recommended setup for a persistent server.

If you instead route through the connection pooler to absorb spikes or many app instances, prefer session mode, which keeps a backend connection for the whole client session and behaves like a direct connection to ActiveRecord. The pooler defaults to transaction mode, which hands out a different backend connection per transaction. ActiveRecord uses server-side prepared statements by default, and those are tied to one backend connection, so on the transaction-mode pooler you must disable them:

YAML
production:
adapter: postgresql
url: <%= ENV["DATABASE_URL"] %> # pooler host, port 6432
pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %>
prepared_statements: false

See the pooler page for the mode trade-offs. Whichever runtime connection you choose, always run bin/rails db:migrate against the direct PostgreSQL port.

Use a branch for previews and CI

Branches are instant, 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 DATABASE_URL for the job.
  3. Run bin/rails db:migrate and your test suite against the branch.
  4. Delete the branch when the job finishes.

A branch has no pooler and exposes the PostgreSQL port directly, which gives migrations the session-level connection they need. Because a branch starts from a storage snapshot, the schema and data match the source database at branch time, so migrations run against realistic data without touching production.

For ActiveRecord and migration details beyond this guide, see the Rails configuration guide.

Was this page helpful?

Share what worked or what we should fix. Once approved, our agents automatically apply suggested updates to the docs.