1---
2title: SNS
3description: 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 —
8Lambda functions, SQS queues, HTTPS endpoints, email. In Alchemy
9it'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
13as a typed `Stream` of notifications, creating the subscription
14and invoke permission for you.
15
16## Create a topic
17
18```typescript
19// src/topic.ts
20import * as SNS from "alchemy/AWS/SNS";
21
22export const Orders = SNS.Topic("Orders");
23```
24
25No name required — Alchemy generates one from the app, stage,
26and logical ID. Raw SNS topic attributes (delivery policies,
27KMS keys, display names) go through the `attributes` prop:
28
29```typescript
30export const Orders = SNS.Topic("Orders", {
31 attributes: {
32 DisplayName: "Order events",
33 },
34});
35```
36
37FIFO topics are a flag:
38
39```typescript
40export const OrdersFifo = SNS.Topic("OrdersFifo", {
41 fifo: true,
42 attributes: {
43 ContentBasedDeduplication: "true",
44 },
45});
46```
47
48Changing `fifo` or the topic name **replaces** the topic —
49everything else updates in place.
50
51## Publish from a Lambda
52
53Bind `SNS.Publish(topic)` in the function's Construction phase and call
54it from runtime handlers. The binding quietly attaches
55`sns:Publish` scoped to the topic ARN to the function's
56execution role:
57
58```typescript
59// src/api.ts
60import * as AWS from "alchemy/AWS";
61import * as SNS from "alchemy/AWS/SNS";
62import * as Effect from "effect/Effect";
63import * as HttpServerResponse from "effect/http/HttpServerResponse";
64import { Orders } from "./topic.ts";
65
66export 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
89with the SNS HTTP API — provide it on the function like any
90other 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
98const publishBatch = yield* SNS.PublishBatch(topic);
99
100// runtime
101const 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
112If you have a `Stream<string>` of payloads — DynamoDB change
113records, SQS messages, anything — `SNS.TopicSink(topic)` is the
114sink-shaped producer: it batches items and publishes each chunk
115through `PublishBatch`:
116
117```typescript
118const sink = yield* SNS.TopicSink(topic);
119
120// runtime
121yield* Stream.fromIterable(payloads).pipe(Stream.run(sink));
122```
123
124`TopicSinkHttp` is implemented on top of the `PublishBatch`
125binding, so provide it in two tiers with `Layer.provideMerge` —
126the sink on top, the batch layer beneath:
127
128```typescript
129Effect.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
137mirror of `SQS.consumeQueueMessages`: a typed `Stream` of SNS
138notifications you compose with any operator. Behind the scenes
139it creates the SNS subscription (protocol `lambda`, endpoint =
140this function) and the `lambda:InvokeFunction` permission for
141`sns.amazonaws.com`:
142
143```typescript
144// src/worker.ts
145import * as AWS from "alchemy/AWS";
146import * as SNS from "alchemy/AWS/SNS";
147import * as Console from "effect/Console";
148import * as Effect from "effect/Effect";
149import * as Stream from "effect/Stream";
150import { Orders } from "./topic.ts";
151
152export 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
173Each notification carries the SNS message shape — `Message`,
174`Subject`, `MessageId`, `TopicArn`, and friends. The
175consumer-side machinery ships in `AWS.Lambda.TopicEventSource`;
176provide it on the function.
177
178## Filter what the consumer sees
179
180Pass subscription attributes as an optional middle argument —
181`FilterPolicy` makes SNS drop non-matching messages before they
182ever invoke your function:
183
184```typescript
185yield* 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
203The classic SNS pattern is topic-to-queues fan-out: one publish,
204one copy per queue, each queue drained by its own consumer at
205its own pace. Subscriptions are their own resource, so wiring a
206queue to a topic is one declaration:
207
208```typescript
209// alchemy.run.ts (inside the Stack's Effect.gen)
210import * as SNS from "alchemy/AWS/SNS";
211import * as SQS from "alchemy/AWS/SQS";
212
213const topic = yield* Orders;
214
215const emails = yield* SQS.Queue("EmailJobs");
216const analytics = yield* SQS.Queue("AnalyticsJobs");
217
218yield* SNS.Subscription("EmailSubscription", {
219 topicArn: topic.topicArn,
220 protocol: "sqs",
221 endpoint: emails.queueArn,
222});
223
224yield* SNS.Subscription("AnalyticsSubscription", {
225 topicArn: topic.topicArn,
226 protocol: "sqs",
227 endpoint: analytics.queueArn,
228});
229```
230
231One AWS-side requirement: the queue's access policy must let SNS
232deliver to it. Grant it with the queue's `policy` prop, scoped
233to the topic:
234
235```typescript
236const 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
254By default SNS wraps each delivery in a JSON envelope
255(`Message`, `Subject`, `TopicArn`, ...). Set
256`RawMessageDelivery` on the subscription to receive the bare
257message body instead:
258
259```typescript
260yield* SNS.Subscription("AnalyticsSubscription", {
261 topicArn: topic.topicArn,
262 protocol: "sqs",
263 endpoint: analytics.queueArn,
264 attributes: {
265 RawMessageDelivery: "true",
266 },
267});
268```
269
270Changing a subscription's `protocol`, `topicArn`, or `endpoint`
271**replaces** the subscription; `attributes` update in place.
272From here each queue is a normal SQS consumer —
273[`SQS.consumeQueueMessages`](/aws/messaging/sqs) picks the messages up on
274whatever Lambda you point at it.
275
276## SNS vs EventBridge vs SQS
277
278All 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.