Oct 26, 20267 min read

How to Add Postgres to a Flask App

Connecting a Flask app to PostgreSQL with Encore, Flask-SQLAlchemy with Flask-Migrate, or psycopg 3 with a connection pool, plus migrations, local databases and production.

Flask leaves the database to you, so every Flask project assembles the same pieces around Postgres. That means a driver and maybe an ORM, a home for migrations, a database for each developer and CI run, and a production instance with its network and credentials.

This guide covers Encore, Flask-SQLAlchemy with Flask-Migrate, and psycopg 3 with a connection pool. Each version uses the same books table.

What you need to decide:

  • ORM models or SQL you write yourself.
  • Which tool owns migrations.
  • How connection details reach the app in each environment.
  • Who creates and secures the production database.

Postgres options for Flask at a glance

CapabilityEncoreFlask-SQLAlchemy with Flask-Migratepsycopg 3 with psycopg_pool
Query styleSQL with db.query, or SQLAlchemy through db.connection_stringORM models and sessionsSQL
MigrationsSQL files applied on encore run and deployAlembic through flask dbBring your own
Connection detailsFrom the declarationSQLALCHEMY_DATABASE_URIDATABASE_URL
Local Postgresencore run starts itYou run itYou run it
Production databaseProvisioned in your AWS or GCP accountYou provision itYou provision it

Encore: Postgres for your existing Flask app

Encore

Encore combines a Postgres database declared in your Flask code with the server behind it, so encore run starts Postgres locally with your migrations applied and a deploy creates RDS or Cloud SQL in your own account. It is an infrastructure SDK added to an existing app, and Flask keeps its blueprints, views and extensions.

Encore declares infrastructure in Python code, runs it locally and provisions it in your AWS or GCP account
Infrastructure declared in Python, running locally under encore run and provisioned in your own AWS or GCP account on deploy.

The SDK's query API is async, so Flask needs its async extra, and the example serves the app with Waitress:

[project] name = "books" version = "0.1.0" requires-python = ">=3.9" dependencies = [ "flask[async]>=3.1", "waitress>=3.0", "encore-dev", "flask-sqlalchemy>=3.1", "psycopg[binary]>=3.2", ]

Migrations are plain SQL files in ./migrations, numbered in the order they apply:

-- migrations/1_create_books.up.sql CREATE TABLE books ( id BIGSERIAL PRIMARY KEY, title TEXT NOT NULL, author TEXT NOT NULL );

The database is declared once at module level. Views query it with db.query(...) and pick .one(), .first() or .all() for the shape of the result, and parameters are passed separately from the SQL, so values are never interpolated into the query:

import os from encore.storage import postgres from flask import Flask, abort, request from flask_sqlalchemy import SQLAlchemy from sqlalchemy import String, func, select from sqlalchemy.engine import make_url from sqlalchemy.orm import Mapped, mapped_column from waitress import serve # Encore provisions this database, locally under `encore run` and in your # cloud on deploy, and applies the SQL files in ./migrations first. db = postgres.Database("books", migrations="./migrations") app = Flask(__name__) @app.post("/books") async def create_book(): data = request.get_json() row = await db.query( "INSERT INTO books (title, author) VALUES ($1, $2) RETURNING id", data["title"], data["author"], ).one() return {"id": row["id"]}, 201 @app.get("/books/<int:book_id>") async def get_book(book_id: int): row = await db.query( "SELECT id, title, author FROM books WHERE id = $1", book_id ).first() if row is None: abort(404) return {"id": row["id"], "title": row["title"], "author": row["author"]} @app.get("/books") async def list_books(): rows = await db.query("SELECT id, title, author FROM books ORDER BY id").all() return [{"id": r["id"], "title": r["title"], "author": r["author"]} for r in rows]

Projects that already use Flask-SQLAlchemy keep their models. db.connection_string points SQLAlchemy at the same database through psycopg 3, so ORM code and SDK queries share one Postgres:

# Optional: hand the same database to Flask-SQLAlchemy through psycopg 3. app.config["SQLALCHEMY_DATABASE_URI"] = make_url(db.connection_string).set( drivername="postgresql+psycopg" ) orm = SQLAlchemy(app) class Book(orm.Model): __tablename__ = "books" id: Mapped[int] = mapped_column(primary_key=True) title: Mapped[str] = mapped_column(String) author: Mapped[str] = mapped_column(String) @app.get("/books/count") def count_books(): total = orm.session.scalar(select(func.count()).select_from(Book)) return {"count": total} if __name__ == "__main__": # Encore's local run sets ENCORE_LISTEN_ADDR to the "host:port" to bind. host, _, port = os.environ.get("ENCORE_LISTEN_ADDR", "0.0.0.0:8000").rpartition(":") serve(app, host=host or "0.0.0.0", port=int(port))

encore run starts Postgres locally and applies the migration before the app takes requests:

~/books
$ encore runSTART postgres db books migrated 66ms$ curl localhost:4000/books/3{"author":"Miguel Grinberg","id":3,"title":"Flask Web Development"}$ curl localhost:4000/books/count{"count":3}

Each developer gets their own local database, so an agent that adds a column writes the migration and the view, and checks both against Postgres on its own machine. On deploy, Encore creates RDS on AWS or Cloud SQL on GCP in your own account, and applies the same migrations. If the app already has an RDS database, Encore can import the existing instance instead of creating a new one, and each environment gets its own database.

Good to know

Encore runs the SQL migrations in ./migrations, so the schema lives there rather than in orm.create_all() or Alembic. The ORM in this setup maps onto tables the migrations create.

Try Encore →

Flask-SQLAlchemy with Flask-Migrate

Flask-SQLAlchemy is the usual ORM choice, and Flask-Migrate wraps Alembic in flask db commands. The database URL comes from the environment:

import os from flask import Flask from flask_migrate import Migrate from flask_sqlalchemy import SQLAlchemy from sqlalchemy import String from sqlalchemy.orm import Mapped, mapped_column app = Flask(__name__) app.config["SQLALCHEMY_DATABASE_URI"] = os.environ["DATABASE_URL"] app.config["SQLALCHEMY_ENGINE_OPTIONS"] = {"pool_pre_ping": True, "pool_size": 5} db = SQLAlchemy(app) migrate = Migrate(app, db) class Book(db.Model): __tablename__ = "books" id: Mapped[int] = mapped_column(primary_key=True) title: Mapped[str] = mapped_column(String) author: Mapped[str] = mapped_column(String)

Migrations are generated from the models with flask db migrate -m "create books" and applied with flask db upgrade.

Good to know

Set DATABASE_URL with the postgresql+psycopg:// scheme to use psycopg 3, since plain postgresql:// selects psycopg2. Autogenerated migrations miss some changes, such as renamed columns, so read each one before committing it.

psycopg 3 with psycopg_pool

Without an ORM, psycopg_pool keeps a pool of connections per process, opened once at startup:

import os from flask import Flask, abort from psycopg.rows import dict_row from psycopg_pool import ConnectionPool app = Flask(__name__) pool = ConnectionPool(os.environ["DATABASE_URL"], min_size=1, max_size=5, open=True) @app.get("/books/<int:book_id>") def get_book(book_id: int): with pool.connection() as conn: row = conn.execute( "SELECT id, title, author FROM books WHERE id = %s", (book_id,), ).fetchone() if row is None: abort(404) return {"id": row[0], "title": row[1], "author": row[2]}

Good to know

The pool is per process, so a Gunicorn server with four workers holds up to four times max_size connections. Migrations need a tool of their own, such as Alembic on its own or a SQL migration runner.

Async views and Flask

Flask supports async def views when installed with the async extra, and the Flask documentation on async explains that each request still occupies one worker, because Flask runs the coroutine in an event loop per request rather than serving many requests on one loop. That is fine for awaiting a database query, and an app that needs high concurrency on one process is better served by an ASGI framework such as Quart or FastAPI.

Postgres in production

On AWS this is usually RDS in private subnets with the password in Secrets Manager and a security group that admits only the app, and on GCP it is Cloud SQL with the Cloud SQL Client role on the service account. RDS enforces SSL by default on PostgreSQL 15 and later through rds.force_ssl, so the connection string needs sslmode=require or stricter. With Encore, the instance, network rules and credentials are created in your account and cloud access sits with Encore's IAM role, not the agent.

How to choose

  • Encore if you want Postgres running locally and in your AWS or GCP account from one declaration, with SQL migrations and optional SQLAlchemy on the same database.
  • Flask-SQLAlchemy with Flask-Migrate if you want ORM models and Alembic migrations and are happy to run the database yourself.
  • psycopg 3 with psycopg_pool if you prefer plain SQL and minimal dependencies.

Python support in Encore is in beta, and you can sign up for access to try it with your Flask app.

Frequently asked questions

How do I connect Flask to PostgreSQL?

Install psycopg 3, then either set SQLALCHEMY_DATABASE_URI for Flask-SQLAlchemy or open a psycopg connection pool when the app starts, with the connection details from the environment. With Encore, declare db = postgres.Database("books", migrations="./migrations") and query it with await db.query(sql, *args).one(), .first() or .all() from async Flask views. encore run starts Postgres locally and applies the migrations, and db.connection_string hands the same database to Flask-SQLAlchemy.

Should I use Flask-SQLAlchemy or raw SQL with Flask?

Flask-SQLAlchemy gives you models, sessions and Alembic migrations through Flask-Migrate, which suits most CRUD apps. Raw SQL through psycopg or a query API keeps the SQL visible and avoids an ORM layer, which some teams prefer for reporting queries and performance-sensitive paths.

Which Postgres driver should Flask use?

psycopg 3 for new projects. SQLAlchemy selects it with the postgresql+psycopg driver name, and psycopg_pool provides a connection pool when you use it directly.

How do I run Postgres locally for a Flask app?

Install Postgres or run it in a container and point DATABASE_URL at it. With Encore, encore run starts Postgres locally for each database the code declares and applies the SQL migrations before the app starts.

Deploy to your cloud on AWS and Google Cloud
without managing infrastructure

Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders