Demo

PluginEnv

Two layers of configuration: process env / base .env for infrastructure (Postgres, Redis), and plugin-scoped Env via @InjectEnv — persisted in the env table with isPublic / isSecret flags.

Two configuration layers

  • Base process env — DB_*, Redis, MODE, paths from base/backend/.env or Docker Compose; read with process.env (e.g. @Database options)
  • Plugin Env (@InjectEnv) — key/value scoped to the current plugin; loaded from Postgres on boot; editable from the ERP env UI / /env API
  • Do not put secrets in git — keep them in .env (local) or the env table (runtime)
  • Full base process-env catalog — see Environment variables

Base environment (.env)

Infrastructure connection settings for the base app and workers. Typical keys live in base/backend/.env and are wired through Docker Compose. For the complete key list, open Environment variables.

  • DB_HOST, DB_PORT, DB_USERNAME, DB_PASSWORD, DB_SCHEMA — Postgres
  • DB_SYNC — optional TypeORM synchronize flag (dev only)
  • REDIS_HOST, REDIS_PORT, REDIS_PASSWORD — cache, queues, BullMQ cron
  • In Compose, DB_HOST is usually the service name db (not localhost inside containers)
  • On the host for local psql / module seed, use localhost / 127.0.0.1
TSbase/backend/.env (example)
1DB_HOST=db 2DB_PORT=5432 3DB_USERNAME=postgres 4DB_PASSWORD=postgres 5DB_SCHEMA=quan_erp 6DB_SYNC=false 7REDIS_HOST=redis 8REDIS_PORT=6379 9REDIS_PASSWORD=

@InjectEnv

Inject the Env helper on a module, service, workflow node, or other DI-resolved class. The instance is scoped to the current plugin — get/set only see that plugin’s keys.

TSXinject
1import { 2 Env, 3 InjectEnv, 4 Module, 5 OnInit, 6 Service, 7} from "@quan-erp/shared-backend-core"; 8import metadata from "../../module.metadata.json" with { type: "json" }; 9 10@Service() 11export class PaymentConfigService { 12 @InjectEnv() 13 private env: Env; 14 15 getApiKey() { 16 return this.env.get("API_KEY"); 17 } 18} 19 20@Module({ 21 name: metadata.name, 22 providers: [PaymentConfigService], 23 controllers: [], 24 entities: [], 25}) 26export class MyPluginModule { 27 @InjectEnv() 28 env: Env; 29 30 @OnInit() 31 async init() { 32 // seed defaults — see below 33 } 34}

Env API

  • get(key) — read a plugin-scoped value (secrets decrypted)
  • set(key, value, options?) — write; defaults from injection: { isPublic: false, sync: true, isSecret: false }
  • sync() — persist the in-memory registry to the env table
  • onChanged(key, callback) — react when that key changes (UI update, set, etc.)
  • clear() — wipe this plugin’s env entries and clear DB via EnvService
  • all() — all keys for this plugin with value / isPublic / isSecret
TSXget / set / sync
1import { Env, InjectEnv, OnInit, Service } from "@quan-erp/shared-backend-core"; 2 3@Service() 4export class IntegrationSettingsService { 5 @InjectEnv() 6 private env: Env; 7 8 @OnInit() 9 async init() { 10 if (!this.env.get("API_BASE_URL")) { 11 await this.env.set("API_BASE_URL", "https://api.example.com", { 12 isPublic: true, 13 isSecret: false, 14 sync: true, 15 }); 16 } 17 18 if (!this.env.get("API_KEY")) { 19 await this.env.set("API_KEY", "", { 20 isPublic: false, 21 isSecret: true, 22 sync: true, 23 }); 24 } 25 26 this.env.onChanged("API_KEY", () => { 27 // reload clients that cache the key 28 }); 29 } 30 31 async rotateKey(next: string) { 32 await this.env.set("API_KEY", next, { 33 isSecret: true, 34 sync: true, 35 }); 36 } 37}

set() options

  • sync (default true via InjectEnv proxy) — write through to Postgres immediately
  • isPublic — exposed on GET /env/public for frontend / unauthenticated public env
  • isSecret — encrypted at rest; masked in owner env listings (maskSecret)
  • Prefer isSecret: true for API keys, tokens, and passwords
  • Prefer isPublic: true only for non-sensitive values the UI must read without owner rights

Persistence & boot

On startup the core loads rows from the env table into PluginEnvConfigManager. Changes via set({ sync: true }) or env.sync() upsert through EnvService. Uninstall / clear removes plugin-scoped entries.

  • Table: env (EnvEntity) — pluginName, key, value, isPublic, isSecret
  • Boot: PluginEnvConfigManager.loadFromDB()
  • Secrets stored encrypted (base64 of encrypt JSON); get() decrypts for the owning plugin
  • Admin UI / API can bulk-update and may broadcast a restart status after /env/many

HTTP surface (core)

  • GET /env — owner; all plugins’ env with secrets masked
  • GET /env/public — public keys only (isPublic: true)
  • GET /env/db — owner; raw rows from EnvService
  • PUT /env — set one key (plugin, key, val) with sync
  • PUT /env/many — bulk set then syncToDB
  • Plugin code should use @InjectEnv() rather than calling these HTTP routes

When to use which

  • process.env / .env — hosts, ports, Redis, compose wiring, @Database connection options
  • @InjectEnv — per-tenant or per-plugin settings operators change from the ERP UI without redeploy
  • DeveloperConfigService — typed plugin developer config (different from Env); see Builtin Services
  • SettingEntity / SettingService — user or system settings UI; not the same as plugin Env

Local-only operations

Module table seeding and similar setup scripts that read DB_* from .env must target local/dev hosts only (localhost / 127.0.0.1). Never run against UAT or production.

  • See Module seed for the local INSERT pattern
  • If DB_HOST is a remote UAT/prod host, stop

Package versions

Align @quan-erp/* versions in plugin package.json with the base image / shared-backend-core version so Env injection and crypto helpers stay compatible.

Checklist

  • Keep infrastructure secrets in base/backend/.env — not in source
  • Inject with @InjectEnv() on property (constructor injection unsupported)
  • Seed missing keys in @OnInit with explicit isPublic / isSecret / sync
  • Mark API keys isSecret: true
  • Use isPublic only for safe, UI-readable values
  • Call sync() (or sync: true) after writes you need persisted across restart