TypeORM_
Use TypeORM with an Appwrite native PostgreSQL database. Configure the DataSource, run migrations against the direct connection, and pool runtime traffic from serverless environments.
4 min read
Appwrite's native PostgreSQL database is a standard PostgreSQL engine, so TypeORM works against it with no Appwrite-specific configuration. Point TypeORM's pg driver at the connection string from the Connections page and use entities, the query builder, and migrations as you would against any PostgreSQL server.
You'll need a native PostgreSQL database in a ready state and its credentials. See native PostgreSQL databases to create one and Connections to retrieve the connection string. The primary user is admin, and the database name is generated per database.
Set the connection string
In the Console, open your database and click Credentials. Copy the connection string from the DSN or .env tab, or fetch it with postgresql.get(). Put it in your environment, never commit it:
DATABASE_URL="postgresql://admin:<password>@db-<hash>.<region>.appwrite.center:6432/<database>?sslmode=require"DIRECT_URL="postgresql://admin:<password>@db-<hash>.<region>.appwrite.center:5432/<database>?sslmode=require"Use the pooled DATABASE_URL (port 6432) for runtime traffic and DIRECT_URL (port 5432) for the TypeORM CLI and migration runners. The TLS parameter (sslmode=require) is part of the connection string Appwrite returns. Appwrite Cloud terminates TLS at the edge and forwards traffic to your database over the internal network. For full certificate verification (verify-full) or mTLS, see Network security.
Install and configure TypeORM
Install TypeORM and the PostgreSQL driver:
npm install typeorm pg reflect-metadatanpm install -D @types/node typescript ts-nodeEnable decorator metadata in tsconfig.json if you use entity decorators ("emitDecoratorMetadata": true and "experimentalDecorators": true).
Create a runtime DataSource that reads DATABASE_URL:
import 'reflect-metadata';import { DataSource } from 'typeorm';import { User } from './entities/User';
export const AppDataSource = new DataSource({ type: 'postgres', url: process.env.DATABASE_URL, ssl: { rejectUnauthorized: true }, synchronize: false, logging: false, entities: [User], migrations: ['src/migrations/**/*.ts'], subscribers: []});Create a separate migrations DataSource for the CLI that reads DIRECT_URL (for example src/data-source.migrations.ts):
import 'reflect-metadata';import { DataSource } from 'typeorm';import { User } from './entities/User';
export default new DataSource({ type: 'postgres', url: process.env.DIRECT_URL, ssl: { rejectUnauthorized: true }, synchronize: false, logging: false, entities: [User], migrations: ['src/migrations/**/*.ts'], subscribers: []});ssl: { rejectUnauthorized: true } validates the server certificate against Node's built-in CA store when the driver does not rely solely on sslmode=require in the URL. Keep synchronize: false outside of local prototypes. Schema changes in production and CI should go through TypeORM migrations against the direct engine port.
Add short package.json script aliases so you do not retype the long CLI:
{ "scripts": { "typeorm:generate": "typeorm-ts-node-commonjs migration:generate src/migrations/InitUsers -d src/data-source.migrations.ts", "typeorm:migrate": "typeorm-ts-node-commonjs migration:run -d src/data-source.migrations.ts" }}Define an entity:
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity('users')export class User { @PrimaryGeneratedColumn() id!: number;
@Column({ type: 'text', unique: true }) email!: string;
@CreateDateColumn({ name: 'created_at' }) createdAt!: Date;}Run migrations
Point the TypeORM CLI at the migrations DataSource so it uses DIRECT_URL, not the pooler. Migrations issue DDL that needs a session-level connection:
npm run typeorm:generatenpm run typeorm:migrateOr call the CLI directly:
npx typeorm-ts-node-commonjs migration:generate src/migrations/InitUsers -d src/data-source.migrations.tsnpx typeorm-ts-node-commonjs migration:run -d src/data-source.migrations.tsThe primary admin user owns the generated database and can run schema changes. Use narrower database roles for application traffic that does not need DDL privileges.
You can also run pending migrations from a one-off job that initializes the migrations DataSource:
import MigrationsDataSource from './data-source.migrations';
await MigrationsDataSource.initialize();await MigrationsDataSource.runMigrations();await MigrationsDataSource.destroy();Do not call AppDataSource.runMigrations() when DATABASE_URL points at the transaction-mode pooler.
Query with TypeORM
Once the schema is migrated, use the repository API or the query builder with the runtime DataSource:
import { AppDataSource } from './data-source';import { User } from './entities/User';
await AppDataSource.initialize();
const users = AppDataSource.getRepository(User);
const user = await users.save({ email: 'ada@example.com' });
const recent = await users.find({ order: { createdAt: 'DESC' }, take: 10});On long-running servers, initialize the DataSource once at module scope and reuse it. On serverless, keep a single initialized instance per module scope so warm invocations reuse it, and rely on the pooler to absorb cold-start connection churn. Always call destroy() during graceful shutdown on long-running processes.
Pool connections from serverless
Each running instance opens its own connections to the engine. On serverless and edge platforms (Vercel, Netlify, Cloudflare), short-lived instances can fan out into more backend connections than the engine allows. Route runtime traffic through the connection pooler. The primary DATABASE_URL already uses the pooler port (6432) on the same hostname.
The pooler defaults to transaction mode, which does not keep a backend connection across statements, so server-side prepared statements and session-scoped state are unreliable. Keep the migrations DataSource pointed at DIRECT_URL so DDL still runs over a session-level connection.
Wire the runtime DataSource to DATABASE_URL and the CLI / migrations DataSource to DIRECT_URL. Cap extra.max / pool size on the runtime DataSource so each serverless isolate opens only a few backend connections through the pooler.
If your application relies on advisory locks, LISTEN/NOTIFY, temporary tables, or SET LOCAL, switch the pooler to session mode or use the direct port. See the pooler page for the trade-offs.
Use a branch for previews and CI
Branches are 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:
- Create a branch from the API and read its
connectionString. - Export it as
DIRECT_URL(and the pooled variant asDATABASE_URL). - Run TypeORM migrations and 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 run against realistic data without touching production.
Related
Connections
Retrieve credentials, rotate the password, and create database roles.
Connection pooler
Pool modes, ports, and read/write splitting for serverless workloads.
Branches
Ephemeral database copies for preview environments and CI.
Network security
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.