enterprise/workers/step-resolver/README.md
Cloudflare Workers for Platforms dispatch worker for Step Resolver resolution.
enterprise/workers/step-resolver/
src/index.ts # HTTP routes + dispatch logic
src/auth/hmac.ts # request signature validation
src/utils/worker-id.ts # worker id mapping
wrangler.jsonc # worker + namespace config
This package is part of the pnpm workspace via enterprise/workers/*.
X-Novu-Signature in format t={timestamp},v1={hmac}).sr-${organizationId}-${stepResolverHash}.DISPATCHER binding).x-request-id.GET /health200 with JSON status payload.GET returns 405.POST /resolve/:organizationId/:stepResolverHash/:stepIdRoute validation (strict):
organizationId: lowercase hex, exactly 24 chars ([a-f0-9]{24})stepResolverHash: format sr-xxxxx-xxxxx (e.g., sr-abc12-def34)stepId: one URL path segment ([^/]+)Content-Type: must be application/json26MB (supports ~20MB raw attachments after base64 and JSON envelope overhead)Auth headers:
X-Novu-Signature: Signature header in format t={timestamp},v1={hmac}On success, request is forwarded as:
POST/resolve/... pathstep=<decoded stepId>x-novu-signature, authorization, x-internal-authUses the same signature format as @novu/framework Bridge authentication, but with a different secret for different trust boundaries:
NOVU_SECRET_KEY to authenticate Novu Cloud → Customer's Bridge EndpointSTEP_RESOLVER_HMAC_SECRET to authenticate Novu API → Novu's Cloudflare WorkersThis separation ensures customer secrets protect their infrastructure while platform secrets protect Novu's worker infrastructure, without requiring per-customer secret lookups in workers.
Signature format:
X-Novu-Signature: t={timestamp},v1={hmac}
HMAC computed over:
${timestamp}.${rawRequestBody}
Note: The HMAC is computed over the raw request body bytes (UTF-8 decoded string), not a re-serialized JSON object. This ensures canonical validation against the exact bytes received.
Validation notes:
300 seconds (5 minutes)import { createHmac } from 'node:crypto';
const secret = process.env.STEP_RESOLVER_HMAC_SECRET!;
const payload = {
payload: { firstName: 'Ada' },
subscriber: { email: '[email protected]' },
context: {},
steps: {},
};
const timestamp = Date.now();
const bodyString = JSON.stringify(payload);
const data = `${timestamp}.${bodyString}`;
const hmac = createHmac('sha256', secret).update(data).digest('hex');
const signature = `t=${timestamp},v1=${hmac}`;
// Send as headers:
// X-Novu-Signature: t=1234567890,v1=abc123...
// Body: <bodyString> (same string used in HMAC computation)
Install dependencies from repo root:
pnpm install
Run with workspace filter from repo root:
pnpm --filter @novu/step-resolver-worker dev
Or run directly from this folder:
pnpm run dev
For local wrangler dev, provide the secret (for example via .dev.vars):
STEP_RESOLVER_HMAC_SECRET=local-dev-secret
From enterprise/workers/step-resolver:
| Wrangler env | Domain | Dispatch namespace |
|---|---|---|
staging | *.workers.dev | novu-step-resolvers-staging |
production (US) | step-resolver.novu.co | novu-step-resolvers-production |
production-eu | eu.step-resolver.novu.co | novu-step-resolvers-production-eu |
EU follows the same multi-region pattern as @novu/socket-worker: separate wrangler env, custom domain, and dispatch namespace in the same Cloudflare account. US (production) is unchanged.
EU compute residency uses two Cloudflare mechanisms:
eu.step-resolver.novu.co.production-eu wrangler env and tenant worker deploy metadata target aws:eu-central-1.pnpm run namespace:create:staging
pnpm run namespace:create:production
pnpm run namespace:create:production-eu
Add the eu.step-resolver.novu.co custom domain in the Cloudflare dashboard (same account as US).
Enable Regional Services for the EU hostname (requires Data Localization Suite on the account):
export CLOUDFLARE_ZONE_ID="<novu.co zone id>"
export STEP_RESOLVER_CF_API_TOKEN="<token with Zone:Edit + DLS permissions>"
pnpm run regional-services:production-eu
pnpm run deploy:staging
pnpm run deploy:production
pnpm run deploy:production-eu
pnpm run secret:staging
pnpm run secret:production
pnpm run secret:production-eu
pnpm run deploy:staging
pnpm run deploy:production
pnpm run deploy:production-eu
If namespace names differ from your Cloudflare account, update wrangler.jsonc.
The EU apps/api and apps/worker stacks (GitHub environment production-eu, Secrets Manager) must set region-specific values. No code changes are required — the same env keys are used; only values differ per region.
| Variable | US (production wrangler / production-us ECS) | EU (production-eu) |
|---|---|---|
STEP_RESOLVER_DISPATCH_URL | https://step-resolver.novu.co | https://eu.step-resolver.novu.co |
STEP_RESOLVER_CF_DISPATCH_NAMESPACE | novu-step-resolvers-production | novu-step-resolvers-production-eu |
STEP_RESOLVER_CF_ACCOUNT_ID | Cloudflare account ID | Same account ID |
STEP_RESOLVER_CF_API_TOKEN | CF API token | CF API token (scoped to EU namespace if possible) |
STEP_RESOLVER_CF_PLACEMENT_REGION | (unset) | aws:eu-central-1 |
STEP_RESOLVER_HMAC_SECRET | US dispatch worker secret | EU dispatch worker secret (from secret:production-eu) |
STEP_RESOLVER_CF_PLACEMENT_REGION is passed as tenant worker placement.region metadata on deploy. Use provider:region format (e.g. aws:eu-central-1). Set it only on the EU API stack so customer code steps are placed near EU infrastructure. Leave unset on US.
apps/api uses all six deploy/runtime variables above. apps/worker uses STEP_RESOLVER_DISPATCH_URL and STEP_RESOLVER_HMAC_SECRET (runtime only).
Step resolver is gated by IS_STEP_RESOLVER_ENABLED and IS_ACTION_STEP_RESOLVER_ENABLED. The API already sends region context from NOVU_REGION to LaunchDarkly. Extend existing US targeting to include region = eu before enabling step resolver for EU customers.
After EU rollout:
novu step publish --api-url https://eu.api.novu.co).novu-step-resolvers-production-eu namespace (Cloudflare dashboard).https://eu.step-resolver.novu.co with a valid HMAC.US production:
DISPATCH_URL="https://step-resolver.novu.co"
EU production:
DISPATCH_URL="https://eu.step-resolver.novu.co"
Staging:
DISPATCH_URL="https://step-resolver-dispatch-staging.<subdomain>.workers.dev"
ORGANIZATION_ID="696a21b632ef1f83460d584d"
STEP_RESOLVER_HASH="abc12-def34"
STEP_ID="welcome-email"
SECRET="${STEP_RESOLVER_HMAC_SECRET:?set STEP_RESOLVER_HMAC_SECRET}"
PATHNAME="/resolve/${ORGANIZATION_ID}/sr-${STEP_RESOLVER_HASH}/${STEP_ID}"
BODY='{"payload":{"firstName":"Ada"},"subscriber":{"email":"[email protected]"},"context":{},"steps":{}}'
# Create HMAC signature using Framework format
TIMESTAMP="$(node -e 'console.log(Date.now())')"
DATA="${TIMESTAMP}.${BODY}"
HMAC="$(printf '%s' "$DATA" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $2}')"
SIGNATURE="t=${TIMESTAMP},v1=${HMAC}"
curl -i -X POST "${DISPATCH_URL}${PATHNAME}" \
-H "Content-Type: application/json" \
-H "X-Novu-Signature: ${SIGNATURE}" \
-d "$BODY"
Set DISPATCH_URL to the US, EU, or staging endpoint above before running.