Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/reusable-dashboard-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ jobs:
env:
NX_NO_CLOUD: ${{ secrets.NX_CLOUD_ACCESS_TOKEN == '' && 'true' || 'false' }}
with:
targets: build
targets: build,typecheck
projects: '@novu/dashboard,@novu/api-service,@novu/worker'

- uses: ./.github/actions/start-localstack
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,4 @@ demos/web-chat-demo/next-env.d.ts

# Local agent design/plan artifacts (not part of published docs)
superpowers/
/tmp/
2 changes: 1 addition & 1 deletion .source
2 changes: 1 addition & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
},
"scripts": {
"prebuild": "rimraf dist",
"build": "pnpm build:metadata && nest build",
"build": "nx build @novu/api-service",
"build:generate": "pnpm build:metadata && nest build && pnpm generate:swagger && pnpm generate:sdk",
"build:watch": "pnpm build:metadata && nest build --watch",
"docker:build": "pnpm --silent --workspace-root pnpm-context -- apps/api/Dockerfile | BULL_MQ_PRO_NPM_TOKEN=${BULL_MQ_PRO_NPM_TOKEN} docker buildx build --load -t novu-api --secret id=BULL_MQ_PRO_NPM_TOKEN --build-arg PACKAGE_PATH=apps/api - $DOCKER_BUILD_ARGUMENTS",
Expand Down
24 changes: 20 additions & 4 deletions apps/api/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,27 @@
"sourceRoot": "apps/api/src",
"projectType": "application",
"targets": {
"build": {
"cache": false,
"build-metadata": {
"executor": "nx:run-commands",
"cache": true,
"dependsOn": ["^build"],
"inputs": ["default"],
"outputs": ["{projectRoot}/src/metadata.ts"]
"inputs": ["default", "^default"],
"outputs": ["{projectRoot}/src/metadata.ts"],
"options": {
"cwd": "apps/api",
"command": "pnpm run build:metadata"
}
},
"build": {
"executor": "nx:run-commands",
"cache": true,
"dependsOn": ["build-metadata", "^build"],
"inputs": ["default", "^default"],
"outputs": ["{projectRoot}/dist"],
"options": {
"cwd": "apps/api",
"command": "pnpm exec rimraf dist && pnpm exec nest build"
}
},
"lint": {
"executor": "nx:run-commands",
Expand Down
20 changes: 19 additions & 1 deletion apps/api/src/.example.env
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,12 @@ REDIS_CACHE_ENABLE_AUTOPIPELINING=
IS_IN_MEMORY_CLUSTER_MODE_ENABLED=false
REDIS_CLUSTER_SERVICE_HOST=
REDIS_CLUSTER_SERVICE_PORT=
REDIS_CLUSTER_SERVICE_PORTS=
REDIS_CLUSTER_USERNAME=
REDIS_CLUSTER_PASSWORD=
REDIS_CLUSTER_TLS=
REDIS_CLUSTER_DB_INDEX=
REDIS_CLUSTER_TTL=
REDIS_CLUSTER_PASSWORD=
REDIS_CLUSTER_CONNECTION_TIMEOUT=
REDIS_CLUSTER_KEEP_ALIVE=
REDIS_CLUSTER_FAMILY=
Expand All @@ -39,11 +42,22 @@ REDIS_CLUSTER_KEY_PREFIX=

JWT_SECRET=LOCAL_ONLY_CHANGE_ME

# File storage for email attachments (API uploads, worker downloads before send).
# Default is AWS S3 (Novu Cloud). Self-hosted Azure Blob Storage: set
# STORAGE_SERVICE=AZURE and the AZURE_* vars on BOTH the API and the worker.
# The Azure container must already exist.
# STORAGE_SERVICE=AZURE
S3_LOCAL_STACK=http://127.0.0.1:4566
S3_BUCKET_NAME=novu-local
S3_REGION=us-east-1
AWS_ACCESS_KEY_ID=test
AWS_SECRET_ACCESS_KEY=test
# AZURE_ACCOUNT_NAME=
# AZURE_ACCOUNT_KEY=
# AZURE_HOST_NAME=https://<account>.blob.core.windows.net
# AZURE_CONTAINER_NAME=novu
# GCS_BUCKET_NAME=
# GCS_DOMAIN=
NEW_RELIC_ENABLED=false
CDN_URL=

Expand Down Expand Up @@ -120,6 +134,10 @@ CLICK_HOUSE_USER=default
CLICK_HOUSE_PASSWORD=
CLICK_HOUSE_DATABASE=novu-local

# Self-hosted EE (Better Auth): max organizations a user can create. Default 10, minimum 1.
# Invalid values log an error and fall back to 10.
# BETTER_AUTH_ORGANIZATION_LIMIT=10

# When using `pnpm dev:portless`, scripts/portless-dev-env.mjs resolves these at
# runtime via `portless get` (worktree-prefixed and proxy-port aware):
# API_ROOT_URL, FRONT_BASE_URL, DASHBOARD_URL, BETTER_AUTH_BASE_URL
Expand Down
12 changes: 9 additions & 3 deletions apps/api/src/config/env.validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,14 @@ export const envValidators = {
REDIS_CACHE_SERVICE_PORT: str({ default: '' }),
REDIS_CACHE_SERVICE_TLS: json({ default: undefined }),
REDIS_CLUSTER_SERVICE_HOST: str({ default: '' }),
REDIS_CLUSTER_SERVICE_PORT: str({ default: '' }),
REDIS_CLUSTER_SERVICE_PORTS: str({ default: '' }),
REDIS_CLUSTER_USERNAME: str({ default: undefined }),
REDIS_CLUSTER_PASSWORD: str({ default: undefined }),
REDIS_CLUSTER_TLS: str({ default: undefined }),
IS_IN_MEMORY_CLUSTER_MODE_ENABLED: bool({ default: false }),
STORE_NOTIFICATION_CONTENT: bool({ default: false }),
STORAGE_SERVICE: str({ default: undefined }),
WORKER_DEFAULT_CONCURRENCY: num({ default: undefined }),
WORKER_DEFAULT_LOCK_DURATION: num({ default: undefined }),
// SQS queue backend (optional - when unset, jobs are produced to BullMQ only)
Expand Down Expand Up @@ -171,21 +177,21 @@ export const envValidators = {
) as Record<FeatureFlagsKeysEnum, ValidatorSpec<string | number | boolean | undefined>>),

// Azure validators
...(processEnv.STORAGE_SERVICE === 'AZURE' && {
...((processEnv.STORAGE_SERVICE || '').toUpperCase() === 'AZURE' && {
AZURE_ACCOUNT_NAME: str(),
AZURE_ACCOUNT_KEY: str(),
AZURE_HOST_NAME: str({ default: `https://${processEnv.AZURE_ACCOUNT_NAME}.blob.core.windows.net` }),
AZURE_CONTAINER_NAME: str({ default: 'novu' }),
}),

// GCS validators
...(processEnv.STORAGE_SERVICE === 'GCS' && {
...((processEnv.STORAGE_SERVICE || '').toUpperCase() === 'GCS' && {
GCS_BUCKET_NAME: str(),
GCS_DOMAIN: str(),
}),

// AWS validators
...(processEnv.STORAGE_SERVICE === 'AWS' && {
...((processEnv.STORAGE_SERVICE || '').toUpperCase() === 'AWS' && {
S3_LOCAL_STACK: str({ default: '' }),
S3_BUCKET_NAME: str(),
S3_REGION: str(),
Expand Down
19 changes: 17 additions & 2 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"start:test": "vite --mode test",
"start:static:build": "http-server dist -p 4201 --proxy http://127.0.0.1:4201?",
"dev": "pnpm start",
"build": "NODE_OPTIONS='--max-old-space-size=6144' tsc -b && NODE_OPTIONS='--max-old-space-size=6144' vite build",
"build": "NODE_OPTIONS='--max-old-space-size=6144' vite build",
"docker:build": "docker buildx build --load -f ./dockerfile -t novu-dashboard ./../.. $DOCKER_BUILD_ARGUMENTS",
"lint": "biome lint .",
"lint:fix": "biome lint --write .",
Expand All @@ -27,7 +27,8 @@
"test:e2e:install": "playwright install --with-deps",
"test:e2e:codegen": "playwright codegen",
"test:e2e:show-report": "npx playwright show-report",
"test:e2e:merge-report": "playwright merge-reports --reporter html"
"test:e2e:merge-report": "playwright merge-reports --reporter html",
"typecheck": "NODE_OPTIONS='--max-old-space-size=6144' tsc -b"
},
"dependencies": {
"@ai-sdk/react": "^3.0.51",
Expand Down Expand Up @@ -209,6 +210,20 @@
"options": {
"command": "npx biome lint apps/dashboard"
}
},
"typecheck": {
"cache": true,
"dependsOn": [
"^build"
],
"inputs": [
"default",
"^default"
],
"outputs": [
"{projectRoot}/tsconfig.app.tsbuildinfo",
"{projectRoot}/tsconfig.node.tsbuildinfo"
]
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions apps/inbound-mail/src/config/env.validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ export const envValidators = {
REDIS_HOST: str(),
REDIS_PORT: port(),
REDIS_TLS: json({ default: undefined }),
IS_IN_MEMORY_CLUSTER_MODE_ENABLED: bool({ default: false }),
REDIS_CLUSTER_SERVICE_HOST: str({ default: undefined }),
REDIS_CLUSTER_SERVICE_PORT: str({ default: undefined }),
REDIS_CLUSTER_SERVICE_PORTS: str({ default: undefined }),
REDIS_CLUSTER_USERNAME: str({ default: undefined }),
REDIS_CLUSTER_PASSWORD: str({ default: undefined }),
REDIS_CLUSTER_TLS: str({ default: undefined }),
WORKER_DEFAULT_CONCURRENCY: num({ default: undefined }),
WORKER_DEFAULT_LOCK_DURATION: num({ default: undefined }),
INBOUND_PARSE_MAIL_WORKER_CONCURRENCY: num({ default: undefined }),
Expand Down
1 change: 1 addition & 0 deletions apps/worker/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"cache": true,
"inputs": [
"default",
"^default",
"{projectRoot}/**/*",
"!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
"!{projectRoot}/tsconfig.spec.json",
Expand Down
28 changes: 19 additions & 9 deletions apps/worker/src/.example.env
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ MAX_NOVU_INTEGRATION_SMS_REQUESTS=20
NOVU_SLACK_INTEGRATION_CLIENT_ID=
NOVU_SLACK_INTEGRATION_CLIENT_SECRET=

# Storage Service
# STORAGE_SERVICE=
# File storage for email attachments (must match the API).
# Default is AWS S3. Self-hosted Azure Blob Storage: STORAGE_SERVICE=AZURE
# STORAGE_SERVICE=AZURE

# Redis
REDIS_PORT=6379
Expand All @@ -33,15 +34,21 @@ REDIS_CACHE_FAMILY=
REDIS_CACHE_KEY_PREFIX=
REDIS_CACHE_SERVICE_TLS=

# Redis (for cluster)
# Redis Cluster (self-hosted enterprise BullMQ)
# Set IS_IN_MEMORY_CLUSTER_MODE_ENABLED=true with NOVU_ENTERPRISE=true and
# REDIS_CLUSTER_SERVICE_HOST plus REDIS_CLUSTER_SERVICE_PORTS (JSON array) or
# REDIS_CLUSTER_SERVICE_PORT. Comma-separated hosts are supported.
IS_IN_MEMORY_CLUSTER_MODE_ENABLED=false
# ELASTICACHE_CLUSTER_SERVICE_HOST=
# ELASTICACHE_CLUSTER_SERVICE_PORT=
# REDIS_CLUSTER_SERVICE_HOST=localhost
# REDIS_CLUSTER_SERVICE_PORT=6379
# REDIS_CLUSTER_SERVICE_PORTS=[7000,7001,7002,7003,7004,7005]
# REDIS_CLUSTER_USERNAME=
# REDIS_CLUSTER_PASSWORD=
# REDIS_CLUSTER_TLS=
# REDIS_CLUSTER_DB_INDEX=
# REDIS_CLUSTER_TTL=
# REDIS_CLUSTER_PASSWORD=
# REDIS_CLUSTER_CONNECTION_TIMEOUT=
# REDIS_CLUSTER_KEEP_ALIVE=
# REDIS_CLUSTER_FAMILY=
Expand All @@ -55,13 +62,16 @@ MONGO_MAX_POOL_SIZE=500
S3_LOCAL_STACK=http://127.0.0.1:4566
S3_BUCKET_NAME=novu-local
S3_REGION=us-east-1
# GCS_BUCKET_NAME=novu-local
# AZURE_ACCOUNT_NAME=novu
# AZURE_ACCOUNT_KEY=123456
# AZURE_HOST_NAME=https://novu.blob.core.windows.net
# AZURE_CONTAINER_NAME=novu-local
AWS_ACCESS_KEY_ID=test
AWS_SECRET_ACCESS_KEY=test
# Azure Blob Storage (self-hosted). Requires STORAGE_SERVICE=AZURE.
# The container must already exist.
# AZURE_ACCOUNT_NAME=
# AZURE_ACCOUNT_KEY=
# AZURE_HOST_NAME=https://<account>.blob.core.windows.net
# AZURE_CONTAINER_NAME=novu
# GCS_BUCKET_NAME=novu-local
# GCS_DOMAIN=

# New Relic
NEW_RELIC_APP_NAME="[LOCAL] - worker"
Expand Down
13 changes: 10 additions & 3 deletions apps/worker/src/config/env.validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ export const envValidators = {
REDIS_MASTER_PORT: str({ default: '' }),
REDIS_SLAVE_HOST: str({ default: '' }),
REDIS_SLAVE_PORT: str({ default: '' }),
IS_IN_MEMORY_CLUSTER_MODE_ENABLED: bool({ default: false }),
REDIS_CLUSTER_SERVICE_HOST: str({ default: undefined }),
REDIS_CLUSTER_SERVICE_PORT: str({ default: undefined }),
REDIS_CLUSTER_SERVICE_PORTS: str({ default: undefined }),
REDIS_CLUSTER_USERNAME: str({ default: undefined }),
REDIS_CLUSTER_PASSWORD: str({ default: undefined }),
REDIS_CLUSTER_TLS: str({ default: undefined }),
MONGO_AUTO_CREATE_INDEXES: bool({ default: false }),
MONGO_MAX_IDLE_TIME_IN_MS: num({ default: 1000 * 30 }),
MONGO_MAX_POOL_SIZE: num({ default: 50 }),
Expand Down Expand Up @@ -114,21 +121,21 @@ export const envValidators = {
) as Record<FeatureFlagsKeysEnum, ValidatorSpec<string | number | boolean | undefined>>),

// Azure validators
...(processEnv.STORAGE_SERVICE === 'AZURE' && {
...((processEnv.STORAGE_SERVICE || '').toUpperCase() === 'AZURE' && {
AZURE_ACCOUNT_NAME: str(),
AZURE_ACCOUNT_KEY: str(),
AZURE_HOST_NAME: str({ default: `https://${processEnv.AZURE_ACCOUNT_NAME}.blob.core.windows.net` }),
AZURE_CONTAINER_NAME: str({ default: 'novu' }),
}),

// GCS validators
...(processEnv.STORAGE_SERVICE === 'GCS' && {
...((processEnv.STORAGE_SERVICE || '').toUpperCase() === 'GCS' && {
GCS_BUCKET_NAME: str(),
GCS_DOMAIN: str(),
}),

// AWS validators
...(processEnv.STORAGE_SERVICE === 'AWS' && {
...((processEnv.STORAGE_SERVICE || '').toUpperCase() === 'AWS' && {
S3_LOCAL_STACK: str({ default: '' }),
S3_BUCKET_NAME: str(),
S3_REGION: str(),
Expand Down
7 changes: 7 additions & 0 deletions apps/ws/src/config/env.validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ export const envValidators = {
REDIS_HOST: str(),
REDIS_PORT: port(),
REDIS_TLS: json({ default: undefined }),
IS_IN_MEMORY_CLUSTER_MODE_ENABLED: bool({ default: false }),
REDIS_CLUSTER_SERVICE_HOST: str({ default: undefined }),
REDIS_CLUSTER_SERVICE_PORT: str({ default: undefined }),
REDIS_CLUSTER_SERVICE_PORTS: str({ default: undefined }),
REDIS_CLUSTER_USERNAME: str({ default: undefined }),
REDIS_CLUSTER_PASSWORD: str({ default: undefined }),
REDIS_CLUSTER_TLS: str({ default: undefined }),
REDIS_MASTER_HOST: str({ default: '' }),
REDIS_MASTER_PORT: str({ default: '' }),
REDIS_SLAVE_HOST: str({ default: '' }),
Expand Down
1 change: 0 additions & 1 deletion docker/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ REDIS_CACHE_SERVICE_HOST=
# Set to true when connecting through a proxy that does not support the Redis INFO command
REDIS_SKIP_VERSION_CHECK=false

# AWS
S3_LOCAL_STACK=$HOST_NAME:4566
S3_BUCKET_NAME=novu-local
S3_REGION=us-east-1
Expand Down
1 change: 0 additions & 1 deletion docker/community/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ REDIS_CACHE_SERVICE_HOST=
# Set to true when connecting through a proxy that does not support the Redis INFO command
REDIS_SKIP_VERSION_CHECK=false

# AWS
S3_LOCAL_STACK=${HOST_NAME}:4566
S3_BUCKET_NAME=novu-local
S3_REGION=us-east-1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,28 @@ describe('BullMQ Service', () => {
const queue = bullMqService.createQueue(JobTopicNameEnum.ACTIVE_JOBS_METRIC, {});
expect(queue.opts.prefix).toEqual('{metric-active-jobs}');
});

it('should hash-tag the prefix when the workflow Redis provider is clustered', () => {
const mockInMemoryProvider = {
providerInUseIsInClusterMode: jest.fn(() => true),
};

bullMqService = new BullMqService(mockInMemoryProvider as unknown as WorkflowInMemoryProviderService);

expect((bullMqService as any).generatePrefix(JobTopicNameEnum.ACTIVE_JOBS_METRIC)).toEqual(
'{metric-active-jobs}'
);
});

it('should omit the prefix for a standalone Redis provider', () => {
const mockInMemoryProvider = {
providerInUseIsInClusterMode: jest.fn(() => false),
};

bullMqService = new BullMqService(mockInMemoryProvider as unknown as WorkflowInMemoryProviderService);

expect((bullMqService as any).generatePrefix(JobTopicNameEnum.ACTIVE_JOBS_METRIC)).toBeUndefined();
});
});

describe('Add job', () => {
Expand Down
Loading
Loading