← All posts
· 3 min read ·
AWSSalesforceTerraformPythonArchitectureOpen Source

aws-sfdc-event-bridge: Connecting Salesforce Platform Events to AWS

How I built a Terraform-managed bridge between Salesforce Platform Events and Amazon EventBridge - with HMAC webhook validation, fan-out to SQS/SNS/Step Functions, and zero long-lived credentials.

Server room with rows of illuminated rack servers

Salesforce Platform Events are a powerful real-time messaging mechanism inside the Salesforce ecosystem. The problem: they don’t natively fan out to AWS services. If you want an order event from Salesforce to trigger a Step Functions workflow, update a DynamoDB table, and notify an SQS queue - you need a bridge.

aws-sfdc-event-bridge is that bridge, built with API Gateway, Python Lambda, and EventBridge, all managed by Terraform.

Architecture

Salesforce Platform Event

        ▼ (Outbound Message / Apex callout)
API Gateway (REST endpoint)

        ▼ HMAC-SHA256 validation
Python Lambda


Amazon EventBridge (custom event bus)

   ┌────┼────┐
   ▼    ▼    ▼
 SQS   SNS  Step Functions

Salesforce sends events to the API Gateway endpoint via either an Outbound Message (declarative, no Apex required for simple payloads) or an Apex callout from a Platform Event trigger. The Lambda validates the HMAC signature, parses the payload, and publishes a structured event to EventBridge.

From EventBridge, rules route events to any number of downstream targets: SQS queues for async processing, SNS topics for fan-out to multiple consumers, or Step Functions for orchestrated workflows.

HMAC Validation

The most important security layer is the HMAC signature check. Without it, anyone who discovers the API Gateway URL can inject arbitrary events into your AWS infrastructure.

The Salesforce side generates a signature:

String payload = JSON.serialize(eventData);
String hmac = EncodingUtil.convertToHex(
    Crypto.generateMac('HmacSHA256',
        Blob.valueOf(payload),
        Blob.valueOf(secret)
    )
);
req.setHeader('X-Salesforce-HMAC', hmac);

The Lambda validates it:

import hmac, hashlib

def validate_signature(body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(),
        body,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

hmac.compare_digest is critical - it prevents timing attacks that could otherwise be used to brute-force the secret by measuring response latency.

The secret lives in AWS Secrets Manager, not in the Lambda environment variables. The Lambda fetches it on cold start and caches it for the duration of the execution environment.

EventBridge Event Structure

Every event published to EventBridge follows a consistent envelope:

{
  "source": "salesforce.platform-events",
  "detail-type": "OrderPlaced__e",
  "detail": {
    "orderId": "a1B3x000001ABCDEF",
    "customerId": "0013x000001ABCDEF",
    "amount": 149.99,
    "currency": "GBP",
    "timestamp": "2026-02-03T14:23:00Z"
  }
}

detail-type is the Salesforce Platform Event API name. EventBridge rules can match on this to route specific event types to specific targets:

resource "aws_cloudwatch_event_rule" "order_placed" {
  event_bus_name = aws_cloudwatch_event_bus.salesforce.name
  event_pattern = jsonencode({
    source      = ["salesforce.platform-events"]
    detail-type = ["OrderPlaced__e"]
  })
}

Terraform Layout

The entire infrastructure is defined in Terraform, split by concern:

terraform/
├── api_gateway.tf        # REST API, stage, deployment
├── lambda.tf             # function, IAM role, log group
├── eventbridge.tf        # custom bus, rules, targets
├── secrets.tf            # HMAC secret in Secrets Manager
├── sqs.tf                # target queues (with DLQ)
└── variables.tf          # environment-agnostic config

No long-lived credentials anywhere. The Lambda’s IAM role has least-privilege permissions: events:PutEvents on the custom bus, secretsmanager:GetSecretValue on the HMAC secret, nothing else. API Gateway uses a resource policy to allow invocation from Lambda only.

Dead Letter Queues and Retries

Lambda is configured with a dead-letter queue for failed invocations (e.g. if Secrets Manager is temporarily unavailable). EventBridge rules have retry policies. SQS targets have their own DLQs.

The failure handling chain:

Lambda fails → DLQ (Lambda DLQ)
EventBridge delivery fails → EventBridge retry (up to 24h)
SQS consumer fails → SQS DLQ after maxReceiveCount

Each layer’s DLQ publishes to an SNS topic that emails the ops team. No silent failures.

Deployment

cd terraform/
terraform init
terraform plan -var="environment=prod"
terraform apply -var="environment=prod"

The outputs include the API Gateway URL, which you configure in Salesforce as the callout endpoint. The HMAC secret is generated by Terraform and stored in Secrets Manager - you retrieve it once to configure the Apex side:

aws secretsmanager get-secret-value \
  --secret-id sfdc-event-bridge-hmac-prod \
  --query SecretString --output text

Why Not Just Use MuleSoft?

MuleSoft is the canonical answer for Salesforce–AWS integration in enterprise environments. It’s also expensive, operationally complex, and overkill for event forwarding. For organisations that are already AWS-native and don’t need the full MuleSoft feature set (data transformation, complex routing, API management), a lightweight Lambda-based bridge is faster to deploy, cheaper to run, and easier to maintain.

The project is on GitHub.

← All posts