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.
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) │
└──────────────────────┘ └────────────────────┘
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.
- 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.
- 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/ringendpoint that verifies theX-SignatureHMAC, 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/subscriptioncalls 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 acomponent_idquery parameter.
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.
| 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. |
- 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 configureorAWS_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 devruns the backend onlocalhost:3000and the Vite dev server onlocalhost:5173(proxying/apito the backend), using the same.env. It's for working on the dashboard: account linking and webhooks still need the deployed URLs.
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.
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.
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-sampleThen 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.
From the project directory you cloned in step 3, install all workspace dependencies:
npm install # installs root + backend + frontend + infra workspacesIf 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 regionThen 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>/.
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:
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.
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.
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.
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.
| 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.
- User installs your app in the Ring Appstore and picks devices.
- Ring
POSTs an authorization code to/oauth/token(form-encoded). The backend exchanges it athttps://oauth.ring.com/oauth/token, calls the Ring User Data APIGET /v1/users/mefor theaccount_idand the user's email, and stores the token as unclaimed (keyed byaccount_id, 15-minute TTL) with the email cached on it. - 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. - The SPA calls
POST /api/linkwith just thenonceandtime(no email, no password). This endpoint checks thetimefreshness window again (Ring requires it everywherenonceandtimeare used), and ignores unclaimed tokens past their TTL, since DynamoDB deletes expired items lazily. - 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. - It reads the Ring-provided email, auto-provisions the user (
emailVerified: true, no password), callsPOST /v1/accounts/me/app-integrations(nonce verify) thenPATCH … {status:"completed"}to finalize, promotes the token to claimed (keyed by your user id), and the user lands on the grid. Theaccount_identifiersent 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.
Account linking establishes the user. After they sign out, they sign back in with an email code: no password, no second Ring round-trip:
- On the sign-in screen the user enters their email; the SPA calls
POST /api/auth/send-code. - 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 fromSES_FROM_EMAIL. A new code is sent at most once a minute per email. (Unknown emails, and requests inside the cooldown, get the samesentresponse, so the endpoint doesn't reveal which emails have accounts.) - 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.
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.
| 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.
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:
- 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. - 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. - 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.
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.
- 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 withSESSION_SECRETif you set one; set at login, verified server-side on every/apiroute). The cookie ishttpOnly+Secureso 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.comandoauth.ring.comsends Ring's required headers (API Reference):User-Agent: Watchtower/<version>andHardware_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 with406 Not Acceptable. If you fork this app, change theUser-Agentname inbackend/src/ringClient.js. - Ring secrets live only in Lambda environment variables / your local
.env; never commit.env.
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-motionsupport (the LIVE pulse stops), visible keyboard focus, Escape-to-close menu, imagealt/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).
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_EMAILrather 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/apiroute via a singlerequireUserchokepoint). 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 usesSameSite=Laxas 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 ownSESSION_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 samegetValidTokenpath, 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 usesDESTROYsocdk destroycleans 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.
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.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).
.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.
MIT-0 (MIT No Attribution). See LICENSE.






