Goal & when
Most plugin tables should stay on the shared core Postgres connection. Add a custom source only when you need a separate host, schema, reporting database, or replica.
- Prefer default — register entities with plugin: "default" (no @Database needed)
- Custom — @Database({ name?, options }) on the root @Module class only
- Reuse — other plugins inject the owner’s connection; they do not re-declare @Database
1. Prerequisites
You need a working plugin backend module and credentials available via env (PluginEnv / base .env).
- Plugin scaffolded with root @Module({ name: metadata.name, … })
- Host, port, username, password, database from env — never hard-code secrets
- For custom sources set synchronize: false and ship schema via migrations
- If you will inject another plugin’s DB, list that owner in pluginDependencies
2. Prefer the default DataSource
Unless you need isolation, merge into the shared connection. Inject with DataSourceManager.DEFAULT_PLUGIN (or the string "default").
- Table names still MUST start with metadata.name
- Extend BaseEntity for create / update / soft-delete columns
- No @Database decorator — the core already owns "default"
1@Module({
2 name: metadata.name,
3 providers: [OrderService],
4 controllers: [OrderController],
5 entities: [
6 {
7 plugin: "default",
8 entities: [OrderEntity],
9 },
10 ],
11})
12export class MyPluginModule {}1import { DataSource } from "typeorm";
2import {
3 InjectDatabaseSource,
4 DataSourceManager,
5 Service,
6} from "@quan-erp/shared-backend-core";
7
8@Service()
9export class OrderService {
10 @InjectDatabaseSource(DataSourceManager.DEFAULT_PLUGIN)
11 source: DataSource;
12}3. Add a custom @Database
Declare the connection on the root @Module. Register entities with plugin: metadata.name and the same optional name. Inject with @InjectDatabaseSource(metadata.name, name?).
- @Database({ name?, options }) — plugin owner is filled from the loading plugin
- name — required when one plugin opens more than one connection
- options — TypeORM DataSourceOptions; load credentials from env
- synchronize: false for custom sources in staging / production
- Entities — matching plugin + name; do not rely on options.entities
1import {
2 Database,
3 Module,
4} from "@quan-erp/shared-backend-core";
5import metadata from "../../module.metadata.json" with { type: "json" };
6import { AnalyticsEntity } from "./analytics.entity.js";
7import { AnalyticsService } from "./analytics.service.js";
8
9@Database({
10 name: "analytics",
11 options: {
12 type: "postgres",
13 host: process.env.ANALYTICS_DB_HOST,
14 port: Number(process.env.ANALYTICS_DB_PORT ?? 5432),
15 username: process.env.ANALYTICS_DB_USERNAME,
16 password: process.env.ANALYTICS_DB_PASSWORD,
17 database: process.env.ANALYTICS_DB_SCHEMA,
18 synchronize: false,
19 },
20})
21@Module({
22 name: metadata.name,
23 providers: [AnalyticsService],
24 controllers: [],
25 entities: [
26 {
27 plugin: metadata.name,
28 name: "analytics",
29 entities: [AnalyticsEntity],
30 },
31 ],
32})
33export class MyPluginModule {}@InjectDatabaseSource(metadata.name, "analytics")
analyticsSource: DataSource;4. Cross-plugin inject
Connections live in a process-wide registry keyed by owner plugin (+ optional name). Consumers inject the owner’s DataSource and may register their own entities against that same plugin + name.
- Owner — declares @Database and boots first (list in pluginDependencies)
- Consumer — @InjectDatabaseSource("owner-plugin", "connection-name?")
- Foreign entities — entities entry uses plugin: "owner-plugin" and name: "connection-name"
- Do not re-declare the same @Database in every consumer
1@Service()
2export class ReportService {
3 // "warehouse" plugin declared @Database({ name: "replica", ... })
4 @InjectDatabaseSource("warehouse", "replica")
5 warehouseReplica: DataSource;
6}5. Verify
- Plugin boots without DI / DataSource errors
- Injected DataSource is defined and getRepository(Entity) works
- Entities appear on the intended connection (default vs custom name)
- Env credentials resolve in Docker / staging the same as local
Common mistakes
- Putting @Database on a service or nested module — must be root @Module
- Registering custom-DB entities with plugin: "default" (or wrong name)
- Hard-coding passwords instead of PluginEnv / base .env
- synchronize: true on a custom source in production
- Re-declaring @Database in consumers instead of @InjectDatabaseSource
- Forgetting pluginDependencies so the owner loads after the consumer