NorthRelayNorthRelay
Operators

Configuration Guide

Complete reference for NorthRelay environment variables, services, and configuration

Configuration Guide

Mailbox version boundary: the hosted mailbox service uses Stalwart Community 0.16.23 with native account administration. The application sending instance has a separate configuration and lifecycle. Older SQL-directory and container examples below must not be applied to the production mailbox; see Hosted email access and the platform infra/mailbox-next deployment source.

Full reference for configuring NorthRelay services, environment variables, and integrations.


Environment Variables

Required Variables

These must be set for NorthRelay to function:

VariableDescriptionExample
DATABASE_URLPostgreSQL connection stringpostgresql://user:pass@postgres:5432/northrelay
NEXTAUTH_SECRETSession encryption key (min 32 chars)openssl rand -base64 32
NEXTAUTH_URLCanonical URL of your dashboardhttps://northrelay.ca
NEXT_PUBLIC_APP_URLPublic-facing app URLhttps://northrelay.ca

Stalwart Mail Server

VariableDescriptionExample
STALWART_API_URLRelay Stalwart Management APIhttp://stalwart:8080
STALWART_API_TOKENRelay admin tokenHex token from config
STALWART_WEBHOOK_SECRETWebhook signature verificationRandom secret string
STALWART_MAILBOX_URLMailbox Stalwart API (for inbox)https://mail.northrelay.ca
STALWART_MAILBOX_TOKENMailbox admin tokenHex token from mailbox config

Important: The relay and mailbox Stalwart instances have different admin tokens. The relay handles outbound queuing; the mailbox handles inbound delivery and JMAP access.

Email Sending

VariableDescriptionDefault
SMTP_FROMSender address for system emailsno-reply@northrelay.ca
SMTP_FROM_NAMESender display nameNorthRelay

Redis

VariableDescriptionDefault
REDIS_HOSTRedis hostnamelocalhost
REDIS_PORTRedis port6379
REDIS_PASSWORDRedis password(empty)

Cron Jobs

VariableDescriptionExample
CRON_SECRETBearer token for cron endpointsRandom secret string

Optional / Development

VariableDescriptionDefault
NODE_ENVRuntime environmentproduction
LOG_LEVELLogging verbosityinfo

Stalwart Two-Layer Configuration

Stalwart Mail Server uses two configuration storage layers. Understanding this is critical for operations.

Layer 1: TOML Config File (Static)

The TOML file is loaded at startup and defines the base configuration:

# Example: config.prod.toml
[server]
hostname = "relay.northrelay.ca"
max-connections = 1024

[server.listener.smtp]
bind = ["0.0.0.0:25"]
protocol = "smtp"

[server.listener.management]
bind = ["127.0.0.1:8080"]
protocol = "http"

Changes to the TOML file require a restart or config reload to take effect.

Layer 2: RocksDB Database (Runtime)

Runtime state is stored in Stalwart's embedded RocksDB database. This includes:

  • Blocked IP addresses (from auto-ban)
  • Session counters
  • Some dynamic settings overridden at runtime

Warning: Database entries take precedence over TOML file entries for the same key. If a setting exists in both places, the database value wins. Use the Stalwart CLI to inspect and manage database configuration.

Managing Configuration Layers

# List all database config keys matching a pattern
docker exec stalwart /usr/local/bin/stalwart-cli \
  -u http://localhost:8080 -c admin:TOKEN \
  server list-config 'server'

# Add a runtime config value
docker exec stalwart /usr/local/bin/stalwart-cli \
  -u http://localhost:8080 -c admin:TOKEN \
  server add-config 'some.setting.key' "value"

# Delete a runtime config value (reverts to TOML default)
docker exec stalwart /usr/local/bin/stalwart-cli \
  -u http://localhost:8080 -c admin:TOKEN \
  server delete-config 'some.setting.key'

# Reload configuration from TOML file
docker exec stalwart /usr/local/bin/stalwart-cli \
  -u http://localhost:8080 -c admin:TOKEN \
  server reload-config

For full Stalwart setup details, see Stalwart Setup.


Database Configuration

PostgreSQL Connection

NorthRelay uses PostgreSQL 15 with Prisma as the ORM. The schema source of truth is prisma/schema.prisma.

# Connection string format
DATABASE_URL="postgresql://USER:PASSWORD@HOST:PORT/DATABASE"

# Docker Compose internal
DATABASE_URL="postgresql://north_user:password@postgres:5432/northrelay"

# External access
DATABASE_URL="postgresql://north_user:password@51.161.10.58:5432/northrelay"

Schema Management

# Validate schema
npx prisma validate

# Generate Prisma client after schema changes
npx prisma generate

# Push schema to database (development - no migration history)
npx prisma db push

# Run migrations (production - creates migration history)
npx prisma migrate deploy

# Open Prisma Studio (GUI)
npx prisma studio

Key Schema Conventions

  • All table names use snake_case with @@map directive
  • Key enums: plan_tier, pool_type, user_role, user_status, email_status
  • Every model requires a corresponding Prisma model in schema.prisma

Redis Configuration

Redis is used for:

  • Rate limiting - Per-user SMTP send rate enforcement
  • Caching - Session data, DNS lookup caching
  • Queues - Background job processing
# Docker Compose configuration
redis:
  image: redis:7-alpine
  command: redis-server --requirepass ${REDIS_PASSWORD}
  ports:
    - "6379:6379"
  volumes:
    - redis_data:/data

If Redis is unavailable, NorthRelay falls back to in-memory caching with reduced functionality.


Cron Jobs

NorthRelay uses a lightweight Alpine curl container to trigger scheduled API endpoints:

ScheduleEndpointPurpose
Every 60s/api/v1/emails/schedule/processProcess scheduled email sends
Every 60s/api/v1/emails/batch/processProcess batch email operations
Every 60min/api/cron/reputation-snapshotCalculate reputation scores and detect incidents

All cron requests authenticate with the CRON_SECRET as a Bearer token:

# Example cron request
curl -s -H "Authorization: Bearer ${CRON_SECRET}" \
  http://app:3000/api/v1/emails/schedule/process

The cron container is defined in docker-compose.yml and runs continuously with crond managing the schedule.


DKIM Configuration

NorthRelay uses OpenDKIM on relay VPSes for DKIM signing, not Stalwart's built-in DKIM.

Why Relay-Based Signing

  • Stalwart's DKIM is disabled (queue.outbound.sign.enable = false)
  • OpenDKIM is battle-tested for dynamic domain signing
  • Signing at the network edge means signatures are never broken by intermediate processing

Adding DKIM for a New Domain

# 1. Generate RSA-2048 key
openssl genrsa -out yourdomain.com.key 2048

# 2. Extract public key for DNS
openssl rsa -in yourdomain.com.key -pubout -outform PEM | \
  grep -v '^-' | tr -d '\n'

# 3. Copy key to relay VPS
scp yourdomain.com.key relay:/opt/relay/dkim/yourdomain.com.default.key

# 4. Add to OpenDKIM KeyTable
echo "default._domainkey.yourdomain.com yourdomain.com:default:/etc/opendkim/keys/yourdomain.com.default.key" \
  >> /opt/relay/KeyTable

# 5. Add to OpenDKIM SigningTable
echo "*@yourdomain.com default._domainkey.yourdomain.com" \
  >> /opt/relay/SigningTable

# 6. Restart OpenDKIM
docker compose restart opendkim

DNS Record

Publish the public key as a TXT record:

default._domainkey.yourdomain.com TXT "v=DKIM1; k=rsa; p=YOUR_PUBLIC_KEY_BASE64"

OpenDKIM Configuration

Key settings in opendkim.conf:

SubDomains    yes       # Signs subdomain emails with parent domain key
Canonicalization relaxed/simple
Mode          s         # Sign only (not verify)

Domain Separation

NorthRelay enforces strict domain separation between corporate and user traffic:

DomainPurposeUsage
northrelay.caCorporateDashboard, docs, admin email - never for user SMTP traffic
north-pipe.caSystemUser subdomains, HELO identity, return-path - all user email traffic

This separation protects the corporate domain reputation from user sending behavior.

User Subdomain Format

Each user gets a subdomain on the system domain:

{prefix}@{prefix}.north-pipe.ca

For example: quantum@quantum.north-pipe.ca


Next Steps

On this page