# SNS **Protocol:** Query (XML) and JSON 1.0 (both supported) **Endpoint:** `POST http://localhost:4566/` ## Supported Actions | Action | Description | |---|---| | `CreateTopic` | Create a topic | | `DeleteTopic` | Delete a topic | | `ListTopics` | List all topics | | `GetTopicAttributes` | Get topic configuration | | `SetTopicAttributes` | Update topic configuration | | `Subscribe` | Subscribe an endpoint (SQS, HTTP, Lambda, email) | | `Unsubscribe` | Remove a subscription | | `ListSubscriptions` | List all subscriptions | | `ListSubscriptionsByTopic` | List subscriptions for a specific topic | | `GetSubscriptionAttributes` | Get subscription settings | | `SetSubscriptionAttributes` | Update subscription settings | | `ConfirmSubscription` | Confirm a pending subscription | | `Publish` | Publish a message to a topic | | `PublishBatch` | Publish up to 10 messages in one call | | `TagResource` | Tag a topic | | `UntagResource` | Remove tags from a topic | | `ListTagsForResource` | List tags on a topic | | `CreatePlatformApplication` | Create a mobile push platform app (iOS or Android) | | `DeletePlatformApplication` | Delete a platform app and its endpoints | | `GetPlatformApplicationAttributes` | Read platform app attributes | | `SetPlatformApplicationAttributes` | Update platform app attributes (e.g. `Enabled`) | | `ListPlatformApplications` | List platform applications in the region | | `CreatePlatformEndpoint` | Register a device token under a platform app | | `DeleteEndpoint` | Delete a platform endpoint | | `GetEndpointAttributes` | Read endpoint attributes | | `SetEndpointAttributes` | Update endpoint attributes (e.g. `Enabled=false` to simulate token expiry) | | `ListEndpointsByPlatformApplication` | List endpoints under a platform app | ## Configuration | Variable | Default | Description | |---|---|---| | `FLOCI_SERVICES_SNS_ENABLED` | `true` | Enable or disable the service | | `FLOCI_STORAGE_SERVICES_SNS_MODE` | *(global default)* | Storage mode override for SNS (`memory`, `persistent`, `hybrid`, `wal`) | | `FLOCI_STORAGE_SERVICES_SNS_FLUSH_INTERVAL_MS` | `5000` | Flush interval for `hybrid`/`wal` storage modes (milliseconds) | ## Examples ```bash export AWS_ENDPOINT_URL=http://localhost:4566 # Create a topic TOPIC_ARN=$(aws sns create-topic --name notifications \ --query TopicArn --output text \ --endpoint-url $AWS_ENDPOINT_URL) # Subscribe an SQS queue QUEUE_ARN=$(aws sqs get-queue-attributes \ --queue-url $AWS_ENDPOINT_URL/000000000000/orders \ --attribute-names QueueArn \ --query Attributes.QueueArn --output text \ --endpoint-url $AWS_ENDPOINT_URL) aws sns subscribe \ --topic-arn $TOPIC_ARN \ --protocol sqs \ --notification-endpoint $QUEUE_ARN \ --endpoint-url $AWS_ENDPOINT_URL # Publish a message aws sns publish \ --topic-arn $TOPIC_ARN \ --message '{"event":"user.registered"}' \ --endpoint-url $AWS_ENDPOINT_URL # Fan-out: publish and verify the SQS queue received the message aws sqs receive-message \ --queue-url $AWS_ENDPOINT_URL/000000000000/orders \ --endpoint-url $AWS_ENDPOINT_URL ``` ## SNS → SQS Fan-Out Floci supports real SNS → SQS fan-out. When you publish to a topic, all SQS-subscribed queues receive the message immediately. Supported subscription protocols: - `sqs`: delivers to a Floci SQS queue - `lambda`: invokes a Floci Lambda function - `firehose`: puts records to a Floci Firehose delivery stream - `http` / `https`: posts to an HTTP endpoint - `application`: fans out to a mobile push platform endpoint (see [Mobile push](#mobile-push-mock)) ## Message size `MaximumMessageSize` is the per-topic limit, in bytes, on a published payload. It accepts `1024` to `1048576` (1 MiB) and defaults to `262144` (256 KiB). AWS raised the maximum in September 2026 but left the default alone, so a topic that never sets the attribute behaves exactly as it did before. `GetTopicAttributes` omits the attribute until it is set rather than reporting the default. ```bash aws sns set-topic-attributes --topic-arn $TOPIC_ARN \ --attribute-name MaximumMessageSize --attribute-value 1048576 \ --endpoint-url $AWS_ENDPOINT_URL ``` The limit counts the message body plus, per message attribute, its name, data type and value. `Subject` is not counted. `PublishBatch` counts the sum of all its entries against the same limit and fails the whole call rather than individual entries. A topic above `262144` is restricted: at most 100 subscriptions, every one of them `sqs`, `firehose` or `lambda`. Floci enforces the protocol rule on `Subscribe` and `SetTopicAttributes` alike, and the subscription count on `SetTopicAttributes` only — matching AWS, which lets an already-raised topic drift past 100 and catches it the next time the attribute is set. Pending confirmations count towards both. | Action | Condition | Error code | HTTP | |---|---|---|---| | `CreateTopic`, `SetTopicAttributes` | `MaximumMessageSize` not an integer between 1024 and 1048576 | `InvalidParameter` | 400 | | `SetTopicAttributes` | Raised above 262144 with a subscription that is not `sqs`, `firehose` or `lambda` | `InvalidParameter` | 400 | | `SetTopicAttributes` | Raised above 262144 with more than 100 subscriptions | `InvalidParameter` | 400 | | `Subscribe` | Unsupported protocol on a topic above 262144 | `InvalidParameter` | 400 | | `Publish` | Payload exceeds the topic's `MaximumMessageSize` | `InvalidParameter` | 400 | | `PublishBatch` | Entries sum to more than the topic's `MaximumMessageSize` | `BatchRequestTooLong` | 400 | CloudFormation carries the setting through: `AWS::SNS::Topic` forwards `MaximumMessageSize`, and an update that drops the property returns the topic to the default. A raised topic fanning out to SQS needs headroom. The notification envelope wraps the body in a few hundred bytes of JSON, so a publish at 1 MiB no longer fits a queue at the SQS maximum and is dropped on delivery — silently, as any delivery failure is. Keep the topic below the queue's own `MaximumMessageSize`, or subscribe with `RawMessageDelivery=true` so the body is forwarded unwrapped. ## FIFO topics A topic whose name ends in `.fifo` is a FIFO topic. `Publish` and `PublishBatch` require a `MessageGroupId`, and a message is deduplicated against its `MessageDeduplicationId` for five minutes. Set `ContentBasedDeduplication` on the topic to derive that id from the message body. The `FifoThroughputScope` attribute decides how wide that deduplication reaches: | Value | Deduplication scope | |---|---| | `Topic` (default) | Across the whole topic: the same `MessageDeduplicationId` is a duplicate no matter which message group it arrives under | | `MessageGroup` | Within a single message group: the same `MessageDeduplicationId` under two different `MessageGroupId`s is two distinct messages | `MessageGroup` is what a fan-out that reuses one deduplication id per group needs, for example publishing the same event to a topic once per tenant with the tenant as the message group. ```bash aws sns create-topic --name events.fifo \ --attributes FifoTopic=true,FifoThroughputScope=MessageGroup \ --endpoint-url $AWS_ENDPOINT_URL ``` When the topic forwards to an SQS FIFO queue, set the matching queue attributes (`DeduplicationScope=messageGroup` and `FifoThroughputLimit=perMessageGroupId`), otherwise the queue deduplicates topic-wide on the way in. CloudFormation carries both settings through: `AWS::SNS::Topic` forwards `FifoThroughputScope` and `ContentBasedDeduplication`, and a FIFO topic left unnamed gets a generated name ending in `.fifo`. ## Control Tower managed topic AWS Control Tower creates the regional `aws-controltower-AggregateSecurityNotifications` topic before Landing Zone Accelerator deploys its audit notification forwarder. Floci lazily materializes that exact same-account managed topic when `Subscribe` first references it, matching the Control Tower prerequisite without weakening normal SNS validation. Subscribing to any other missing topic still returns `NotFound`. ## Mobile push (mock) Floci mocks SNS mobile push for iOS and Android. No real APNS or FCM connection is made — every push is captured in memory so tests can assert what would have been sent. **Supported platforms:** `APNS`, `APNS_SANDBOX`, `GCM`, `FCM`. Any other platform value returns `InvalidParameter`. ### End-to-end flow ```bash APP_ARN=$(aws sns create-platform-application \ --name ios-app --platform APNS \ --attributes PlatformCredential=fake-cert \ --endpoint-url http://localhost:4566 --query PlatformApplicationArn --output text) ENDPOINT_ARN=$(aws sns create-platform-endpoint \ --platform-application-arn $APP_ARN \ --token ios-device-token-abc \ --endpoint-url http://localhost:4566 --query EndpointArn --output text) # Plain string payload aws sns publish --target-arn $ENDPOINT_ARN --message '{"aps":{"alert":"hi"}}' \ --endpoint-url http://localhost:4566 # Platform-specific payloads with MessageStructure=json aws sns publish --target-arn $ENDPOINT_ARN --message-structure json \ --message '{"default":"fallback","APNS":"{\"aps\":{\"alert\":\"ios\"}}","GCM":"{\"notification\":{\"body\":\"android\"}}"}' \ --endpoint-url http://localhost:4566 ``` When `MessageStructure="json"`, Floci picks the key matching the endpoint's platform (`APNS`, `APNS_SANDBOX`, `GCM`, or `FCM`), falling back to `default`. The envelope must be a JSON object and must include `default` — otherwise `InvalidParameter`. ### Broadcast to devices via a topic Subscribe platform endpoints to a topic with `Protocol="application"`, then publish to the topic to fan out to every subscribed device — each endpoint is captured exactly as if you had published to it directly (same platform-payload resolution, same `Enabled` gating). A disabled endpoint in the fan-out is skipped; the rest still receive the push. ```bash aws sns subscribe --topic-arn $TOPIC_ARN \ --protocol application --notification-endpoint $ENDPOINT_ARN \ --endpoint-url http://localhost:4566 aws sns publish --topic-arn $TOPIC_ARN --message-structure json \ --message '{"default":"market open","GCM":"{\"notification\":{\"body\":\"market alert\"}}"}' \ --endpoint-url http://localhost:4566 ``` Broadcast pushes surface in the same retrospection API, keyed by `EndpointArn`. ### Per-protocol payloads on topic publish `MessageStructure="json"` resolves per subscriber, not just for mobile endpoints. Each subscription receives the value under its own protocol key — `sqs`, `lambda`, `http`, `https`, `email`, `email-json`, `sms`, or the push platform (`APNS`, `GCM`, …) for `application` — falling back to `default` when that key is absent. ```bash aws sns publish --topic-arn $TOPIC_ARN --message-structure json \ --message '{"default":"hello","sqs":"hi sqs","GCM":"{\"notification\":{\"body\":\"hi device\"}}"}' \ --endpoint-url http://localhost:4566 ``` The SQS subscriber receives `hi sqs`, the platform endpoint receives the `GCM` payload, and every other subscriber receives `hello`. As with any topic publish, the envelope must be a JSON object carrying `default`, validated before fan-out begins. ### Inspecting captured pushes ```bash # All captured pushes (newest first), or filtered by endpoint curl http://localhost:4566/_aws/sns/push-notifications curl "http://localhost:4566/_aws/sns/push-notifications?EndpointArn=$ENDPOINT_ARN" # Reset between tests curl -X DELETE http://localhost:4566/_aws/sns/push-notifications ``` ### Simulating expired tokens Two ways to make `Publish` fail with `EndpointDisabledException`: 1. **Explicit** — call `SetEndpointAttributes` with `Enabled=false`. Matches the real AWS flow after an async APNS/FCM failure. 2. **Sentinel** — create an endpoint whose token contains `EXPIRED` (case-insensitive). Floci marks it `Enabled=false` on creation, so the first publish fails. Lets you exercise the unhappy path with a single API call. ### Error codes | Action | Condition | Error code | HTTP | |---|---|---|---| | `CreatePlatformApplication` | Missing `Name` | `InvalidParameter` | 400 | | `CreatePlatformApplication` | Unsupported `Platform` (e.g. `WNS`, `ADM`) | `InvalidParameter` | 400 | | `CreatePlatformEndpoint` | Missing `Token` | `InvalidParameter` | 400 | | `CreatePlatformEndpoint` | Unknown `PlatformApplicationArn` | `NotFound` | 404 | | `CreatePlatformEndpoint` | Same `Token`, different `CustomUserData` or attrs | `InvalidParameter` | 400 | | `CreatePlatformEndpoint` | Platform app disabled | `PlatformApplicationDisabledException` | 400 | | `Publish` | Unknown endpoint ARN | `NotFound` | 404 | | `Publish` | `TargetArn` is a platform application ARN | `InvalidParameter` | 400 | | `Publish` | Endpoint `Enabled=false` | `EndpointDisabledException` | 400 | | `Publish` | Platform application `Enabled=false` | `PlatformApplicationDisabledException` | 400 | | `Publish` | `MessageStructure=json` missing `default` key | `InvalidParameter` | 400 | | `Publish` | `MessageStructure=json` message is not valid JSON | `InvalidParameter` | 400 | | `GetPlatformApplicationAttributes` | Unknown ARN | `NotFound` | 404 | | `GetEndpointAttributes` | Unknown ARN | `NotFound` | 404 | | `SetEndpointAttributes` | Unknown ARN | `NotFound` | 404 | `DeletePlatformApplication` and `DeleteEndpoint` are idempotent — they succeed silently if the resource does not exist, matching real SNS behavior.