Data & TypeORM Guidelines¶
This is the canonical engineering guideline for working with the persistence layer of the
Mr. Mentor / MAS backend. It covers the TypeORM DataSource singleton and its connection
pool, the very important implications of running with synchronize: true in this codebase,
entity-authoring conventions (primary keys, timestamps, enums, relations, cascades, indexes),
repository and transaction patterns, entity subscribers / lifecycle hooks, and database
seeding. Read this before adding or changing any entity — schema changes here apply to a
shared, multi-product PostgreSQL instance with auto-sync turned on.
Status: documented from source on this branch.
Overview¶
Every product in this monorepo backend (Mr. Mentor mentorship, Mr. Hire recruitment, MAS LMS,
Sales CRM, Finance, the AI platform, the workflow engine, the vendor API platform) persists to
one PostgreSQL database through a single shared TypeORM DataSource. There is no
per-domain database; isolation is achieved with Postgres schemas (public, mas_crm,
mrlearn, mrtest, superadmin) and naming conventions rather than separate connections.
- Who uses this layer: every controller, service, worker, socket handler and seed script.
Services receive (or fetch) the
DataSourceand callgetRepository(Entity). - Where it sits: the
DatabaseServicesingleton (src/config/database.ts) is initialized first during app startup (src/index.ts), before Redis, BullMQ workers, and the HTTP / Socket.IO server. Nothing else can run until theDataSourceis connected and (auto-)synced. - Key fact that dominates everything below:
synchronize: trueis hard-coded on. On every boot, TypeORM diffs the entity metadata against the live schema and issuesALTER/CREATEstatements automatically. There are no migration files in normal operation. This is fast for development but is the single largest source of operational risk in this backend.
Defined entity count is large (the entities array in src/config/database.ts lists well
over 130 classes spanning all products). Treat the entity list in that file as the source of
truth: an entity is only synced if it is both imported and added to the entities array.
Key concepts & entities¶
| Term | Meaning |
|---|---|
DataSource |
TypeORM's connection manager. One instance, wrapped by DatabaseService (src/config/database.ts). |
DatabaseService |
Singleton (getInstance()) owning the DataSource; exposes initialize(), close(), isConnected(). |
synchronize: true |
TypeORM auto-applies entity → schema diffs on connect. ON in this repo for all environments. |
| Repository | dataSource.getRepository(Entity) — the primary data-access API used across services. |
| Entity | A @Entity('table_name')-decorated class in src/entities/. Maps 1:1 to a Postgres table. |
| Subscriber | An EntitySubscriberInterface reacting to lifecycle events (afterInsert, etc.). Registered in the subscribers array. |
| Named schema | A non-public Postgres schema (mas_crm, mrlearn, mrtest, superadmin). Must be pre-created — synchronize does not create schemas, only tables. |
| Pool | The pg connection pool: min: 5, max: 20, 30s idle timeout, 10s connect timeout. |
Representative entities to study as templates:
src/entities/User.ts— UUID PK, enums (UserRole,UserStage), self-referencingManyToOne(salesHead),select: falseonpassword, manual timestamp columns,@Index.src/entities/Slots.ts— twoManyToOnetoUserwithonDelete: 'CASCADE', an enum status (MeetingStatus),OneToOne/OneToManyinverse relations.src/entities/TokenUsage.ts— ledger row pattern (balanceBefore/balanceAfter),onDelete: 'SET NULL'for a nullable FK.src/entities/DiscountRequest.ts— multiple named@Index,decimalmoney columns, documented status lifecycle in a comment block.src/entities/Mas101PapWorkflow.ts—@CreateDateColumn/@UpdateDateColumn, a unique composite-style@Index, and a rich status enum.
Architecture¶
How a request reaches the database through the shared DataSource.
flowchart TD
REQ["HTTP request / Socket event / BullMQ job"] --> CTRL["Controller (src/controllers)"]
CTRL --> SVC["Service (src/services)"]
SVC -->|"getRepository(Entity)"| REPO["TypeORM Repository"]
SVC -->|"dataSource.transaction(...)"| TX["Transactional EntityManager"]
REPO --> DS["DataSource singleton (DatabaseService)"]
TX --> DS
DS --> POOL["pg connection pool (min 5 / max 20)"]
POOL --> PG[("PostgreSQL")]
subgraph SUB["Entity lifecycle"]
SUBS["Subscribers (UserSubscriber, ApplicationSubscriber)"]
end
DS -. "fires events" .-> SUBS
SUBS -->|"event.manager"| PG
subgraph SCHEMAS["Postgres schemas (one DB)"]
PUB["public"]
CRM["mas_crm"]
ML["mrlearn"]
MT["mrtest"]
SA["superadmin"]
end
PG --- SCHEMAS
BOOT["App startup (src/index.ts)"] -->|"initialize() then synchronize"| DS
BOOT --> SEED["Seed scripts (src/seeding)"]
SEED --> DS
Data model¶
The persistence layer itself does not "own" a domain table set; instead it is the substrate for every domain. Below is an illustrative ER snippet using a few real entities to show the conventions you should follow (UUID PKs, FK columns paired with relation objects, enums for status). For full per-domain models see data-model.md.
erDiagram
USER ||--o| TOKEN : "owns balance"
USER ||--o{ TOKEN_USAGE : "ledger of"
USER ||--o{ SLOT : "mentors"
USER ||--o{ SLOT : "books as student"
SLOT ||--o{ TOKEN_USAGE : "consumed by"
USER ||--o| MENTOR : "expert profile"
APPLICATION ||--o{ DISCOUNT_REQUEST : "has"
USER ||--o{ USER : "salesHead of"
USER {
uuid id PK
string email UK
string password "select false"
enum role "UserRole"
enum stage "UserStage"
uuid salesHeadId FK
timestamp createdAt
timestamp updatedAt
}
SLOT {
uuid id PK
uuid mentorId FK
uuid studentId FK
enum status "MeetingStatus"
bool isAvailable
bool earningsCredited
}
TOKEN_USAGE {
uuid id PK
uuid userId FK
uuid slotId FK
enum usageType "TokenUsageType"
int tokensUsed
int balanceBefore
int balanceAfter
}
DISCOUNT_REQUEST {
uuid id PK
uuid applicationId FK
uuid batchId FK
decimal requestedPrice
decimal approvedPrice
enum status "DiscountRequestStatus"
}
Notable enum/status patterns (always model status as a TypeScript enum mapped to a Postgres
enum column with a default):
MeetingStatusinsrc/entities/Slots.ts(tentative,confirmed,completed, ...).TokenUsageTypeinsrc/entities/TokenUsage.ts(meeting_booking,penalty,refund, ...).DiscountRequestStatusinsrc/entities/DiscountRequest.ts(pending/approved/rejected).Mas101PapWorkflowStatusand friends insrc/entities/Mas101PapWorkflow.ts.
API surface¶
This is an internal data layer, not an HTTP surface — there are no routes that belong to it.
The relevant "API" is the TypeORM DataSource / Repository API plus the DatabaseService
helpers:
| Symbol | Where | Purpose |
|---|---|---|
DatabaseService.getInstance() |
src/config/database.ts |
Get the singleton owning the DataSource. |
.initialize() |
DatabaseService |
Ensure named schemas, pre-sync data fixes, then connect + auto-sync. |
.dataSource.getRepository(Entity) |
TypeORM | Primary data-access handle used in services. |
.dataSource.transaction(cb) |
TypeORM | Run multiple writes atomically. |
.dataSource.createQueryRunner() |
TypeORM | Manual transaction / raw SQL with explicit control. |
.isConnected() / .close() |
DatabaseService |
Health checks and graceful shutdown. |
Operational-facing endpoints that touch this layer indirectly: /api/health (connection
liveness) and /api/redis/* (cache admin). Those belong to other docs.
User journeys¶
Journey 1 — App startup, schema bootstrap, and auto-sync¶
The most consequential journey: how the schema is brought up to match the entities on every
boot. Note the deliberate ordering — named schemas and a pre-sync data fix must run before
synchronize so the auto-diff does not fail.
sequenceDiagram
participant Boot as src/index.ts
participant DB as DatabaseService
participant PG as PostgreSQL
participant Seed as Seed scripts
Boot->>DB: initialize
DB->>PG: ensureNamedSchemas for mrlearn mrtest mas_crm superadmin
Note over DB,PG: synchronize creates tables only not schemas so they must exist first
DB->>PG: migrateSuperadminRoleBeforeSync updates superadmin rows to admin
Note over DB,PG: enum recreate would fail on stale label so fix data first
DB->>PG: dataSource.initialize triggers schema auto-sync
PG-->>DB: schema diffed and ALTER or CREATE applied
DB->>PG: migrateLegacyRawLeadsToMasCrm one-shot idempotent copy
DB-->>Boot: connection established
Boot->>Seed: seedColleges seedUsers seedJobTemplates and more
Seed->>PG: insert if missing
Boot->>Boot: init Redis then workers then HTTP and Socket.IO
Journey 2 — A service reads and writes through a repository¶
The everyday path. A service obtains a repository from the shared DataSource and performs
reads/writes; the connection pool hands out and recycles connections automatically.
sequenceDiagram
participant Ctrl as Controller
participant Svc as Service
participant Repo as Repository
participant Pool as pg pool
participant PG as PostgreSQL
Ctrl->>Svc: call method with validated input
Svc->>Repo: getRepository Entity from dataSource
Svc->>Repo: findOne with where clause
Repo->>Pool: acquire connection
Pool->>PG: SELECT
PG-->>Pool: row
Pool-->>Repo: result then release connection
Repo-->>Svc: entity or null
Svc->>Repo: save updated entity
Repo->>PG: INSERT or UPDATE
PG-->>Repo: persisted row
Svc-->>Ctrl: return DTO
Journey 3 — Atomic multi-write with a transaction¶
When several rows must change together (for example a token deduction plus a ledger row plus a
slot booking), wrap them in dataSource.transaction so partial failures roll back. This is the
preferred pattern over manual query runners for most cases.
sequenceDiagram
participant Svc as Service
participant DS as DataSource
participant TX as Transactional manager
participant PG as PostgreSQL
Svc->>DS: transaction with async callback
DS->>TX: open transaction and BEGIN
Svc->>TX: getRepository User then update balance
TX->>PG: UPDATE users
Svc->>TX: getRepository TokenUsage then insert ledger row
TX->>PG: INSERT token_usage
alt all writes succeed
DS->>PG: COMMIT
DS-->>Svc: callback result
else any write throws
DS->>PG: ROLLBACK
DS-->>Svc: error propagated
end
Journey 4 — Manual query runner for fine-grained control¶
For batch imports and raw SQL (for example the legacy CRM migration in database.ts, or
createQueryRunner usage in services like TokenAllotmentRequestService), use a query runner
to control connect, transaction boundaries, and release explicitly.
sequenceDiagram
participant Svc as Service
participant QR as QueryRunner
participant PG as PostgreSQL
Svc->>QR: createQueryRunner
Svc->>QR: connect
Svc->>QR: startTransaction
Svc->>QR: query raw SQL or manager save
QR->>PG: execute statements
alt success
Svc->>QR: commitTransaction
else failure
Svc->>QR: rollbackTransaction
Note over Svc,QR: rethrow after rollback so caller sees the error
end
Svc->>QR: release always in finally
Journey 5 — Entity subscriber side-effects on insert¶
Some cross-entity invariants are enforced by subscribers rather than service code. When a
User is inserted, UserSubscriber ensures a zero-balance Token row exists. When an
Application is inserted, ApplicationSubscriber auto-creates a CRM Lead. Subscribers run
inside the same transaction as the triggering write and must use event.manager, not a
fresh repository, to stay transactional.
sequenceDiagram
participant Svc as Service
participant Repo as User repository
participant DS as DataSource
participant Sub as UserSubscriber
participant PG as PostgreSQL
Svc->>Repo: save new User
Repo->>PG: INSERT users
DS->>Sub: afterInsert event with entity and manager
Sub->>PG: find Token for userId using event.manager
alt no token exists
Sub->>PG: insert Token with balance 0
else token exists
Note over Sub: no-op
end
Sub-->>DS: resolve
DS-->>Svc: save completes
Background jobs & async¶
The data layer is not itself a queue, but several async paths write through the shared
DataSource:
- BullMQ workers (
src/workers/*) —database.worker,cleanup.worker,kpi.worker,email.worker,resumeAnalysis.worker— all use the same singletonDataSource. They run in the same process, so they share themin 5 / max 20pool; long-running heavy jobs can starve the pool. See background-jobs-and-queues.md. - Cleanup job (every 24h) removes expired slots — a bulk write path; prefer query builder or query runner with batching over loading every row.
- KPI job (every 15min) aggregates dashboard metrics — read-heavy; use aggregate SQL, not N+1 entity loads.
- External sync runs (
MrLearnSyncService,MrTestSyncService) read theDataSourceviaDatabaseService.getInstance().dataSourceand write into the dedicatedmrlearn/mrtestschemas.
There is no migration runner job. The migrations array in database.ts is populated only
if a migrations/ directory exists (it does not in normal dev), and synchronize: true means
migrations are never the active mechanism.
External integrations¶
| Concern | Detail |
|---|---|
| PostgreSQL | Single instance. Connection via DB_HOST, DB_PORT, DB_USERNAME, DB_PASSWORD, DB_NAME. Defaults (shubham/template) are dev-only fallbacks — production sets all five. |
| Connection pool | extra: { max: 20, min: 5, idleTimeoutMillis: 30000, connectionTimeoutMillis: 10000 }. Tune max only with awareness of Postgres max_connections across all app instances + workers. |
| SSL | Currently commented out (ssl: { rejectUnauthorized: false }). Enable per environment if the DB requires TLS. |
pg raw client |
database.ts opens short-lived raw pg.Client connections for ensureNamedSchemas and the pre-sync superadmin role fix, because these must run before the synced DataSource connects. |
| Logging | logging: false. Turn on selectively when debugging query issues; it is verbose. |
There are no feature flags governing the data layer itself, but several domains key off rows in
config tables (SystemConfig, AiScreeningConfig, etc.) rather than env vars.
Status lifecycles¶
synchronize-managed enums are how status is modeled. The example below is the
DiscountRequestStatus lifecycle from src/entities/DiscountRequest.ts — a typical
approval-flow enum. Note the gotcha it implies: removing an enum label while rows still hold
it makes the auto-sync enum-recreate fail (the same hazard the startup superadmin-role
migration guards against for UserRole).
stateDiagram-v2
[*] --> pending : sales creates request
pending --> approved : admin approves and sets approvedPrice
pending --> rejected : admin rejects with note
rejected --> pending : sales retries with new request
approved --> [*] : Application finalAmount updated
rejected --> [*]
A second example — the meeting/slot status enum (MeetingStatus in src/entities/Slots.ts):
stateDiagram-v2
[*] --> tentative : slot created
tentative --> meeting_requested : student requests
meeting_requested --> under_review : pending approval
under_review --> confirmed : approved
tentative --> confirmed : direct booking
confirmed --> cancellation_requested : either party requests cancel
cancellation_requested --> cancelled : approved
confirmed --> completed : meeting finished
completed --> [*]
cancelled --> [*]
Entity anatomy — annotated reference¶
Use this as the template for new entities. It encodes every convention this codebase follows.
// src/entities/Example.ts
import {
Entity, PrimaryGeneratedColumn, Column,
ManyToOne, OneToMany, JoinColumn, Index,
CreateDateColumn, UpdateDateColumn,
} from 'typeorm';
import { User } from './User';
// 1. Status is ALWAYS a TS enum mapped to a Postgres enum column with a default.
export enum ExampleStatus {
PENDING = 'pending',
ACTIVE = 'active',
CLOSED = 'closed',
}
// 2. Declare named indexes at class level for columns you filter/join on a lot.
@Index('IDX_EXAMPLE_OWNER', ['ownerId'])
@Index('IDX_EXAMPLE_STATUS', ['status'])
@Entity('examples') // 3. Explicit snake_case-ish table name, plural.
export class Example {
// 4. UUID primary key is the house standard (NOT auto-increment int).
@PrimaryGeneratedColumn('uuid')
id: string;
// 5. Always specify column type, length for varchar, and nullability explicitly.
@Column({ type: 'varchar', length: 255 })
title: string;
@Column({ type: 'text', nullable: true })
description?: string;
// 6. Money: decimal with explicit precision/scale, never float.
@Column({ type: 'decimal', precision: 10, scale: 2, nullable: true })
amount?: number;
// 7. Enum column with a default so existing rows backfill cleanly under auto-sync.
@Column({ type: 'enum', enum: ExampleStatus, default: ExampleStatus.PENDING })
status: ExampleStatus;
// 8. FK pattern: a raw scalar column PLUS the relation object on the same name.
// The scalar lets you set/read the id without loading the relation.
@Column({ type: 'uuid' })
ownerId: string;
@ManyToOne(() => User, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'ownerId' })
owner: User;
// 9. Inverse side of a one-to-many. No DB column; lazy/eager off by default.
@OneToMany(() => Example, (e) => e.owner)
children: Example[];
// 10. Timestamps: prefer the decorator columns for new entities. (Legacy
// entities use manual CURRENT_TIMESTAMP columns — both are acceptable,
// but be consistent within an entity.)
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
Two timestamp styles coexist in the codebase — know both:
// Legacy/manual style (e.g. User.ts, TokenUsage.ts)
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
createdAt: Date;
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP', onUpdate: 'CURRENT_TIMESTAMP' })
updatedAt: Date;
// Decorator style (e.g. Mas101PapWorkflow.ts) — preferred for new code
@CreateDateColumn() createdAt: Date;
@UpdateDateColumn() updatedAt: Date;
Other conventions observed in real entities:
- Sensitive columns use
select: false(e.g.User.password) so they are excluded from defaultfindresults; load them explicitly withaddSelectwhen needed. - Cascade choices are intentional:
onDelete: 'CASCADE'for owned children (Slots→User),onDelete: 'SET NULL'for optional references that should survive parent deletion (TokenUsage.slotId). - Soft deletes are NOT used anywhere in this codebase — there are no
@DeleteDateColumncolumns. Deletions are hard. If you need recoverable deletes, add an explicitisActive/ status flag (asUser.isActivedoes) rather than introducing soft-delete semantics. jsonbcolumns are used for flexible/extra data (extraData, config blobs, badge metadata). Type them as{ type: 'jsonb', nullable: true }and give them a TS interface.- Self-referencing relations are allowed (
User.salesHead→User); always make the FK column nullable.
Do / Don't — schema changes under synchronize: true¶
Because synchronize auto-applies the entity → schema diff on every boot, some changes are
safe and some silently destroy data. Use this table.
| Change | Verdict | Why / How |
|---|---|---|
| Add a new nullable column | DO | Pure additive ALTER ADD COLUMN. Safe. |
Add a column with a default |
DO | Existing rows backfill with the default. Safe. |
| Add a column NOT NULL without default to a populated table | DON'T | Sync will fail or lock; add as nullable first, backfill, then tighten. |
Add a new entity (and register it in the entities array) |
DO | Creates the table. Must be added to the array in database.ts or it is ignored. |
| Add a new value to an enum | DO (carefully) | Postgres enum gets the new label. Order matters for some tools; append. |
| Remove an enum value while rows still use it | DON'T | Enum recreate cast fails. Migrate rows off the label first (see migrateSuperadminRoleBeforeSync). |
| Rename a column or entity property | DON'T (directly) | TypeORM sees it as drop-old + add-new = data loss. Add new, copy data, then remove old in a later deploy. |
Change a column type (e.g. varchar → int) |
DON'T (directly) | Auto-cast may fail or truncate. Add a new column, migrate, swap. |
| Narrow a column (length, precision) | DON'T | Can truncate or fail mid-flight. Widen-only is safe. |
Add an index via @Index |
DO | Created on next sync. For huge tables, prefer creating CONCURRENTLY by hand to avoid a write lock. |
Add a non-public schema-scoped entity |
DO, but pre-create the schema | synchronize does NOT create schemas. Add the schema name to ensureNamedSchemas in database.ts. |
| Drop a column / entity | DON'T without a backup | Sync issues DROP COLUMN / DROP TABLE immediately on the next boot. Irreversible. |
Rely on synchronize in production |
DON'T (long-term) | See recommended migration path below. |
Recommended migration path (away from blind auto-sync)¶
synchronize: true is appropriate for local dev but is a production hazard (uncontrolled
ALTER/DROP, no review, no rollback, schema drift between environments). The recommended
trajectory:
- Generate, don't hand-write. Set
synchronize: falsein a staging config, thentypeorm migration:generateto capture the current diff as a reviewable file. - Run migrations explicitly at deploy time (
migration:run). Themigrationsglob is already wired indatabase.ts(src/migrations/migrations), so dropping files there makes them discoverable. - Review every generated migration for destructive statements (
DROP, type changes,NOT NULLadds) before merging. - Keep dev on
synchronizeif desired, but never let an unreviewed schema reach prod.
Until that path is adopted, treat the Do/Don't table as mandatory and always back up the database before deploying an entity change, since a careless rename ships as a drop on the next boot.
Repository, transaction & query patterns¶
- Get a repository:
const repo = dataSource.getRepository(Entity). Services typically cache these in the constructor (seeTokenAllotmentRequestService). 100+ services follow this pattern — do not instantiate raw SQL clients for ordinary CRUD. - Reads: prefer
findOne({ where })/find({ where, relations }). For aggregates, reports, and anything performance-sensitive, usecreateQueryBuilderinstead of loading entities and counting in JS. - Relations: the FK scalar + relation-object pattern lets you write
entity.ownerId = idwithout loadingowner. Userelations: ['owner'](or a query builder join) only when you actually need the related object — avoid eager relations by default. - Transactions: use
dataSource.transaction(async (manager) => { ... })for multi-row atomic writes. Inside the callback, always use the providedmanager(e.g.manager.getRepository(X)), never the global repository, or the writes escape the transaction. - Query runners: use
dataSource.createQueryRunner()for raw SQL, bulk operations, or when you need explicitBEGIN/COMMIT/ROLLBACKcontrol. Alwaysrelease()in afinallyblock (the legacy CRM migration indatabase.tsis the canonical example). - Accessing the DataSource outside a service:
DatabaseService.getInstance().dataSource.
Entity subscribers & lifecycle hooks¶
Subscribers live in src/subscribers/ and are registered in the subscribers array of the
DataSource config. Only two are active:
UserSubscriber(src/subscribers/user.subscriber.ts) —afterInsert: creates a zero-balanceTokenfor every newUserif one does not already exist. (It also contains commented-out logic to auto-create aMentorprofile forEXPERTusers.)ApplicationSubscriber(src/subscribers/application.subscriber.ts) —afterInsert: auto-creates a CRMLead(sourcemy-analytics-school, statusACTIVE) for the application's user, unless a matching lead already exists.
Rules for writing subscribers:
- Implement
listenTo()returning the entity class so the subscriber is scoped. - Always use
event.managerfor any DB access inside a hook so the work joins the triggering write's transaction. Using a fresh repository breaks atomicity. - Make hooks idempotent — both existing subscribers check for an existing row before inserting, because the same insert path can run more than once.
- Keep hooks fast and side-effect-light. Heavy work (emails, external calls) belongs on a BullMQ queue, not in a subscriber that blocks the write.
- Prefer explicit service logic over subscribers for anything a reader needs to discover — subscriber side-effects are easy to miss.
Seeding¶
Seed scripts live in src/seeding/ and run at startup from src/index.ts. They fall into two
groups:
- Idempotent "ensure" seeds run on every boot —
seedColleges,seedUsers,seedJobTemplates,seedGlobalScreeningConfig,seedWorkflowDefaults,seedApplicationLeads,seedLeadTags,ensureSalesHeadProfiles,ensureAdminUser. These insert-if-missing and are safe to re-run. - Heavy/destructive seeds gated behind
ENABLE_SEEDING=true—seedBatches,seedCourses, and the standaloneseed.ts(run vianpm run seed).seed.tstruncatestokens,mentors, andusers(TRUNCATE ... CASCADE) before loading JSON fixtures — never run it against a database you care about.
Seed conventions:
- Each seed takes a
DataSourceand usesgetRepository. - JSON fixtures (
batches.json,studentData.json,top-indian-colleges.json,updated_mentors_smart.json) live alongside the scripts and are loaded with a guardedloadSeedJsonhelper. - One-shot data migrations (e.g.
backfillLeadOwnership,campaignLeadBackfill) are written as idempotent scripts, not as schema migrations. - npm entry points:
npm run seed,npm run seed:test-meeting,npm run seed-sales.
Edge cases, limits & gotchas¶
- Auto-sync is the elephant. Any property rename or type change ships as drop+recreate on the next boot — silent data loss. Re-read the Do/Don't table before changing a column.
- An entity must be in the
entitiesarray. Importing the file is not enough; if it is not listed indatabase.tsit is never synced and its table never appears. - Named schemas are not created by sync. New schema-scoped entities (
mrlearn,mrtest,mas_crm,superadmin) require the schema to be pre-created inensureNamedSchemas. - Enum value removal breaks boot. The startup
migrateSuperadminRoleBeforeSyncexists precisely because removing thesuperadminlabel fromUserRolewould make the enum recreate fail on rows still carrying it. Migrate data off a label before you delete the label. - Shared pool across workers. All 5 BullMQ workers, HTTP handlers, and socket handlers share
the same
min 5 / max 20pool in one process. A long transaction or a leaked query runner (missingrelease()) starves everyone — always release runners infinally. - No soft deletes. Deletes are permanent. Use status/
isActiveflags for recoverable state. logging: false. You will not see SQL by default; flip it on locally to debug.- Multi-platform (
x-platformheader). The DB is shared across products; rows are distinguished bysource/platformcolumns and schemas, not by separate connections. Scope queries by the relevant ownership/platform column — never assume a table belongs to one product. select: falsecolumns (likeUser.password) are absent from default finds; you mustaddSelectthem explicitly, which is easy to forget when debugging auth.- Decimal columns return as strings from
pgin some configurations — coerce money values with care when doing arithmetic.
Related docs¶
- Data Model — full per-domain ER diagrams and table catalog.
- Backend Database Schema — schema-level reference.
- Design Patterns — service/repository conventions.
- Background Jobs & Queues — BullMQ workers that share this DataSource.
- Multi-Platform Architecture — how
x-platformand schemas isolate products. - Request Lifecycle & Middleware — how requests reach services that hit the DB.
- Getting Started — local DB setup and seeding.