GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — GitHub - AmazonAppDev/ring-appstore-watchtower-sample

BranchesTags
Open more actions menu

Latest commit

 

History

8 Commits
8 Commits

Folders and files

NameName
Last commit message
Last commit date
.github
.github
 
 
backend
backend
 
 
docs
docs
 
 
frontend
frontend
 
 
infra
infra
 
 
.env.example
.env.example
 
 
.gitignore
.gitignore
 
 
.repolinterrc.json
.repolinterrc.json
 
 
.ring-readiness.json
.ring-readiness.json
 
 
CHANGELOG.md
CHANGELOG.md
 
 
CODE_OF_CONDUCT.md
CODE_OF_CONDUCT.md
 
 
CONTRIBUTING.md
CONTRIBUTING.md
 
 
LICENSE
LICENSE
 
 
PRIVACY.md
PRIVACY.md
 
 
README.md
README.md
 
 
SECURITY.md
SECURITY.md
 
 
SUPPORT.md
SUPPORT.md
 
 
THIRD-PARTY-NOTICES.md
THIRD-PARTY-NOTICES.md
 
 
package-lock.json
package-lock.json
 
 
package.json
package.json
 
 

Repository files navigation

Watchtower — a Ring Appstore Sample App

Watchtower is a minimal, open-source reference app for the Ring Appstore (Amazon Vision API). It's built for the enterprise and SMB use case of watching many Ring cameras live on one screen: a lobby monitor, a front-desk display, or a small security desk. It links a Ring user's account to your app, receives Ring webhooks, and shows a live grid of every shared camera in a single page.

Watchtower live camera grid (illustration: camera views are drawn, not real footage)

Illustration of the dashboard layout. Camera views are drawn placeholders, not real footage.

It is intentionally small so you can read it end to end and adapt it to your own deployment. The whole thing deploys to AWS with one CDK command: API Gateway → Lambda → DynamoDB.

┌──────────────┐      HTTPS       ┌───────────────┐     ┌────────────┐
│  Browser SPA │ ───────────────▶ │  API Gateway  │ ──▶ │   Lambda   │
│ (camera grid)│ ◀─ SDP answer ── │  (HTTP API)   │     │  (Express) │
└──────┬───────┘                  └───────────────┘     └─────┬──────┘
       │                                                      │
       │ WebRTC/WHEP media (peer ⇄ Ring)                      │ Bearer token
       ▼                                                      ▼
┌──────────────────────┐                           ┌────────────────────┐
│  Ring Amazon Vision   │ ◀── token refresh ─────── │   DynamoDB (one     │
│  api.amazonvision.com │ ─── webhooks ───────────▶ │   single table)     │
└──────────────────────┘                           └────────────────────┘

Watchtower AWS architecture

Who it's for (developer story)

As a developer building on the Ring Appstore (Amazon Vision API), I want a small, complete reference app that links a Ring account, receives webhooks, and streams every shared camera live on one screen, so that I can see exactly how the account-linking, webhook, and WHEP streaming pieces fit together and adapt them to my own multi-camera monitoring product.

The target use case is an enterprise/SMB wall of cameras (a lobby monitor, a front-desk display, or a small security desk) rather than a single-camera consumer view.

What this sample proves

  • A developer can deploy the whole stack to AWS with a single command (npm run deploy).
  • The deploy prints the four URLs needed for the Ring Developer Portal Account linking form.
  • A Ring user links their account passwordlessly (no form, no password), driven entirely by Ring's redirect, with the email read from the Ring User Data API.
  • After linking, the user sees a confirmation screen listing their connected devices, then a live grid where every shared camera (and every Elite 360 lens) plays as a low-latency WHEP stream.
  • A returning user signs back in with a 6-digit email code (Amazon SES), still passwordless.
  • Incoming Ring webhooks are signature-verified over the raw request bytes and acknowledged within the 5-second window.
  • Online/offline state is conveyed with both a text label and a color (never color alone), and offline cameras are shown dimmed with an explanation rather than hidden.
  • The user can delete their Watchtower account and data from within the app.

What it does

  • Passwordless account linking (one-way / Ring-driven): Ring POSTs an auth code to your Token Exchange URL, then redirects the user to your Account Link URL with an HMAC nonce. The app exchanges the code, reads the user's email from the Ring User Data API (GET /v1/users/me), matches the nonce, auto-provisions the user (no password), and finalizes the link with Ring (POST + PATCH /v1/accounts/me/app-integrations). The user never types anything to link.
  • Passwordless dashboard sign-in (email code via SES): after signing out, a returning user enters their email and receives a 6-digit code by email (Amazon SES). Entering the code signs them back in.
  • Webhooks: a signed POST /webhooks/ring endpoint that verifies the X-Signature HMAC, de-dupes deliveries in DynamoDB, answers within 5 seconds, and deletes the customer's data when they remove the app (app_integration_removed).
  • Live camera grid: lists the shared devices (GET /v1/devices) and plays each one as a low-latency WebRTC stream using Ring's WHEP endpoint, proxied through the Lambda. Each tile has a fullscreen button, and a top-right dropdown shows the customer's subscription/trial status (Subscriptions Query API) and lists every camera (and every lens) with its product image from Ring and an online/offline indicator, plus a "How this works" tutorial replay, Sign out, and account deletion.
  • Subscription & trial status: GET /api/subscription calls the Ring Subscriptions Query API (GET /v1/accounts/me/subscriptions) and summarizes active paid plans and trials in the dropdown.
  • Elite 360 multi-lens: cameras that expose multiple lenses (capabilities.components.items) are rendered as one live tile per lens, each streamed with its own WHEP session targeting the lens via a component_id query parameter.

What it is NOT

This is a sample. It skips: RTSP, server-side transcoding, analytics, clip downloads, and 360-panorama stitching (Elite 360 lenses are shown as separate live tiles, not stitched into one panorama). For auth it uses a signed, httpOnly session cookie (set at login, verified on every API route) — a minimal but real session mechanism, not a production identity system. For production you'd likely add CSRF tokens, token rotation, and a revocation/session store (see Going to production). Add what you need.

Project layout

Path What's inside
backend/ The Lambda: an Express app wrapped by serverless-express.
frontend/ The single-page camera grid (Vite + vanilla JS, no framework).
infra/ AWS CDK stack: HTTP API Gateway, Lambda, one DynamoDB table.

Prerequisites

  • Node.js 22+ (the Lambda runs on Node.js 22)
  • Works on macOS, Linux, and Windows. On Windows, run the commands below in PowerShell (or Git Bash); where a shell command differs by platform, both variants are shown.
  • An AWS account and credentials (aws configure or AWS_PROFILE)
  • AWS CDK v2. You don't need to install it: the project includes the CDK CLI, and step 5 runs it for you.
  • Ring Developer credentials: Client ID, Client Secret, HMAC Signing Key (from the Ring Developer Console; see developer.ring.com to get started)
  • An Amazon SES verified identity (an email address or domain) for sending sign-in codes. Set it as SES_FROM_EMAIL. For the sample, verifying a single address is enough, just note that while your SES account is in the sandbox, codes only deliver to verified recipient addresses. See Going to production before relying on this for real users.

The steps below go in order: create the app on the Ring Developer Portal first (you need its credentials to configure the backend), deploy to AWS, then wire the deployed URLs back into the portal and link a camera. You need a deployed endpoint to test end to end, because the Ring Appstore drives account linking over public HTTPS and will not call back to localhost.

Local UI iteration. After you've deployed and linked once, npm run dev runs the backend on localhost:3000 and the Vite dev server on localhost:5173 (proxying /api to the backend), using the same .env. It's for working on the dashboard: account linking and webhooks still need the deployed URLs.

1. Create a new app on the Ring Developer Portal

Sign in to the Ring Developer Console and create a new app for the Ring Appstore (Amazon Vision API). Give it a name and the basic details; you'll fill in the account-linking URLs later (step 6), after you have a deployed endpoint.

Ring Developer Portal: create a new app

2. Capture credentials

From your new app, copy the three secrets the backend needs:

  • Client ID (RING_CLIENT_ID)
  • Client Secret (RING_CLIENT_SECRET)
  • HMAC Signing Key (RING_HMAC_SIGNING_KEY)

Keep these somewhere safe; you'll paste them into .env in the next step. Treat the secret and the signing key like passwords; never commit them.

Ring Developer Portal: app credentials

3. Configure

First, clone this project and change into the project directory. The commands in this and the following steps are all run from the project root:

git clone https://github.com/AmazonAppDev/ring-appstore-watchtower-sample.git
cd ring-appstore-watchtower-sample

Then create your .env from the example and fill in the credentials from step 2:

# macOS / Linux
cp .env.example .env
# Windows (PowerShell)
Copy-Item .env.example .env

Then edit .env and fill in RING_CLIENT_ID, RING_CLIENT_SECRET, RING_HMAC_SIGNING_KEY, and SES_FROM_EMAIL (a verified SES sender address). The deploy stops with a clear message if any of them is still the example value.

4. Install

From the project directory you cloned in step 3, install all workspace dependencies:

npm install          # installs root + backend + frontend + infra workspaces

5. Deploy to AWS

If you've never used the AWS CDK in this AWS account and region, bootstrap it once (this creates the resources CDK uses to deploy, such as an S3 bucket for the Lambda code):

npm run bootstrap    # once per AWS account and region

Then deploy:

npm run deploy

CDK provisions the table, the Lambda (with your Ring secrets as environment variables), and an HTTP API Gateway, then prints four stack outputs, one per field on the Ring Developer Portal Account linking form (you'll use these in the next step):

Ring Developer Portal field CDK output Value
Account Link URL AccountLinkUrl https://<api-url>/ring/link
App Homepage URL AppHomepageUrl https://<api-url>
Token Exchange URL TokenExchangeUrl https://<api-url>/oauth/token
Webhook URL WebhookUrl https://<api-url>/webhooks/ring

The frontend is served by the same Lambda (static files bundled into the deployment) so the whole app lives behind one URL. The dashboard (and the App Homepage URL) is https://<api-url>/.

6. Add the deployed endpoints to Account linking

Back in the Developer Portal, open your app's Account linking page and paste each stack output into the matching field (the fields appear in the same order as the table above), then Save:

Ring Developer Portal: Account linking configuration

7. Add your Ring user as a test user

While the app is in development it isn't public, so add the Ring account you'll test with as a test user for your app. Use the email of the Ring account whose cameras you want to view.

Ring Developer Portal: add a test user

8. Initiate account linking from the Ring Appstore

As that test user, open the Ring Appstore (in the Ring app), find your app, and start the install / connect flow. Pick the cameras you want to share. Ring then runs the passwordless link (token exchange → redirect to your Account Link URL), and your app auto-provisions the account.

Ring Appstore: install and link the app

9. Confirm the dashboard shows your camera feed

After linking, you land on the dashboard at https://<api-url>/. Each shared camera appears as a live tile (Elite 360 cameras show one tile per lens). You now have your Ring cameras streaming on a single screen.

Watchtower dashboard layout (illustration: camera views are drawn, not real footage)

Tear it down

To remove all AWS resources this stack created, including the DynamoDB table (with every linked account's tokens) and the Lambda's logs:

npm run destroy

Then remove the app's URLs from the Ring Developer Portal, or delete the app there. The CDK bootstrap resources from step 5 stay in your account; they're shared by any CDK project.

Troubleshooting

Symptom Likely cause Fix
npm run deploy fails with This stack uses assets… requires bootstrapping The target AWS account/region hasn't been CDK-bootstrapped. Run npm run bootstrap once (step 5), then redeploy.
Deploy throws ... is still the example value from .env.example .env still has a value from .env.example. Replace it as described in step 3, then redeploy.
Deploy throws Missing RING_CLIENT_ID (or another var) .env isn't filled in. Create .env from the example (cp .env.example .env, or Copy-Item .env.example .env on Windows) and set RING_CLIENT_ID, RING_CLIENT_SECRET, RING_HMAC_SIGNING_KEY, SES_FROM_EMAIL.
Sign-in code email never arrives SES is in the sandbox, which only delivers to verified recipients, or SES_FROM_EMAIL isn't verified. Verify both the sender and the recipient in the SES console, or request SES production access. See Going to production.
Account linking fails / "This link is missing information" The Account Link / Token Exchange URLs in the Developer Portal don't match the deployed endpoints, or the HMAC signing key differs. Re-copy the four stack outputs into the portal (step 6) and confirm RING_HMAC_SIGNING_KEY matches your app's signing key.
Camera tiles say "Live view isn't available right now" or stay connecting The camera is offline, the browser blocked WebRTC, or Ring rate-limited the WHEP handshake. Check the device is online in the Ring app, try the tile's Reconnect button, and confirm the browser allows WebRTC (no blocking extension/VPN).
Webhook calls return 401 The X-Signature HMAC didn't verify (wrong signing key, or a proxy altered the raw body). Ensure RING_HMAC_SIGNING_KEY matches the portal and that nothing re-encodes the request body before it reaches the Lambda.
Dashboard loads but shows "No Ring devices connected yet" No cameras were shared during linking, or the session user has no claimed token. Re-run the link flow from the Ring Appstore and pick the cameras you want to share.
"Unable to validate URL" when saving the Token Exchange / Webhook URL in the portal The portal probes each URL (a GET) to confirm it resolves before saving, and the URL isn't reachable. Confirm you pasted the exact deployed URL (from the npm run deploy outputs) and that the stack is deployed. These endpoints answer the GET probe with 200; a 404 usually means a typo or a stale URL from a previous deploy.
npm audit reports a moderate minimatch advisory It's bundled inside aws-cdk-lib, which is used only to deploy (see the note at the end of this README). Safe to ignore for this sample; it never reaches the deployed Lambda or the browser. Update aws-cdk-lib when AWS ships a fix.
Dashboard says "Reconnect Watchtower to Ring" Watchtower was removed in Ring My Apps, or the link expired after about 30 days without use (this sample refreshes tokens only when the dashboard is open). Add Watchtower again from the Ring Appstore in the Ring app.

For anything deeper, check the Lambda's CloudWatch Logs (log group /watchtower-sample/api); the backend logs linking, webhook, and streaming errors there. Logs are kept for 30 days.

How account linking works (passwordless, one-way)

  1. User installs your app in the Ring Appstore and picks devices.
  2. Ring POSTs an authorization code to /oauth/token (form-encoded). The backend exchanges it at https://oauth.ring.com/oauth/token, calls the Ring User Data API GET /v1/users/me for the account_id and the user's email, and stores the token as unclaimed (keyed by account_id, 15-minute TTL) with the email cached on it.
  3. Ring redirects the user to /ring/link?nonce=…&time=…. The backend checks the timestamp is fresh (< 600s) and forwards the user to the SPA, which auto-finalizes with no form to fill in.
  4. The SPA calls POST /api/link with just the nonce and time (no email, no password). This endpoint checks the time freshness window again (Ring requires it everywhere nonce and time are used), and ignores unclaimed tokens past their TTL, since DynamoDB deletes expired items lazily.
  5. The backend recomputes HMAC-SHA256(key, "<time>:<account_id>") for each unclaimed token and constant-time compares it to the nonce to find the right account.
  6. It reads the Ring-provided email, auto-provisions the user (emailVerified: true, no password), calls POST /v1/accounts/me/app-integrations (nonce verify) then PATCH … {status:"completed"} to finalize, promotes the token to claimed (keyed by your user id), and the user lands on the grid. The account_identifier sent to Ring is the masked email (u***[email protected]), per Ring's App Integrations best practices; Ring identifies the account from the Bearer token.

If the token exchange (step 2) fails, /oauth/token answers 502, never a misleading 200.

How dashboard sign-in works (email code via SES)

Account linking establishes the user. After they sign out, they sign back in with an email code: no password, no second Ring round-trip:

  1. On the sign-in screen the user enters their email; the SPA calls POST /api/auth/send-code.
  2. The backend looks up the linked user, generates a 6-digit code, stores it in DynamoDB (PASSCODE#<email> / CODE, 10-minute expiry + TTL cleanup, max 5 attempts), and e-mails it with Amazon SES from SES_FROM_EMAIL. A new code is sent at most once a minute per email. (Unknown emails, and requests inside the cooldown, get the same sent response, so the endpoint doesn't reveal which emails have accounts.)
  3. The user enters the code; the SPA calls POST /api/auth/verify-code. The backend constant-time compares it, deletes the single-use code, and sets the signed, httpOnly session cookie (the user id is never returned to the browser), landing the user on the grid.

How live streaming works (WHEP)

The browser creates a recvonly WebRTC offer and sends the SDP to POST /api/devices/:id/stream. The Lambda forwards it (with the user's bearer token) to Ring's WHEP endpoint POST /v1/devices/:id/media/streaming/whep/sessions, and returns the SDP answer. Media flows peer-to-peer between the browser and Ring. On teardown the browser calls DELETE /api/stream so the Lambda closes the Ring session (important for battery-powered cameras). The Lambda only accepts Ring's session URL here (https://api.amazonvision.com/v1/devices/{id}/media/streaming/whep/sessions/{id}, plus whatever query string Ring adds, such as ?location=…), because the URL comes back from the browser and gets the user's Ring token attached.

If Ring rate-limits a stream (429), the tile waits for Ring's Retry-After (or 1s, doubling, up to 60s) before trying again. If Ring rejects the access token (401), the backend refreshes it and retries once. If the link itself is gone (refresh rejected, or Watchtower removed in Ring My Apps), the dashboard shows "Reconnect Watchtower to Ring" instead of a generic error.

Battery-powered cameras. Watchtower streams every shared camera for as long as the dashboard is open. That's the point of a video wall, but it drains battery cameras quickly, and Ring caps live session length by power source. For battery devices, consider streaming on demand (click to play) or pausing streams for tiles that aren't visible.

DynamoDB schema (single table)

Entity PK SK Notes
Unclaimed token UNCLAIMED#<accountId> TOKEN status=UNCLAIMED, 15-min TTL
Claimed token USER#<userId> TOKEN status=CLAIMED
App user EMAIL#<email> USER your side of the link
Sign-in code PASSCODE#<email> CODE 6-digit code, 10-min expiry + TTL
Webhook seen WEBHOOK#<request_id> SEEN dedupe record, 24-hour TTL
Refresh lock REFRESHLOCK#<userId> LOCK 10-second lease while a token refreshes

A GSI named StatusIndex (partition key status) lets the linking step scan unclaimed tokens, and lets the app_integration_removed webhook find a customer's claimed token by Ring account ID.

How token refresh works

Access tokens last about 4 hours. getValidToken refreshes one when it's within 5 minutes of expiry, or when Ring has just rejected it (401). Ring rotates refresh tokens: each refresh returns a new one and invalidates the one used. That matters here, because opening the dashboard sends a request per camera at the same moment. If each one refreshed, only the first would succeed and the rest would fail with invalid_grant, which looks like a broken link.

So refreshes are serialized per customer:

  1. One request takes a short lock: a conditional put on REFRESHLOCK#<userId> with a 10-second lease, so a crashed holder can't block anyone for long.
  2. Inside the lock it re-reads the token (another request may have just refreshed it), calls Ring, and saves the new pair with a conditional write (refreshToken = <the one used>), so a stale write can never overwrite a newer pair.
  3. The other requests wait for the stored token to change, then use it. If they wait more than 8 seconds, they answer with a temporary 503 instead of a "reconnect" prompt.

Within one process, concurrent calls also share a single in-flight refresh.

How webhooks are handled

POST /webhooks/ring verifies the signature, then records meta.request_id with a DynamoDB conditional put, so a duplicate delivery is skipped even across Lambda instances. The event is handled before the 200 response: on Lambda, work scheduled after responding may never run. It's a few small DynamoDB calls, well inside Ring's 5-second limit. If handling fails, the request_id is released and the endpoint answers 500 so Ring retries.

On app_integration_removed (the customer removed Watchtower in Ring My Apps), Ring has already revoked the tokens, so the handler deletes everything Watchtower stores for that customer: the claimed token, the user record, any sign-in code, and any unclaimed token. Other events are logged only.

Security notes

  • Webhook signatures are verified over the raw request bytes before any processing.
  • Nonces are compared with crypto.timingSafeEqual (constant time).
  • Dashboard API routes require a signed, httpOnly session cookie (HMAC-signed with a key derived from RING_CLIENT_SECRET, or with SESSION_SECRET if you set one; set at login, verified server-side on every /api route). The cookie is httpOnly + Secure so it isn't readable by JavaScript, and its signature is checked constant-time — a client can't forge a session or impersonate another user by supplying an id.
  • Access tokens auto-refresh (5-minute buffer) via the refresh token.
  • Every request to api.amazonvision.com and oauth.ring.com sends Ring's required headers (API Reference): User-Agent: Watchtower/<version> and Hardware_Id, a UUID that's stable for each customer (a name-based UUID derived from their Ring account ID, so nothing extra is stored). Calls made before the account ID is known use an app-level ID. Ring's firewall may answer requests without these headers with 406 Not Acceptable. If you fork this app, change the User-Agent name in backend/src/ringClient.js.
  • Ring secrets live only in Lambda environment variables / your local .env; never commit .env.

Ring UX design guide compliance

This sample follows Amazon's App Design Guide for Ring Customer Experiences. What it implements:

  • Passwordless account linking (the guide's recommended, lowest-friction pattern): the email comes from the Ring User Data API, the user is auto-provisioned, no form or password.
  • Account linking confirmation screen: after linking, the app shows an "Account linked" screen listing connected devices with online/offline status and a "Go to dashboard" button, rather than dropping the user straight into the app.
  • No verification during linking: email codes are only used for later dashboard sign-in, never as a blocking step in the link flow.
  • Devices by name, never by ID; online/offline status with a dot and a text label (color is never the only signal).
  • Offline devices are shown dimmed with guidance ("Check its power and Wi-Fi in the Ring app"), not hidden.
  • Plain-language errors with a next step: an expired link, a failed link, and a link that has ended ("Reconnect Watchtower to Ring") each explain what happened and where to fix it.
  • Helpful empty state directing the customer to add the app from the Ring Appstore, kept distinct from a "couldn't load" error state (which offers a retry) so the two are never confused.
  • First-visit tutorial: a short, skippable walkthrough shown once on the first dashboard visit, replayable any time via the menu's "How this works" item.
  • Subscription & trial status: the menu shows the customer's paid-subscription / trial state (via the Subscriptions Query API), including trial days remaining, and points to Ring My Apps for managing the plan (and notes that a trial continues as a paid plan when it ends if auto-renewal is on).
  • Silent token refresh; tokens and internal IDs are never shown to the customer.
  • In-app data deletion: "Delete my account & data" in the menu deletes the stored token, user record and any pending sign-in code (Watchtower data only; it doesn't touch the Ring account). The confirmation says exactly what's deleted and that removing Watchtower in Ring My Apps is what ends camera access, since one-way-linked apps can't revoke it themselves.
  • Accessibility (WCAG 2.2 AA intent): text labels alongside color, prefers-reduced-motion support (the LIVE pulse stops), visible keyboard focus, Escape-to-close menu, image alt/aria labels. Full WCAG conformance still requires manual testing with assistive technologies.

Intentionally out of scope for a minimal sample (add these for a real app; see the guide): Smart Alerts / motion-zone configuration guidance messages, live device_removed webhook-driven UI updates, an activity history view, and proactive engagement (daily summaries / notification preferences).

Going to production

This is a sample, optimized to be read and deployed quickly. Before you put something like it in front of real users, work through these:

  • Amazon SES: leave the sandbox. New SES accounts are in the sandbox, which only delivers to verified recipient addresses, so the email sign-in code only reaches addresses you've verified. Request production access in the SES console to email arbitrary users, and verify a domain identity (with DKIM) for SES_FROM_EMAIL rather than a single address. Watch your bounce/complaint rates. Consider SESv2 and a configuration set for event tracking.
  • Sessions / auth. The dashboard uses a signed, httpOnly, Secure session cookie (HMAC-signed with a key derived from RING_CLIENT_SECRET, set at login and verified server-side on every /api route via a single requireUser chokepoint). That's a real, if minimal, session: the cookie can't be read by JS or forged by a client. For production, harden it further — add CSRF protection (the cookie uses SameSite=Lax as a baseline; add CSRF tokens for state-changing requests), rotate/short-expire the token, and keep a server-side session/revocation store so you can invalidate sessions. A signed JWT is a fine alternative to the HMAC cookie. To sign everyone out on demand, set your own SESSION_SECRET (at least 32 random characters) and change it when needed; the derived key never changes because the Ring client secret can't be rotated. Store it in Secrets Manager (see below).
  • Sign-in code hardening. Codes are 6 digits with a 10-minute expiry, a 5-attempt cap per code, and a one-minute resend cooldown per email. For production also add per-IP rate limiting (for example, API Gateway throttling or AWS WAF) and consider a longer code or lockout/backoff.
  • IAM least privilege. The Lambda's SES policy uses Resource: "*". Scope it to your verified SES identity ARN, and review the DynamoDB grant. Nothing here should run with broad permissions.
  • Webhook processing. Events are de-duped in DynamoDB and handled before the 200 response, which is fine for the few fast DynamoDB calls here. If you add slow work (notifications, video analysis), put events on a queue such as SQS with a dead-letter queue and process them there.
  • Secrets management. Ring secrets are passed as plaintext Lambda environment variables. For production, store them in AWS Secrets Manager (or SSM Parameter Store with KMS) and load them at runtime, so they're encrypted at rest and auditable. Never commit .env.
  • Proactive token refresh. This sample refreshes a customer's access token only on demand, when they open the dashboard (getValidToken, 5-minute buffer). Ring refresh tokens expire after about 30 days without use, so a customer who doesn't open the app for a month has to re-link from the Ring app. Ring requires production apps to refresh proactively: add a scheduled job (for example, an EventBridge rule that runs daily) that refreshes every linked customer's token through the same getValidToken path, so it shares the per-customer refresh lock described in How token refresh works. See Ring's Refresh Tokens guide.
  • Token storage. OAuth access/refresh tokens are stored in DynamoDB. Enable encryption at rest with a customer-managed KMS key, restrict table access, and set RemovalPolicy.RETAIN (this sample uses DESTROY so cdk destroy cleans up).
  • CORS. The API sends no CORS headers because the SPA and API are same-origin. If you move the frontend to a separate origin, allow exactly that origin (with credentials), never all origins.
  • Observability & limits. The backend already writes structured JSON logs. Add metrics and CloudWatch alarms (for example, a metric filter on "refresh token rejected" or "webhook processing error" log lines that alerts you), and request throttling on API Gateway.

A note on npm audit

The backend and frontend have no known vulnerabilities. npm audit may still flag a moderate minimatch advisory: it's bundled inside aws-cdk-lib (used only to deploy, never in the Lambda or the browser), so it can't be patched here. Updating aws-cdk-lib picks up the fix once AWS ships it.

Privacy

PRIVACY.md is a privacy policy template that describes exactly what this sample collects, why, who it's shared with, how long it's kept, and how customers delete their data or withdraw consent. If you publish an app based on Watchtower, adapt it, have it reviewed, and link it from your Ring Appstore listing (Ring requires a privacy policy link on the listing).

Readiness checklist exceptions

.ring-readiness.json records the checks this sample intentionally doesn't implement, with the reason and where the gap is documented. Right now that's the scheduled token refresh job (TOK-02); see Going to production. A real app should implement them rather than copy this file.

License

MIT-0 (MIT No Attribution). See LICENSE.

About

No description, website, or topics provided.

Resources

Readme
MIT-0 license

Code of conduct

Code of conduct

Contributing

Contributing

Security policy

Security policy
Activity
Custom properties

Stars

0 stars

Watchers

0 watching

Forks

0 forks
Report repository

Releases

Packages

Used by

Contributors

Languages

Generated from amazon-archives/__template_MIT-0
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.