How to Build an App Store Server Notifications SMS Alert
Why build an alert system?
I recently added an in-app purchase subscription to Retinelle. Once the subscription was live, I wanted real-time alerts whenever something happens — a new subscriber, a renewal, a cancellation, a refund. Apple’s App Store Server Notifications V2 push these events to a URL you control, but I didn’t want to constantly check a dashboard.
Here’s how I built a tiny server that receives Apple’s notifications and forwards them as text messages.
Tech stack
I wanted to keep things “simple”. A plain serverless function (like an AWS Lambda or Scaleway Function) would have been the simplest option. But I’m planning to build more small tools like this, so I set up a monorepo from the start. That led me to create a Bun monorepo, with its first app being a serverless container.
- TypeScript — because I have been using TypeScript quite a lot lately
- Bun — both as package manager and runtime. No need for a separate bundler or transpiler, and
Bun.serve()replaces Express - Scaleway Serverless Containers — scales to zero, so I only pay when Apple sends a notification. The free tier covers my usage entirely. And since I worked there, I know the team is solid
The entire project has zero npm dependencies — just Bun’s runtime and TypeScript types as dev dependencies.
Architecture
- Apple sends a signed JWS payload to our endpoint
- Bun.serve() decodes it, formats a human-readable message, and calls the SMS API
- Free Mobile delivers the SMS (free API for their subscribers in France). I might switch to another messaging app at some point, but here it was straightforward
- Scaleway Serverless Containers hosts the whole thing, scaling to zero when idle
Decoding Apple’s JWS payloads
Apple wraps everything in JWS (JSON Web Signature) tokens. A JWS is three base64url-encoded segments separated by dots: header.payload.signature. We only need the payload (middle segment).
For a server-to-server notification endpoint, signature verification is optional — the connection already comes over HTTPS from Apple’s servers. This keeps our code minimal:
|
|
The key gotcha here is base64url vs base64. Apple uses base64url encoding (RFC 4648 §5), which replaces + with - and / with _, and omits padding =. The atob() function expects standard base64, so we need to convert back and add padding before decoding.
Typing the notifications
Apple’s V2 notification spec is pretty large, and Apple documents the full set of notification types. For an SMS alert service, we only need a small subset:
|
|
Full file: Download “types.ts”.
The nested JWS pattern is the main thing worth noting: the top-level payload contains signedTransactionInfo, which is itself another JWS that you decode separately to get the transaction details.
Formatting human-readable SMS
An SMS has 160 characters. Not that we can’t send multiple for free, but let’s take that into consideration (we need to be concise). The formatter maps Apple’s SCREAMING_SNAKE_CASE types to readable labels and extracts the key info:
|
|
A typical SMS looks like:
|
|
Sending SMS via Free Mobile
Free Mobile is a French carrier that offers a free SMS API to its subscribers. One GET request, no SDK needed:
|
|
You activate the API in your Free Mobile subscriber dashboard under “My Options” → “Notifications by SMS”. You get a user ID (warning: it might be different from the user ID you use to connect!) and an API key.
The server
The server itself is minimal. The critical design decision: always return HTTP 200, even if processing fails.
|
|
Why always 200? Because if you return an error, Apple will retry the notification — potentially dozens of times. If your SMS API is temporarily down, you’d get a flood of duplicate messages once it recovers. Better to acknowledge receipt and log the error.
Here is the Dockerfile:
|
|
It stays intentionally minimal: no bun install, no multi-stage build, and no compilation step. It just copies the sources and runs Bun directly.
CI/CD with GitHub Actions
The GitHub Actions workflow is short enough to summarize:
- Check out the repo.
- Build and push the Docker image to Scaleway Container Registry.
- Update the Serverless Container to the new image, then deploy it.
Full file: Download “deploy-app-store-notifications.yml”.
The workflow triggers on pushes to main that touch files under apps/app-store-notifications/, or manually via workflow_dispatch. On the container side, I kept the resources tiny: 128 MB RAM, 100 mcpu, min-scale=0, max-scale=1.
Environment variables and secrets
There are two different buckets of configuration here, and mixing them up is the easiest way to lose time:
- Deploy-time credentials in GitHub.
- Runtime secrets in Scaleway.
In GitHub, I would store:
secrets.SCW_ACCESS_KEYandsecrets.SCW_SECRET_KEYfor the API keys.vars.SCW_PROJECT_ID,vars.SCW_ORGANIZATION_ID,vars.SCW_CONTAINER_ID, andvars.SCW_REGISTRY_NAMESPACEfor the non-secret identifiers.
In Scaleway Serverless Containers, I would store:
FREE_MOBILE_USER.FREE_MOBILE_PASS.PORToptionally, if you don’t want the default8080.
The important rule is simple: deployment credentials live in GitHub, runtime secrets live with the running service. For local development, you can export the same values in your shell or load them from a local .env file. Just don’t commit that file.
Configuring App Store Connect
- Go to App Store Connect → your app → App Information
- Scroll to App Store Server Notifications
- Set the Production Server URL to your endpoint:
https://your-container-url.scw.cloud/notifications - Optionally configure a Sandbox URL for testing
You can test your setup by using Apple’s App Store Server API to request a test notification programmatically.
Production hardening
Before using this in production, there are still two important hardening steps:
- Verify Apple’s JWS signature before sending the SMS. Right now this article only shows how to decode the payload, which is fine for a prototype but not enough for a public endpoint. The proper way is to validate the JWS signature and certificate chain against Apple’s requirements before trusting
signedPayload. - Deduplicate notifications by
notificationUUID. App Store Server Notifications are delivered at least once, so retries can happen. A simple approach would be to store each processednotificationUUIDin a local Redis instance with a TTL, and skip the SMS if the key already exists. Bun works well here since talking to Redis is straightforward.
Author Vinzius
LastMod 9 March, 2026