Docs / SNS

SNS

SNS is a custom Python reimplementation. 36 of the 42 SNS operations are implemented in LocalEmu's own code: standard and FIFO topics, nine subscription protocols (HTTP, HTTPS, email, email-json, SMS, SQS, application, Lambda, Firehose), filter policies on message attributes or message body, per-subscription DLQs, FIFO content-based deduplication with a 5-minute window, subscription confirmation tokens, application platform endpoints for mobile push, phone-number opt-out, and three CloudFormation resource types.

SMS and mobile push (the sms and application subscription protocols) do not reach a real carrier or push service. Messages are stored in LocalEmu and can be inspected over HTTP at /_aws/sns/sms-messages and /_aws/sns/platform-endpoint-messages. The other seven protocols deliver to the corresponding LocalEmu service.

Operation-level coverage: see the SNS coverage matrix.

Quick start

Create a topic, subscribe an SQS queue, publish a message, read it from the queue.

Terminal
$ awsemu sns create-topic --name orders
{
  "TopicArn": "arn:aws:sns:us-east-1:000000000000:orders"
}

$ awsemu sqs create-queue --queue-name order-events
$ QURL=http://sqs.us-east-1.localhost:4566/000000000000/order-events
$ QARN=$(awsemu sqs get-queue-attributes --queue-url $QURL \
    --attribute-names QueueArn --query 'Attributes.QueueArn' --output text)

$ awsemu sns subscribe \
    --topic-arn arn:aws:sns:us-east-1:000000000000:orders \
    --protocol sqs \
    --notification-endpoint $QARN

$ awsemu sns publish \
    --topic-arn arn:aws:sns:us-east-1:000000000000:orders \
    --message '{"order_id":"o-42","total":99}'

$ awsemu sqs receive-message --queue-url $QURL --query 'Messages[0].Body' --output text
{"Type":"Notification","MessageId":"...","TopicArn":"arn:aws:sns:us-east-1:000000000000:orders","Message":"{\"order_id\":\"o-42\",\"total\":99}", ...}

Architecture

SNS lives at services/sns/. The provider class SnsProvider (provider.py:156) handles the API surface and routes published messages through the PublishDispatcher:

State lives in SnsStore (models.py:182): topics, subscriptions, FIFO dedup cache, platform applications, platform endpoint messages, SMS messages, opted-out phone numbers, and subscription confirmation tokens. When PERSISTENCE=1 is set, the store is serialised and restored across restarts.

Subscription protocols

Nine protocols are accepted by Subscribe (services/sns/constants.py:7-17). Each has its own publisher class in services/sns/publisher.py.

ProtocolDeliverySource
sqsSendMessage (single) or SendMessageBatch to the subscribed queue.publisher.py:314,416
lambdaInvoke with InvocationType=Event (async).publisher.py:193
http / httpsPOST to the subscriber endpoint with the SNS notification envelope. Requires confirmation via the SubscribeURL in the envelope.publisher.py:517
email / email-jsonSES SendEmail from SNS_SES_SENDER_ADDRESS (falls back to no-reply@localemu.cloud). The subscription stays in PendingConfirmation until ConfirmSubscription is called.publisher.py:602,596
firehoseFirehose PutRecord to the configured delivery stream. SubscriptionRoleArn is required.publisher.py:769
smsStored only. The message is appended to an in-memory buffer (max 10 000 messages). No carrier delivery. Inspect via GET /_aws/sns/sms-messages.publisher.py:704
applicationStored only. Messages for mobile push platform endpoints (APNs, FCM, ADM, etc.) are stored, not forwarded to a real push service. Inspect via GET /_aws/sns/platform-endpoint-messages.publisher.py:647

Configuration

VariableDefaultPurpose
SNS_SES_SENDER_ADDRESSno-reply@localemu.cloudFrom address used when SNS sends an email subscription notification via SES. Set this to a verified SES identity if you turn on SES sender enforcement.
SNS_CERT_URL_HOST(unset)Override the host portion of the certificate URL embedded in HTTP/HTTPS notification envelopes. Subscribers that verify SNS message signatures fetch the certificate from this host.

Features supported

FeatureNotes
Topic lifecycleCreateTopic, ListTopics, GetTopicAttributes, SetTopicAttributes, DeleteTopic.
Standard and FIFO topicsFIFO topics end in .fifo and require FifoTopic=true. A FIFO topic can only subscribe to FIFO queues; a standard topic can subscribe to either.
PublishPublish and PublishBatch. FIFO topics return SequenceNumber on each publish.
Subscribe and confirmSubscribe, ConfirmSubscription, Unsubscribe, ListSubscriptions, ListSubscriptionsByTopic, GetSubscriptionAttributes, SetSubscriptionAttributes.
Subscription attributesFilterPolicy, FilterPolicyScope, RawMessageDelivery, RedrivePolicy, DeliveryPolicy, SubscriptionRoleArn.
Filter policiesScope = MessageAttributes (default) or MessageBody. Operators: string, prefix, suffix, numeric (<, <=, =, >=, >), anything-but, exists. Max 5 keys, max 150 value combinations.
Per-subscription DLQRedrivePolicy points at an SQS queue. Failed deliveries land there with the original message.
Raw message deliveryRawMessageDelivery=true sends the bare message body to the subscriber instead of the SNS envelope.
FIFO content-based deduplicationContentBasedDeduplication=true derives the dedup ID from SHA-256 of the body. Otherwise MessageDeduplicationId is required on publish.
Topic tagsTagResource, UntagResource, ListTagsForResource.
Topic policyAddPermission, RemovePermission. Policy is stored and returned but not enforced on incoming requests.
Mobile push platform endpointsCreatePlatformApplication, CreatePlatformEndpoint, GetEndpointAttributes, SetEndpointAttributes, DeleteEndpoint. Push delivery is stored only.
Phone-number opt-outCheckIfPhoneNumberIsOptedOut, OptInPhoneNumber, ListPhoneNumbersOptedOut.
CloudFormationAWS::SNS::Topic, AWS::SNS::Subscription, AWS::SNS::TopicPolicy.

FIFO topics

FIFO topics preserve publish order within a MessageGroupId and deduplicate messages within a 5-minute window. The dispatch executor partitions work by group ID, so messages in the same group run serially while different groups run in parallel.

Terminal
$ awsemu sns create-topic --name orders.fifo \
    --attributes FifoTopic=true,ContentBasedDeduplication=true

$ awsemu sqs create-queue --queue-name order-events.fifo \
    --attributes FifoQueue=true,ContentBasedDeduplication=true

$ QARN=arn:aws:sqs:us-east-1:000000000000:order-events.fifo
$ awsemu sns subscribe \
    --topic-arn arn:aws:sns:us-east-1:000000000000:orders.fifo \
    --protocol sqs \
    --notification-endpoint $QARN

# Three messages, same MessageGroupId => strict order downstream
$ for i in 1 2 3; do
    awsemu sns publish \
        --topic-arn arn:aws:sns:us-east-1:000000000000:orders.fifo \
        --message-group-id customer-42 \
        --message "{\"order\": $i}"
  done

$ awsemu sqs receive-message \
    --queue-url http://sqs.us-east-1.localhost:4566/000000000000/order-events.fifo \
    --max-number-of-messages 10 --wait-time-seconds 2 \
    --query 'Messages[].Body' --output json | python3 -c "import sys,json; [print(json.loads(m)[\"Message\"]) for m in json.load(sys.stdin)]"
{"order": 1}
{"order": 2}
{"order": 3}

Filter policies

A subscription's FilterPolicy attribute decides which published messages reach that subscription. The policy is JSON; each key maps to a list of value matchers. The default FilterPolicyScope is MessageAttributes; set it to MessageBody to match against the message body instead.

Operators are exact-string, numeric ranges, prefix, suffix, anything-but, and exists. A policy can declare up to 5 keys and the cross-product of all value alternatives must not exceed 150 combinations (filter.py:290 and filter.py:298).

Terminal
$ awsemu sns subscribe \
    --topic-arn arn:aws:sns:us-east-1:000000000000:orders \
    --protocol sqs \
    --notification-endpoint $QARN \
    --attributes '{
      "FilterPolicy": "{\"priority\":[\"high\"],\"total\":[{\"numeric\":[\">\",100]}]}",
      "FilterPolicyScope": "MessageAttributes"
    }'

# This message matches: priority=high AND total>100. It is delivered.
$ awsemu sns publish \
    --topic-arn arn:aws:sns:us-east-1:000000000000:orders \
    --message 'priority order' \
    --message-attributes '{
      "priority": {"DataType":"String","StringValue":"high"},
      "total":    {"DataType":"Number","StringValue":"500"}
    }'

# This message does NOT match (priority=low). It is dropped before reaching the queue.
$ awsemu sns publish \
    --topic-arn arn:aws:sns:us-east-1:000000000000:orders \
    --message 'noise' \
    --message-attributes '{
      "priority": {"DataType":"String","StringValue":"low"}
    }'

Wire a topic to Lambda

Terminal
$ awsemu lambda create-function \
    --function-name on-order \
    --runtime python3.12 \
    --role arn:aws:iam::000000000000:role/lambda-role \
    --handler handler.handler \
    --zip-file fileb://handler.zip

$ FARN=arn:aws:lambda:us-east-1:000000000000:function:on-order
$ awsemu sns subscribe \
    --topic-arn arn:aws:sns:us-east-1:000000000000:orders \
    --protocol lambda \
    --notification-endpoint $FARN

$ awsemu sns publish \
    --topic-arn arn:aws:sns:us-east-1:000000000000:orders \
    --message '{"order_id":"o-99"}'

# The Lambda is invoked asynchronously with the SNS event envelope
$ awsemu logs tail /aws/lambda/on-order --follow

Inspecting stored protocols

SMS and mobile push do not leave LocalEmu. The published messages are kept in memory and four internal HTTP endpoints let you inspect them:

EndpointContents
GET /_aws/sns/sms-messagesEvery SMS that would have been sent through the sms protocol or a direct phone-number publish. Bounded to the last 10 000 messages.
GET /_aws/sns/platform-endpoint-messagesEvery message routed to a mobile-push platform endpoint, grouped by endpoint ARN.
GET /_aws/sns/subscription-tokensMap of confirmation tokens to subscription ARNs. Use the token to call ConfirmSubscription for HTTP/HTTPS or email subscriptions.
GET /_aws/sns/phone-opt-outsPhone numbers that called OptOutPhoneNumber. Direct publishes and sms protocol subscriptions to these numbers are dropped.
Terminal
# Inspect the SMS publish history (sms protocol + direct phone-number publishes)
$ curl -s http://localhost:4566/_aws/sns/sms-messages

# Inspect mobile push history (application platform endpoints)
$ curl -s http://localhost:4566/_aws/sns/platform-endpoint-messages

# Look up subscription confirmation tokens
$ curl -s http://localhost:4566/_aws/sns/subscription-tokens

# Phone numbers that called OptOutPhoneNumber
$ curl -s http://localhost:4566/_aws/sns/phone-opt-outs

Subscription confirmation

When you call Subscribe, the behaviour depends on the protocol:

ProtocolConfirmation behaviour
sqs, lambda, firehose, application, smsAuto-confirmed at Subscribe time. PendingConfirmation is immediately false.
http, httpsSNS POSTs a SubscriptionConfirmation envelope to the subscriber URL. The subscriber must call the SubscribeURL inside that envelope (a GET to ConfirmSubscription with a token) before deliveries start.
email, email-jsonThe subscription stays in PendingConfirmation. The confirmation email is sent through SES. Look up the token at /_aws/sns/subscription-tokens and call ConfirmSubscription manually.

Tokens are 288 hex characters and encode the region. Token validation strips region back out so cross-region confirmations work without extra plumbing.

Limits and defaults

LimitLocalEmuAWSSource
Max message size256 KiB256 KiBservices/sns/constants.py:64
FIFO dedup window5 min5 minservices/sns/provider.py:836
Filter policy keys55services/sns/filter.py:290
Filter policy combinations150150services/sns/filter.py:298
ListTopics / ListSubscriptions page size100100services/sns/provider.py:331,691,711
SMS message buffer10 000 (in-memory)n/aservices/sns/models.py:211
Max subject lengthnot enforced100 charsLocalEmu accepts longer subjects.
Max message attributes per publishnot enforced10LocalEmu accepts more.

Known limitations