Skip to main content
Sequelize is the database ORM: it loads all models, runs associations, and exposes a typed models object that every part of the backend uses to read and write data.

Why

Sequelize gives you a fully typed ORM over PostgreSQL with first-class migration support and soft-delete semantics built in. It’s the only database client supported by this stack, and every model, migration, and seed is colocated under apps/backend/src/tools/rds/sequelize/ for easy navigation.

Setup

  1. In development.ts, production.ts, and test.ts, set tools.rds:
    rds: {
      client: RDSClientType.SEQUELIZE,
      dialect: SequelizeDialectType.POSTGRES,
      host: process.env.PGHOST,
      port: Number(process.env.PGPORT),
      username: process.env.PGUSER,
      password: process.env.PGPASSWORD,
      database: process.env.PGDATABASE,
    }
    
  2. In compose.yml, the db service provides PostgreSQL:
    db:
      image: postgres:16
      environment:
        POSTGRES_USER: ${PGUSER}
        POSTGRES_PASSWORD: ${PGPASSWORD}
        POSTGRES_DB: ${PGDATABASE}
      ports:
        - 5432:5432
      healthcheck:
        test: ["CMD-SHELL", "pg_isready -U ${PGUSER}"]
        interval: 2s
        timeout: 5s
        retries: 10
    
  3. In .env.development, set:
    PGHOST=db
    PGPORT=5432
    PGUSER=postgres
    PGPASSWORD=postgres
    PGDATABASE=app_development
    
  4. Run migrations: make migrate.
  5. Run make test module=backend.

Adding a new model

  1. Create apps/backend/src/tools/rds/sequelize/models/<ModelName>/index.ts (or a flat file for simple models). Extend Model from Sequelize, define attributes, and call ModelName.init(...) with the table name in snake_case plural.
  2. Register associations in the model file’s associate static method, called automatically by the model loader.
  3. Generate a migration: copy the closest existing migration in apps/backend/src/tools/rds/sequelize/migrations/ and name it <timestamp>-<describe-change>.ts.
  4. Export the model from apps/backend/src/tools/rds/sequelize/models/index.ts.
  5. Run make migrate to apply.
  6. Run make test module=backend.

Migrations

Migrations live in apps/backend/src/tools/rds/sequelize/migrations/ and are run in timestamp order. Run them with:
make migrate         # apply all pending migrations
make migrate-down    # roll back the last migration
Never edit a migration that has already been applied in production — create a new one instead.

Soft deletes (paranoid models)

Models marked paranoid: true set a deleted_at timestamp on deletion instead of removing the row. Sequelize automatically excludes soft-deleted rows from all queries unless you explicitly pass paranoid: false:
// Returns only active members (default behaviour):
await tools.rds.models.OrganizationMember.findAll({ where: { organization_id } });

// Returns ALL members including revoked (only when you need it):
await tools.rds.models.OrganizationMember.findAll({
  where: { organization_id },
  paranoid: false,
});

Gotchas

  • Always pass the active transaction to every ORM call inside a transaction block. Omitting it causes dirty reads and partial writes.
  • Never run sequelize.sync() in production — use migrations.
  • The verbose: true config flag logs every SQL query. Useful for debugging, but leave it off in production.
  • PGPORT must be a number. The config parses it with Number(process.env.PGPORT) — ensure your .env has no quotes around it.

What’s next?