Best practices for deploying Data Hub in production.
debug: false)DATA_HUB_EXPORT_ROOT points to writable persistent storage for local outputsUse environment variables for deployment-specific configuration and secrets:
# Database connections
ERP_DB_HOST=db.production.internal
ERP_DB_USER=vendure_reader
ERP_DB_PASSWORD=secure-password
# API keys
SUPPLIER_API_KEY=sk_live_...
GOOGLE_MERCHANT_API_KEY=...
# AWS credentials (for S3)
AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=...
# Server-local Data Hub exports
DATA_HUB_EXPORT_ROOT=/var/lib/vendure-data-hub/exports
DATA_HUB_EXPORT_ROOT is resolved when each process starts and defaults to <cwd>/exports. Set it explicitly in production and provide a writable persistent mount. API servers and workers that must share local outputs need the same configured root and shared storage.
Local exporter path and feed outputPath values remain relative, such as catalog and feeds/google.xml. Absolute paths, URLs, and traversal are invalid for local output fields; remote destinations keep their own remotePath, bucket/prefix, or URL settings.
DataHubPlugin.init({
enabled: true,
debug: false,
retentionDaysRuns: 30,
retentionDaysErrors: 90,
secrets: [
{ code: 'supplier-api', provider: 'ENV', value: 'SUPPLIER_API_KEY' },
{ code: 'erp-db-password', provider: 'ENV', value: 'ERP_DB_PASSWORD' },
],
connections: [
{
code: 'erp-db',
type: 'POSTGRES',
settings: {
host: '${ERP_DB_HOST}',
port: 5432,
database: 'erp',
username: '${ERP_DB_USER}',
passwordSecretCode: 'erp-db-password',
ssl: true,
},
},
],
})
For smaller deployments, the default configuration works:
jobQueueOptions: {
activeQueues: [
'default',
'data-hub.event-trigger-outbox',
'data-hub.webhook-retry',
'data-hub.remote-source-acknowledgement',
'data-hub.run',
],
}
For high-volume processing, run dedicated workers:
// Main server - handles API requests
jobQueueOptions: {
activeQueues: ['default'],
}
// Worker process - handles data hub jobs
jobQueueOptions: {
activeQueues: [
'data-hub.event-trigger-outbox',
'data-hub.webhook-retry',
'data-hub.remote-source-acknowledgement',
'data-hub.run',
],
}
EVENT and outgoing webhook delivery use database outboxes plus leased Vendure
jobs. Configure a persistent strategy such as the database-backed
DefaultJobQueuePlugin, and run workers that consume
data-hub.event-trigger-outbox, data-hub.webhook-retry,
data-hub.remote-source-acknowledgement, and data-hub.run. Event and webhook
outbox rows recover expired or lost queue publications. Pipeline run rows retain
a queue request and stale-dispatch claim until a worker owns execution, so
startup reconciliation recovers a failed run-queue handoff. Completed S3 and
FTP/SFTP delete-or-move acknowledgements remain in pipeline checkpoints until
the remote action succeeds; bounded checkpoint reconciliation republishes that
work to the remote-source queue. Reconciliation has one renewable distributed
leader across API and worker replicas, while each pipeline’s remote operations
use a separately renewed lease. A bounded token-protected dispatch lease keeps
repeated scans from amplifying the queue while Vendure retries a failed remote
system; successful jobs and failed publications release it, and process loss is
recovered after expiry. Recovery resolves the persisted Vendure channel ID to
the current Channel entity, so channel-token rotation does not redirect or
strand queued cleanup. A persistent queue avoids recovery delays and is required
for normal multi-process worker operation. Every worker that handles webhooks or
remote acknowledgements needs the same DATAHUB_MASTER_KEY and Secret Code
providers as the API process.
Every API server and worker must also receive identical code-first pipelines,
connections, and configPath configuration. One API server reconciles those
database rows under a distributed lock. Workers verify the resulting shared
state read-only and delay schedule, message-consumer, and file-watcher discovery
until it matches. A mismatch aborts worker startup instead of running with stale
connection or pipeline configuration.
Data Hub follows Vendure’s scheduler process ownership. With Vendure’s default
schedulerOptions.runTasksInWorkerOnly: true, scheduled pipeline discovery and
triggering run in the worker process. A single-process deployment must set
runTasksInWorkerOnly: false explicitly; do not start schedule polling in both
API and worker roles.
// worker.ts
import { bootstrapWorker, Logger } from '@vendure/core';
import config from './vendure-config';
bootstrapWorker({
...config,
jobQueueOptions: {
activeQueues: [
'data-hub.event-trigger-outbox',
'data-hub.webhook-retry',
'data-hub.remote-source-acknowledgement',
'data-hub.run',
],
pollInterval: 1000,
},
})
.then(worker => worker.startJobQueue())
.then(worker => worker.startHealthCheckServer({ port: 3020 }))
.catch(err => {
Logger.error(
`Worker failed to start: ${err instanceof Error ? err.message : String(err)}`,
'DataHubWorker',
);
process.exitCode = 1;
});
The scheduler uses process timers and occurrence-scoped distributed leases.
Each cron minute or fixed-interval bucket is claimed once across Data Hub processes,
and scheduled starts are handed to data-hub.run.
Set the database extractor pool explicitly to avoid exhausting database connections:
.extract('read-external-products', {
adapterCode: 'database',
connectionCode: 'erp-db',
databaseType: 'POSTGRESQL',
query: 'SELECT * FROM products ORDER BY id',
pool: {
max: 5,
},
})
For read-heavy operations, configure read replicas:
connections: [
{
code: 'erp-db-read',
type: 'POSTGRES',
settings: {
host: '${ERP_DB_READ_HOST}',
port: 5432,
database: 'erp',
username: '${ERP_DB_READ_USER}',
passwordSecretCode: 'erp-db-password',
ssl: true,
},
},
]
Set the minimum level to persist:
mutation {
updateDataHubSettings(input: {
logPersistenceLevel: PIPELINE
}) {
logPersistenceLevel
}
}
ERROR_ONLY persists errors.PIPELINE persists pipeline lifecycle events and errors and is the default.STEP also persists step lifecycle events.DEBUG persists all supported events and has the highest storage cost.Vendure writes through the logger configured on VendureConfig.logger. The
built-in logger can be configured explicitly:
import { DefaultLogger, LogLevel, type VendureConfig } from '@vendure/core';
export const config: VendureConfig = {
// ...
logger: new DefaultLogger({
level: LogLevel.Info,
timestamp: true,
}),
};
For CloudWatch, Datadog, or another structured backend, either collect the
application’s stdout or provide an implementation of Vendure’s
VendureLogger interface. There is no LoggingService base class in the
current Vendure API. Keep log redaction at both the Data Hub and application
logger boundaries.
The plugin does not install alert rules or expose a Prometheus endpoint. Derive deployment-specific signals from the Admin API, persisted logs, the database, and infrastructure monitoring. Example signals are:
| Signal | Source |
|---|---|
| Pipeline success and failure trend | Pipeline run statuses |
| Run duration | Run start/finish timestamps and metrics |
| Record-error trend | Quarantined record-error rows |
| Queue depth and recent failures | dataHubQueueStats |
| Worker health | Vendure worker health endpoint and infrastructure probes |
Vendure servers expose /health. Add deployment-critical dependencies through
systemOptions.healthChecks with Vendure HealthCheckStrategy implementations;
when replacing that array, retain TypeORMHealthCheckStrategy. Vendure workers
expose /health after startHealthCheckServer() is called, as in the worker
example above. Data Hub does not register a separate DataHubHealthService or
plugin-specific health endpoint.
Use authenticated Admin API checks for dataHubQueueStats, representative run
queries, and message-consumer status when deeper readiness evidence is needed.
Do not expose those Admin API operations as unauthenticated health routes. See
Vendure’s deployment health-check guidance.
Set up alerts for:
Treat the host database, persistent artifact storage, application build, and secret material as one recovery unit.
DATAHUB_MASTER_KEY, environment-backed secret values, and external secret
provider configuration stored separately from the database backup;A definition export is a useful secondary aid, but it is not a backup. Code-first configuration also does not reconstruct revision history, active run state, checkpoints, encrypted database values, or delivery outboxes.
Do not routinely recreate database connections or INLINE secrets from documentation after a restore; restoring the database and the matching master key preserves them. ENV-backed secret values still come from the external secret store and are never contained in the database.
Repository acceptance tests prove code paths against disposable local services; they do not certify a customer’s network, credentials, HA topology, or remote product configuration. Record an owner, endpoint/topology, CA and key source, credential rotation plan, allowlist/firewall rule, expected volume, timeout and retry policy, failover scenario, evidence timestamp, rollback trigger, and pass/fail result for every configured dependency.
| Dependency | Repository evidence | Required production evidence |
|---|---|---|
| PostgreSQL/MySQL extractor | Disposable mTLS query, active-session proof, untrusted CA/hostname/client-cert rejection, and PostgreSQL new-install migration apply/revert | Target TLS/CA or mTLS, least privilege, query plan, stable failover DNS/proxy, timeout, upgrade from the actual prior schema, and recovery |
| Redis | Atomic counters, locks, address-pinned Streams clients, process crash, outage/reconnect against one server, and automatic primary-loss election with two replicas and a three-Sentinel quorum | Automatic election or managed failover in the exact target topology, TLS/server-name policy, persistence policy, partition and split-brain controls, promotion time, and accepted data-loss behavior |
| RabbitMQ | Disposable-broker publisher/consumer round trip, broker-enforced prefetch, cancellation/redelivery, plus DNS/address pinning, bounded setup, publisher confirms, manual settlement, pool cleanup, and partial-setup/shutdown failure tests | Target TLS/CA, credentials/vhost permissions, firewall/DNS policy, acknowledgement timeout, quorum/durability policy, broker failover, load, and redelivery behavior |
| SQS | Batch-result correlation, manual settlement and lease renewal, URL credential rejection, and bounded DNS-pinned custom endpoint transports | Target AWS account/region, IAM or workload identity, KMS policy, DLQ/redrive policy, endpoint/VPC policy, throttling, retention, load, and regional failure behavior |
| OTLP | Real Collector metrics/traces export and outage recovery | Target collector authentication, TLS, capacity, retention, alert routing, and collector/egress failure |
| S3 | MinIO object round trip and signed URL | Target AWS/S3-compatible IAM, region, HTTPS/CA, bucket policy, encryption, large-object, and interruption behavior |
| FTP/FTPS/SFTP | FTP and password-SFTP round trip with SFTP host-key pinning | FTPS certificate validation where used, private-key/passphrase rotation, firewall/passive ports, transfer interruption, and reconnect |
| Pimcore | Synthetic local HTTP server covering authentication headers, pagination, retry, and checkpoint contracts | Active target Data Hub GraphQL configuration, supported Pimcore/schema version, real auth, deterministic pagination, rate limiting, and representative data |
Data Hub coordinates schedule triggers, message consumers, and individual run jobs across multiple processes when a shared lock backend is configured:
Distributed Locking:
# Option 1a: standalone or managed Redis endpoint
DATAHUB_REDIS_URL=redis://redis.production.internal:6379
# Option 1b: Redis Sentinel discovery
DATAHUB_REDIS_SENTINELS=redis-sentinel-1.internal:26379,redis-sentinel-2.internal:26379,redis-sentinel-3.internal:26379
DATAHUB_REDIS_SENTINEL_NAME=vendure-primary
DATAHUB_REDIS_TLS=true
DATAHUB_REDIS_SENTINEL_TLS=true
# Option 2: force PostgreSQL (no additional infrastructure)
DATAHUB_LOCK_BACKEND=postgres
Provide Sentinel and data-node ACL credentials through the deployment secret
manager when the target requires them. TLS uses the Node.js trust store; add a
private CA with NODE_EXTRA_CA_CERTS before startup. Use the same discovery,
database, TLS, and authentication settings on every API server and worker.
Either Redis discovery mode also enables atomic shared rate-limit counters for
incoming webhooks. Without Redis, those counters are process-local. If Redis
becomes unavailable after startup, webhook admission fails closed with 503;
the limiter never weakens itself to per-process counters in a multi-instance
deployment.
A configured Redis topology also auto-selects Redis for distributed locks unless a
different valid backend is forced. Redis lock initialization is fail-closed, so
an unavailable lock backend can prevent application bootstrap. On PostgreSQL,
set DATAHUB_LOCK_BACKEND=POSTGRES to keep locking independent of Redis.
The disposable repository topology proves automatic election after an external
SIGKILL of the primary, with two replicas, three Sentinels, and surviving
lock/quota state through existing and fresh application clients. Production
sign-off must repeat the failure in the exact target topology and record
election time, application reconnect time, surviving state, partition behavior,
and the target’s persistence/data-loss result. Global Sentinel settings do not
configure Redis Streams; each Streams trigger or sink uses its saved connection.
What’s Protected:
Deployment Architecture:
┌─────────────────┐
│ Load Balancer │
└────────┬────────┘
│
┌────────────────────┼────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Vendure 1 │ │ Vendure 2 │ │ Vendure 3 │
│ + Data Hub │ │ + Data Hub │ │ + Data Hub │
└───────┬───────┘ └───────┬───────┘ └───────┬───────┘
│ │ │
└────────────────────┼────────────────────┘
│
┌────────────────────┼────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Primary DB │ │ Redis │ │ Message Queue│
│ │ │ (conditional) │ │ (optional) │
└───────────────┘ └───────────────┘ └───────────────┘
Without Redis:
With Redis:
Limit aggregate loader batch starts across the pipeline run:
.load('write-products', {
adapterCode: 'productUpsert',
throughput: {
rateLimitRps: 10,
},
})
This setting controls load execution; it does not rate-limit extractor HTTP requests. Configure external API throttling in the extractor or connection adapter that owns those requests.