| 1 | --- |
| 2 | title: SNS |
| 3 | description: Create an SNS Topic, publish to it from a Lambda with the Publish binding, fan messages out to SQS queues, and consume notifications as a typed Stream. |
| 4 | --- |
| 5 | |
| 6 | **SNS** is AWS's pub/sub layer: publishers send a message to a |
| 7 | **topic** once, and SNS pushes a copy to every subscriber — |
| 8 | Lambda functions, SQS queues, HTTPS endpoints, email. In Alchemy |
| 9 | it's the `Topic` and `Subscription` resources, producer bindings |
| 10 | (`Publish`, `PublishBatch`, and the `Stream`-sink-shaped |
| 11 | `TopicSink`), and a consumer side: |
| 12 | `SNS.consumeTopicNotifications` subscribes a Lambda to the topic |
| 13 | as a typed `Stream` of notifications, creating the subscription |
| 14 | and invoke permission for you. |
| 15 | |
| 16 | ## Create a topic |
| 17 | |
| 18 | ```typescript |
| 19 | // src/topic.ts |
| 20 | import * as SNS from "alchemy/AWS/SNS"; |
| 21 | |
| 22 | export const Orders = SNS.Topic("Orders"); |
| 23 | ``` |
| 24 | |
| 25 | No name required — Alchemy generates one from the app, stage, |
| 26 | and logical ID. Raw SNS topic attributes (delivery policies, |
| 27 | KMS keys, display names) go through the `attributes` prop: |
| 28 | |
| 29 | ```typescript |
| 30 | export const Orders = SNS.Topic("Orders", { |
| 31 | attributes: { |
| 32 | DisplayName: "Order events", |
| 33 | }, |
| 34 | }); |
| 35 | ``` |
| 36 | |
| 37 | FIFO topics are a flag: |
| 38 | |
| 39 | ```typescript |
| 40 | export const OrdersFifo = SNS.Topic("OrdersFifo", { |
| 41 | fifo: true, |
| 42 | attributes: { |
| 43 | ContentBasedDeduplication: "true", |
| 44 | }, |
| 45 | }); |
| 46 | ``` |
| 47 | |
| 48 | Changing `fifo` or the topic name **replaces** the topic — |
| 49 | everything else updates in place. |
| 50 | |
| 51 | ## Publish from a Lambda |
| 52 | |
| 53 | Bind `SNS.Publish(topic)` in the function's Construction phase and call |
| 54 | it from runtime handlers. The binding quietly attaches |
| 55 | `sns:Publish` scoped to the topic ARN to the function's |
| 56 | execution role: |
| 57 | |
| 58 | ```typescript |
| 59 | // src/api.ts |
| 60 | import * as AWS from "alchemy/AWS"; |
| 61 | import * as SNS from "alchemy/AWS/SNS"; |
| 62 | import * as Effect from "effect/Effect"; |
| 63 | import * as HttpServerResponse from "effect/http/HttpServerResponse"; |
| 64 | import { Orders } from "./topic.ts"; |
| 65 | |
| 66 | export default class Api extends AWS.Lambda.Function<Api>()( |
| 67 | "Api", |
| 68 | { main: import.meta.url, functionUrl: true }, |
| 69 | Effect.gen(function* () { |
| 70 | const topic = yield* Orders; |
| 71 | const publish = yield* SNS.Publish(topic); |
| 72 | |
| 73 | return { |
| 74 | fetch: Effect.gen(function* () { |
| 75 | const response = yield* publish({ |
| 76 | Message: JSON.stringify({ orderId: "o_123" }), |
| 77 | Subject: "OrderCreated", |
| 78 | }); |
| 79 | return yield* HttpServerResponse.json({ |
| 80 | id: response.MessageId, |
| 81 | }); |
| 82 | }).pipe(Effect.orDie), |
| 83 | }; |
| 84 | }).pipe(Effect.provide(SNS.PublishHttp)), |
| 85 | ) {} |
| 86 | ``` |
| 87 | |
| 88 | `SNS.PublishHttp` is the runtime layer that backs the binding |
| 89 | with the SNS HTTP API — provide it on the function like any |
| 90 | other binding layer. |
| 91 | |
| 92 | ## Publish in batches |
| 93 | |
| 94 | `SNS.PublishBatch(topic)` ships up to 10 messages per API call |
| 95 | (runtime layer: `SNS.PublishBatchHttp`): |
| 96 | |
| 97 | ```typescript |
| 98 | const publishBatch = yield* SNS.PublishBatch(topic); |
| 99 | |
| 100 | // runtime |
| 101 | const response = yield* publishBatch({ |
| 102 | PublishBatchRequestEntries: messages.map((message, index) => ({ |
| 103 | Id: `${index}`, |
| 104 | Message: message, |
| 105 | })), |
| 106 | }); |
| 107 | // response.Successful / response.Failed |
| 108 | ``` |
| 109 | |
| 110 | ## Forward a stream with `TopicSink` |
| 111 | |
| 112 | If you have a `Stream<string>` of payloads — DynamoDB change |
| 113 | records, SQS messages, anything — `SNS.TopicSink(topic)` is the |
| 114 | sink-shaped producer: it batches items and publishes each chunk |
| 115 | through `PublishBatch`: |
| 116 | |
| 117 | ```typescript |
| 118 | const sink = yield* SNS.TopicSink(topic); |
| 119 | |
| 120 | // runtime |
| 121 | yield* Stream.fromIterable(payloads).pipe(Stream.run(sink)); |
| 122 | ``` |
| 123 | |
| 124 | `TopicSinkHttp` is implemented on top of the `PublishBatch` |
| 125 | binding, so provide it in two tiers with `Layer.provideMerge` — |
| 126 | the sink on top, the batch layer beneath: |
| 127 | |
| 128 | ```typescript |
| 129 | Effect.provide( |
| 130 | Layer.provideMerge(SNS.TopicSinkHttp, SNS.PublishBatchHttp), |
| 131 | ) |
| 132 | ``` |
| 133 | |
| 134 | ## Consume notifications on a Lambda |
| 135 | |
| 136 | `SNS.consumeTopicNotifications(topic, ...)` is the consumer-side |
| 137 | mirror of `SQS.consumeQueueMessages`: a typed `Stream` of SNS |
| 138 | notifications you compose with any operator. Behind the scenes |
| 139 | it creates the SNS subscription (protocol `lambda`, endpoint = |
| 140 | this function) and the `lambda:InvokeFunction` permission for |
| 141 | `sns.amazonaws.com`: |
| 142 | |
| 143 | ```typescript |
| 144 | // src/worker.ts |
| 145 | import * as AWS from "alchemy/AWS"; |
| 146 | import * as SNS from "alchemy/AWS/SNS"; |
| 147 | import * as Console from "effect/Console"; |
| 148 | import * as Effect from "effect/Effect"; |
| 149 | import * as Stream from "effect/Stream"; |
| 150 | import { Orders } from "./topic.ts"; |
| 151 | |
| 152 | export default class Worker extends AWS.Lambda.Function<Worker>()( |
| 153 | "OrdersWorker", |
| 154 | { main: import.meta.url }, |
| 155 | Effect.gen(function* () { |
| 156 | const topic = yield* Orders; |
| 157 | |
| 158 | yield* SNS.consumeTopicNotifications(topic, (stream) => |
| 159 | stream.pipe( |
| 160 | Stream.runForEach((notification) => |
| 161 | Console.log( |
| 162 | `${notification.Subject}: ${notification.Message}`, |
| 163 | ), |
| 164 | ), |
| 165 | ), |
| 166 | ); |
| 167 | |
| 168 | return {}; |
| 169 | }).pipe(Effect.provide(AWS.Lambda.TopicEventSource)), |
| 170 | ) {} |
| 171 | ``` |
| 172 | |
| 173 | Each notification carries the SNS message shape — `Message`, |
| 174 | `Subject`, `MessageId`, `TopicArn`, and friends. The |
| 175 | consumer-side machinery ships in `AWS.Lambda.TopicEventSource`; |
| 176 | provide it on the function. |
| 177 | |
| 178 | ## Filter what the consumer sees |
| 179 | |
| 180 | Pass subscription attributes as an optional middle argument — |
| 181 | `FilterPolicy` makes SNS drop non-matching messages before they |
| 182 | ever invoke your function: |
| 183 | |
| 184 | ```typescript |
| 185 | yield* SNS.consumeTopicNotifications( |
| 186 | topic, |
| 187 | { |
| 188 | attributes: { |
| 189 | FilterPolicy: JSON.stringify({ type: ["order.created"] }), |
| 190 | }, |
| 191 | }, |
| 192 | (stream) => |
| 193 | stream.pipe( |
| 194 | Stream.runForEach((notification) => |
| 195 | Console.log(notification.Message), |
| 196 | ), |
| 197 | ), |
| 198 | ); |
| 199 | ``` |
| 200 | |
| 201 | ## Fan out to SQS queues |
| 202 | |
| 203 | The classic SNS pattern is topic-to-queues fan-out: one publish, |
| 204 | one copy per queue, each queue drained by its own consumer at |
| 205 | its own pace. Subscriptions are their own resource, so wiring a |
| 206 | queue to a topic is one declaration: |
| 207 | |
| 208 | ```typescript |
| 209 | // alchemy.run.ts (inside the Stack's Effect.gen) |
| 210 | import * as SNS from "alchemy/AWS/SNS"; |
| 211 | import * as SQS from "alchemy/AWS/SQS"; |
| 212 | |
| 213 | const topic = yield* Orders; |
| 214 | |
| 215 | const emails = yield* SQS.Queue("EmailJobs"); |
| 216 | const analytics = yield* SQS.Queue("AnalyticsJobs"); |
| 217 | |
| 218 | yield* SNS.Subscription("EmailSubscription", { |
| 219 | topicArn: topic.topicArn, |
| 220 | protocol: "sqs", |
| 221 | endpoint: emails.queueArn, |
| 222 | }); |
| 223 | |
| 224 | yield* SNS.Subscription("AnalyticsSubscription", { |
| 225 | topicArn: topic.topicArn, |
| 226 | protocol: "sqs", |
| 227 | endpoint: analytics.queueArn, |
| 228 | }); |
| 229 | ``` |
| 230 | |
| 231 | One AWS-side requirement: the queue's access policy must let SNS |
| 232 | deliver to it. Grant it with the queue's `policy` prop, scoped |
| 233 | to the topic: |
| 234 | |
| 235 | ```typescript |
| 236 | const emails = yield* SQS.Queue("EmailJobs", { |
| 237 | policy: { |
| 238 | Version: "2012-10-17", |
| 239 | Statement: [ |
| 240 | { |
| 241 | Effect: "Allow", |
| 242 | Principal: { Service: "sns.amazonaws.com" }, |
| 243 | Action: "sqs:SendMessage", |
| 244 | Resource: "*", |
| 245 | Condition: { |
| 246 | ArnEquals: { "aws:SourceArn": topic.topicArn }, |
| 247 | }, |
| 248 | }, |
| 249 | ], |
| 250 | }, |
| 251 | }); |
| 252 | ``` |
| 253 | |
| 254 | By default SNS wraps each delivery in a JSON envelope |
| 255 | (`Message`, `Subject`, `TopicArn`, ...). Set |
| 256 | `RawMessageDelivery` on the subscription to receive the bare |
| 257 | message body instead: |
| 258 | |
| 259 | ```typescript |
| 260 | yield* SNS.Subscription("AnalyticsSubscription", { |
| 261 | topicArn: topic.topicArn, |
| 262 | protocol: "sqs", |
| 263 | endpoint: analytics.queueArn, |
| 264 | attributes: { |
| 265 | RawMessageDelivery: "true", |
| 266 | }, |
| 267 | }); |
| 268 | ``` |
| 269 | |
| 270 | Changing a subscription's `protocol`, `topicArn`, or `endpoint` |
| 271 | **replaces** the subscription; `attributes` update in place. |
| 272 | From here each queue is a normal SQS consumer — |
| 273 | [`SQS.consumeQueueMessages`](/aws/messaging/sqs) picks the messages up on |
| 274 | whatever Lambda you point at it. |
| 275 | |
| 276 | ## SNS vs EventBridge vs SQS |
| 277 | |
| 278 | All three move messages; they sit at different points: |
| 279 | |
| 280 | - **[SQS](/aws/messaging/sqs)** is one queue, one consumer group. |
| 281 | Point-to-point work distribution: producers enqueue, a pool of |
| 282 | workers drains. No fan-out — a message is consumed once. |
| 283 | - **SNS** is broadcast. Every subscriber gets its own copy, with |
| 284 | per-subscription filter policies. Reach for it when several |
| 285 | independent systems must react to the same event, or when you |
| 286 | want push delivery (Lambda, HTTPS, email) rather than polling. |
| 287 | Pair it with SQS (fan-out above) when each subscriber also |
| 288 | needs buffering and retries. |
| 289 | - **[EventBridge](/aws/messaging/eventbridge)** is a routed event bus: |
| 290 | rules match on the event *content* and route to targets, with |
| 291 | first-class support for AWS service events, schedules, and |
| 292 | third-party sources. Choose it for event-driven architectures |
| 293 | with many event types on one bus; choose SNS when you have one |
| 294 | well-known channel and want the simplest possible |
| 295 | publish/subscribe with high fan-out. |
| 296 | |
| 297 | ## Where next |
| 298 | |
| 299 | - [SQS](/aws/messaging/sqs) — the queue side of the fan-out pattern: |
| 300 | consumers, batching, dead-letter queues. |
| 301 | - [EventBridge](/aws/messaging/eventbridge) — content-based routing when |
| 302 | one topic per channel stops scaling. |
| 303 | - [Lambda](/aws/compute/lambda) — the runtime both the publisher and |
| 304 | consumer above are built on. |
| 305 | - [`Topic` reference](/providers/aws/sns/topic) and |
| 306 | [`Subscription` reference](/providers/aws/sns/subscription) — |
| 307 | every prop and attribute. |