SNS

Lightweight, dependency-free fake of AWS SNS that speaks the real SNS AWS Query wire protocol (form-encoded requests, XML responses, API version 2010-03-31), so application code using @aws-sdk/client-sns can run against it with zero cost and zero side effects.

KeyValue
Port4569
ProtocolAWS Query (application/x-www-form-urlencoded request, XML response) over HTTP
API version2010-03-31
Compatible client@aws-sdk/client-sns (v3)
Size~90 KB
Startup< 100ms
StateIn-memory, ephemeral, resettable

Quick Start

Start the server:

import { SnsServer } from "./services/sns/src/server.js";

const server = new SnsServer(4569);
await server.start();
// ... use it ...
await server.stop();

Connect with the real AWS SDK client:

import {
  SNSClient,
  CreateTopicCommand,
  SubscribeCommand,
  PublishCommand,
} from "@aws-sdk/client-sns";

const sns = new SNSClient({
  region: "us-east-1",
  endpoint: "http://127.0.0.1:4569",
  credentials: { accessKeyId: "parlel", secretAccessKey: "parlel" },
});

// Create a topic
const { TopicArn } = await sns.send(new CreateTopicCommand({ Name: "events" }));

// Subscribe an SQS queue (auto-confirmed)
await sns.send(
  new SubscribeCommand({
    TopicArn,
    Protocol: "sqs",
    Endpoint: "arn:aws:sqs:us-east-1:000000000000:events-queue",
    ReturnSubscriptionArn: true,
  }),
);

// Publish a message
const { MessageId } = await sns.send(
  new PublishCommand({ TopicArn, Message: "hello world" }),
);
console.log(MessageId);

ARNs

The default region is us-east-1 and the default account id is 000000000000 (both configurable via the constructor: new SnsServer(port, { region, accountId, host })).

Internal endpoints (not part of SNS)

EndpointMethodPurpose
/_parlel/healthGETReturns { status, service, topics, subscriptions }
/_parlel/resetPOSTClears all in-memory state

State can also be reset in-process with server.reset(). Published messages are captured in server.published for test assertions.

Implemented operations

All 42 operations exposed by @aws-sdk/client-sns v3 are implemented.

Topics

Subscriptions

Publishing

Permissions

Tags

Data protection policy

SMS

SMS sandbox

Platform applications & endpoints (mobile push)

Surface coverage

This emulator faithfully replicates the API surface most application code and agents exercise. Anything below the supported lines is either an intentional design choice for a fast, zero-cost local emulator (✓ By design) or a candidate for a future release (⟳ Roadmap) — never a silent inaccuracy.

Legend: ✅ fully supported · ◐ accepted (stored, not strictly enforced) · ✓ by design · ⟳ on the roadmap.

FeatureSupportedNotes
Topic lifecycle & attributesFull
FIFO topicsName-suffix detection, MessageGroupId/dedup validation, SequenceNumber returned
Standard & FIFO publishCaptured in server.published
PublishBatchUp to 10 entries, partial failures
Subscriptions (all protocols)Protocol validation enforced
Confirmation flowPending tokens are generated and confirmable
Filter policies / raw delivery✅ (stored)Stored as subscription attributes; messages are not fanned out to endpoints
TagsTopic resources only
Data protection policyStored verbatim, not enforced
SMS attributes & opt-outStored in-memory
SMS sandboxOTP defaults to 123456
Mobile push (platform apps/endpoints)Lifecycle + attributes
Actual message delivery / fan-out to SQS, HTTP, email, Lambda✓ By design — Captured in-memory for inspection — no real messages sent
Real signature verification (SignatureVersion 1/2)✓ By design — Structurally faithful tokens; cryptographic verification is skipped for local use
KMS encryption✓ By design — Plain in-memory storage — transport/at-rest crypto is unnecessary locally
Cross-account / IAM policy enforcement⟳ Roadmap

Error codes & shapes

Errors are returned as non-2xx HTTP responses with an XML body in the AWS Query error envelope:

<?xml version="1.0"?>
<ErrorResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
  <Error>
    <Type>Sender</Type>
    <Code>InvalidParameter</Code>
    <Message>Invalid parameter: Topic Name</Message>
  </Error>
  <RequestId>...</RequestId>
</ErrorResponse>

<Type> is Sender for client (4xx) faults and Receiver for server (5xx) faults.

CodeHTTPWhen
InvalidParameter400Missing/invalid parameter (bad topic name, empty message, bad protocol, immutable attribute, etc.)
InvalidAction400Unknown Action
NotFound404Topic / subscription / platform application / endpoint does not exist
ResourceNotFound404Tag resource or sandbox number not found
AuthorizationError403Authorization failures
EmptyBatchRequest400PublishBatch with no entries
TooManyEntriesInBatchRequest400PublishBatch with > 10 entries
BatchEntryIdsNotDistinct400Duplicate Id in a batch
VerificationException400Wrong OTP on VerifySMSSandboxPhoneNumber
InternalError500Unexpected server error

Successful responses use the AWS Query success envelope, e.g.:

<?xml version="1.0"?>
<CreateTopicResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
  <CreateTopicResult>
    <TopicArn>arn:aws:sns:us-east-1:000000000000:events</TopicArn>
  </CreateTopicResult>
  <ResponseMetadata>
    <RequestId>...</RequestId>
  </ResponseMetadata>
</CreateTopicResponse>

Running the tests

npx vitest run tests/sns.test.ts

The test suite starts the server on port 14569, exercises every implemented operation (happy paths plus key edge cases), asserts the real SDK-parsed responses, and tears the server down in afterAll.

<!-- parlel:testenv:start -->

Configuration — test.env

Copy these into your test.env (used by the bridge sidecar flow). Tokens are Parlel's seeded test credentials — any non-empty value is accepted by the emulator, so you rarely need to change them. Swap in real credentials only when pointing at the live service in prod.env.

AWS_ACCESS_KEY_ID=parlel
AWS_SECRET_ACCESS_KEY=parlel
AWS_REGION=us-east-1
AWS_ENDPOINT_URL_SNS=http://parlel-bridge:4569
AWS_ENDPOINT_URL=http://parlel-bridge:4569
<!-- parlel:testenv:end -->