{"id":16605924,"url":"https://github.com/frm/sns","last_synced_at":"2026-05-17T01:39:12.918Z","repository":{"id":141632963,"uuid":"582969747","full_name":"frm/sns","owner":"frm","description":"Plug-based wrapper around SNS pub/sub subscriptions used internally at Avenue.","archived":false,"fork":false,"pushed_at":"2023-11-21T19:50:43.000Z","size":56,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-03-16T22:54:07.107Z","etag":null,"topics":["aws","aws-sns","elixir","elixir-phoenix","plug","sns","sns-notifications"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/frm.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2022-12-28T11:32:56.000Z","updated_at":"2023-11-21T19:45:38.000Z","dependencies_parsed_at":"2023-03-13T10:30:36.447Z","dependency_job_id":null,"html_url":"https://github.com/frm/sns","commit_stats":null,"previous_names":["frm/sns"],"tags_count":9,"template":false,"template_full_name":null,"purl":"pkg:github/frm/sns","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/frm%2Fsns","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/frm%2Fsns/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/frm%2Fsns/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/frm%2Fsns/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/frm","download_url":"https://codeload.github.com/frm/sns/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/frm%2Fsns/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":266780344,"owners_count":23983039,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-07-23T02:00:09.312Z","response_time":66,"last_error":null,"robots_txt_status":null,"robots_txt_updated_at":null,"robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["aws","aws-sns","elixir","elixir-phoenix","plug","sns","sns-notifications"],"created_at":"2024-10-12T01:03:16.016Z","updated_at":"2026-05-17T01:39:07.896Z","avatar_url":"https://github.com/frm.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# SNS\n\n**⚠️ WARNING: This library is missing automatic tests and is not yet deemed\nproduction ready. Proceed at your own peril ⚠️**\n\nSNS is the internal wrapper for AWS SNS used in some of my projects.\n\nA few goodies:\n\n- Automatically subscribes to SNS via HTTP/HTTPS protocol on startup;\n- Abstracts away the nasty HTTP callback flow;\n- Allows configuring a local pub sub that mocks the AWS API so you can develop\n  locally and test with confidence without AWS configs or testing hacks;\n- Allows you to set up multiple different subscriptions in the same app, perfect\n  for umbrella apps.\n\n**Table of Contents**\n\n- [Installation](#installation)\n- [Quick Start](#quick-start)\n- [Usage](#usage)\n- [Development](#development)\n- [About](#about)\n\n## Installation\n\nAdd `:sns` as a dependency in your `mix.exs`. Until this package is production\nready, please use the latest tag from GitHub.\n\n```elixir\ndef deps do\n  [{:sns, git: \"git@github.com:frm/sns.git\", tag: \"0.4.0\"}]\nend\n```\n\n## Quick Start\n\ntl;dr:\n\n1. configure `:sns`;\n2. (optional) start the local server and the automatic subscription;\n3. define a handler;\n4. add the callback plug.\n\n```elixir\n# config/config.exs\nconfig :sns,\n  scheme: {:system, \"AWS_SNS_SCHEME\"},\n  host: {:system, \"AWS_SNS_HOST\"},\n  secret_access_key: {:system, \"AWS_SECRET_ACCESS_KEY\"},\n  access_key_id: {:system, \"AWS_ACCESS_KEY_ID\"},\n  # requires you to have this environment variable set up,\n  # even if you don't use the AWS API\n  region: {:system, \"AWS_REGION\"},\n  # if running locally instead of using AWS, use SNS.API.Mock instead\n  adapter: SNS.API.AWS\n\nconfig :my_app, MyApp.SNS.Subscription,\n  endpoint: {:system, \"SNS_ENDPOINT\"},\n  topic: {:system, \"SNS_TOPIC\"},\n  protocol: \"http\" # you'll want \"https\" in production\n\n\n# (optional) lib/application.ex\ndef MyApp.Application do\n  # ...\n\n  @impl true\n  def start(_type, _args) do\n    children = [\n      # optional, automatically subscribes on startup,\n      # remove it if you want to subscribe to a topic manually\n      {MyApp.SNS.Subscription,\n        Application.fetch_env!(:my_app, MyApp.SNS.Subscription},\n      # only add this if you're running locally, instead of using AWS,\n      # for development purposes only.\n      SNS.Local.PubSub\n    ]\n\n    # ...\n  end\nend\n\n# (optional) lib/sns/subscription.ex\ndefmodule MyApp.SNS.Subscription do\n  # optional, only do this if you want to automatically subscribe to the given\n  # topic on startup\n  use SNS.Subscription\nend\n\n\n# lib/my_app/sns/handler.ex\ndefmodule MyApp.SNS.Handler do\n  # handle new messages here\n  def handle(message) do\n    IO.puts(\"just received #{inspect(message)}\"\n  end\nend\n\n\n# lib/my_app_web/router.ex\ndefmodule MyAppWeb.Router do\n  use MyAppWeb, :router\n\n  # ...\n\n  forward \"/sns/callback\", SNS.Router, handler: MyApp.SNS.Handler\n\n  # ...\nend\n```\n\nIf you want a detailed explanation on what all of this is, see below.\n\n## Usage\n\nSNS was built to have two distinct modes that act similarly\n\n- when in development or test, you should be able to make use of everything that AWS\n  provides you without actually having to configure it.\n- when in production, you should hit AWS but allow `:sns` to handle subscription\n  confirmation and all those things developers don't want to handle.\n\nThere is a base set of config params that are shared across both modes, which\nyou'll always need:\n\n```elixir\n# config/config.exs\nconfig :sns,\n  scheme: {:system, \"AWS_SNS_SCHEME\"},\n  host: {:system, \"AWS_SNS_HOST\"},\n  secret_access_key: {:system, \"AWS_SECRET_ACCESS_KEY\"},\n  access_key_id: {:system, \"AWS_ACCESS_KEY_ID\"},\n  # requires you to have this environment variable set up,\n  # even if you don't use the AWS API\n  region: {:system, \"AWS_REGION\"}\n```\n\nThe way `:sns` works is by defining a plug that receives HTTP subscription\nevents from AWS SNS. In your `router.ex`\nfile:\n\n```elixir\ndefmodule MyAppWeb.Router do\n  use MyAppWeb, :router\n\n  # ...\n\n  forward \"/sns/callback\", SNS.Router, handler: MyApp.SNS.Handler\n\n  # ...\nend\n```\n\nTo start handling events, you just need to define your own handler. It should\nimplement a `handle/1` function that receives a binary string.\n\n```elixir\ndefmodule MyApp.SNS.Handler do\n  def handle(message) do\n    IO.puts(\"just received #{inspect(message)}\"\n  end\nend\n```\n\nThe one final thing that remains is actually subscribing to a topic. You can\neither call `SNS.API.subscribe/4` or you can make use of the\n`SNS.Subscription` module. This is a utility task that you can add to your\n`application.ex` so that it automatically subscribes to the configured topics.\n\nAs an example:\n\n```elixir\n# lib/application.ex\ndef MyApp.Application do\n  # ...\n\n  @impl true\n  def start(_type, _args) do\n    children = [\n      # automatically subscribes on startup\n      {MyApp.SNS.Subscription,\n        Application.fetch_env!(:my_app, MyApp.SNS.Subscription},\n    ]\n\n    # ...\n  end\nend\n\n# config/config.exs\n# you can hardcode these values or use them as env variables\nconfig :my_app, MyApp.SNS.Subscription,\n  endpoint: {:system, \"SNS_ENDPOINT\"},\n  topic: {:system, \"SNS_TOPIC\"},\n  protocol: \"http\" # make sure you use \"https\" in prod\n\n# lib/my_app/sns/subscription.ex\ndefmodule MyApp.SNS.Subscription do\n  use SNS.Subscription\nend\n```\n\nDepending on what you want to do, the next stages differ.\n\n### I want to use it in development\n\nPerfect, so you'll have to do two things:\n\n1. add `SNS.Local.PubSub` to your `application.ex` file, just like in the\n   `SNS.Subscription` example above. This is a pub sub server that mimics\n   AWS behaviour;\n2. configure `:sns` to use the mock API adapter by adding `adapter:\nSNS.API.Mock` to the `config/config.exs` file as per the example in\n   [Quick Start](#quick-start).\n\n### I want to use it in production\n\nScrumptious, so to do that, you'll just have make sure you have the right environment\nvariables set and now just configure `:sns` to use the AWS API adapter by\nadding `adapter: SNS.API.AWS` to the `config/config.exs` file, as per the\nexample in [Quick Start](#quick-start).\n\n### Umbrella apps\n\n`:sns` was written to work well with umbrella apps. You can define a handler for\neach separate router:\n\n```elixir\n#\n# Configuring AppOne\n#\n\n# apps/app_one/lib/app_one/sns/handler.ex\ndefmodule AppOne.SNS.Handler do\n  def handle(message) do\n    # handle things in any way you like\n  end\nend\n\n# apps/app_one/lib/app_one_web/router.ex\ndefmodule AppOneWeb.Router do\n  use MyAppWeb, :router\n\n  # ...\n\n  forward \"/sns/callback\", SNS.Router, handler: MyApp.SNS.Handler\n\n  # ...\nend\n\n# (optional) apps/app_one/lib/app_one/application.ex\ndef AppOne.Application do\n  # ...\n\n  @impl true\n  def start(_type, _args) do\n    children = [\n      # optional, automatically subscribes on startup,\n      # remove it if you want to subscribe to a topic manually\n      {\n        AppOne.SNS.Subscription,\n        Application.fetch_env!(:app_one, AppOne.SNS.Subscription)\n      },\n      # only add this if you're running locally, instead of using AWS,\n      # for development purposes only.\n      SNS.Local.PubSub\n    ]\n\n    # ...\n  end\nend\n\n# (optional) apps/app_one/lib/app_one/sns/subscription.ex\ndefmodule AppOne.SNS.Subscription do\n  # optional, only do this if you want to automatically subscribe to the given\n  # topic on startup\n  use SNS.Subscription\nend\n\n#\n# Configuring AppTwo\n#\n\n# apps/app_two/lib/app_two/sns/handler.ex\ndefmodule AppTwo.SNS.Handler do\n  def handle(message) do\n    # handle things in any way you like\n  end\nend\n\n# apps/app_two/lib/app_two_web/router.ex\ndefmodule AppTwoWeb.Router do\n  use MyAppWeb, :router\n\n  # ...\n\n  forward \"/sns/callback\", SNS.Router, handler: MyApp.SNS.Handler\n\n  # ...\nend\n\n# (optional) apps/app_two/lib/app_two/application.ex\ndef AppTwo.Application do\n  # ...\n\n  @impl true\n  def start(_type, _args) do\n    children = [\n      # optional, automatically subscribes on startup,\n      # remove it if you want to subscribe to a topic manually\n      {\n        AppTwo.SNS.Subscription,\n        Application.fetch_env!(:app_two, AppTwo.SNS.Subscription)\n      },\n      # only add this if you're running locally, instead of using AWS,\n      # for development purposes only.\n      SNS.Local.PubSub\n    ]\n\n    # ...\n  end\nend\n\n# (optional) apps/app_two/lib/app_two/sns/subscription.ex\ndefmodule AppTwo.SNS.Subscription do\n  # optional, only do this if you want to automatically subscribe to the given\n  # topic on startup\n  use SNS.Subscription\nend\n```\n\nIn the example above, we configure two umbrella apps. In this case, each of them\nwould have a dedicated endpoint to handle SNS callbacks and they could even each\nhave a different subscription topic.\n\n### Production security concerns\n\nSince this library relies on adding a publicly accessible endpoint to confirm\nsubscriptions and receive events, we need to protect against an outside party\njamming it. To do that, you can use route obfuscation.\n\nAs an example:\n\n```elixir\ndefmodule MyAppWeb.Router do\n  use MyAppWeb, :router\n\n  # ...\n\n  post \"/sns/:api_key/callback\",\n    SNS.Router,\n    handler: MyApp.SNS.Handler,\n    verify_with: {\"api_key\", {:system, \"INTERNAL_SNS_API_KEY\"}}\n\n  # ...\nend\n```\n\nUsing a route param with the `:verify_with` option will cause the plug to\nvalidate the path is the same as your internal API Key to prevent malicious\nthird-parties. While not a complete solution, this helps easily obfuscate the\nendpoint being used.\n\nThe `:verify_with` receives a tuple with the first element being the route\nparam to check against and the second element either the value of the API key or\na `{:system, env_var_name}` tuple. This will check the environment in runtime to\navoid compile-time env var issues.\n\n## Development\n\nIf you want to help develop, please feel free to open issues, pull requests, the\nworks. All contributions welcome but they **must** follow the [Code of Conduct][coc].\n\nThere's a development utility included, that runs a [`cowboy`][cowboy] server to\nhandle the callbacks:\n\n```elixir\nSNS.Local.Server.start() # starts the server\n```\n\n## About\n\n\u0026copy; 2022 Fernando Mendes\n\nIt is open-source, made available for free, and is subject to the terms in its\n[license].\n\n[license]: ./LICENSE\n[coc]: ./CODE_OF_CONDUCT.md\n[cowboy]: https://github.com/ninenines/cowboy\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffrm%2Fsns","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffrm%2Fsns","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffrm%2Fsns/lists"}