atmo.watch #
review movies and tv shows with your atmosphere account, using popfeed.social's lexicons.
movie and show data by tmdb
API Worker #
The public Contrail API is a separate Cloudflare Worker under api/. Contrail's generated client supports both the deployed target and the local API:
pnpm dev # use the deployed API at https://api.atmo.watch
pnpm api:dev # start only the local SQLite API
pnpm dev:local # use an already-running local API
pnpm dev:stack # start the local API and web app together
Local mode uses http://127.0.0.1:8787 and stores its SQLite database under .contrail/. Set CONTRAIL_URL to another loopback URL when running the web app against a custom local port. It never falls back to production when the local API is unavailable.
Run pnpm contrail:generate after changing the API config to regenerate the typed source contract. After deploying that contract with pnpm api:deploy, run pnpm contrail:update:prod to refresh the production provider lock and generated target. Commit the regenerated lock, client, Lexicons, and types.
Use pnpm api:check for API validation.
Cloudflare deployment #
The web app uses @sveltejs/adapter-cloudflare and the Worker configuration in
wrangler.jsonc. It has dedicated atmo-watch KV namespaces bound as:
OAUTH_SESSIONSOAUTH_STATESMEDIA_CACHE
The first two namespaces store AT Protocol OAuth state and sessions. MEDIA_CACHE stores the small, slow-changing OMDb ratings dataset and site-wide TMDB artwork overrides. General TMDB data uses Cloudflare's Cache API with a small per-isolate memory cache, avoiding high-volume KV writes for media and search caching.
Public response caching #
- Movie/TV
og.pnghandlers cache rendered PNGs for 24 hours in Cloudflare's Cache API. Keys use the media type, numeric TMDB ID, and saved artwork revision; slugs and query parameters cannot create extra render variants. Editing artwork selects a new cache key. Concurrent requests for the same key share one render within an isolate, with a separate readable response body for each caller. Bump the cache version insrc/lib/og-cache.server.tswhen changing the image template. - Public rating summaries, anonymous media review pages, and submitted video lists use a 60-second Cache API + per-isolate memory cache. Successful empty results are cached; failed requests are not. Review pagination, media types/IDs, and TV seasons have separate keys. Signed-in catalog loads bypass community caches for fresh post-publish data; authenticated review results and hydrated viewer likes never enter the shared cache. Scores embedded in OG images refresh with the image TTL.
- Anonymous catalog loads first check
watch.atmo.review.getCommunityPresence, cached for five minutes by media type/numeric ID. Empty movies/shows make one indexed existence query instead of three/four enrichment calls. Review-only and video-only titles load only the relevant branches; TV presence includes season/episode videos. Video and season subpages use the same hint. New first-time community content can take up to five minutes to appear anonymously, plus ingestion delay; signed-in loads bypass the hint and public community caches. Errors are not cached as empty and fall back to normal enrichment. Nonexistent catalog entries fail before any community queries. - Healthy homepage results keep their six-hour cache, including displayed Popfeed ratings; degraded/empty results expire after one minute. Titles are selected/ordered by the last 30 days of ratings, but their displayed score and count use the all-time summary, just like the title page. A new rating can therefore take up to six hours to appear on the homepage; underlying TMDB/OMDb data keeps its longer caches.
- Cache API storage is best-effort and local to each Cloudflare data center. New/evicted keys still require rendering or API reads; this complements crawler blocking, rather than replacing it. Cache failures fall back to normal generation/fetching. Outside Cloudflare, public JSON caching still uses memory; finished PNGs are not retained after their in-flight render completes.
- Do not enable blanket shared caching for HTML: pages contain viewer-specific likes and country-dependent streaming availability. OAuth/session data is never put into these public caches. Profile-review OG images and intentionally random scene reads are unchanged.
Homepage availability #
Homepage enrichment has a shared eight-second network deadline and preserves available sections when other providers fail. TMDB media-detail and theaters GETs use a cancellable direct transport (2.5 seconds, no SDK retry loop); the SDK does not support signals/injectable fetch. Contrail server reads use a three-second transport deadline. Both cover response-body transfer, not just headers, and preserve the generated endpoint/auth contract.
OMDb fetches have a 1.8-second deadline inside a 3.5-second total enrichment budget; homepage optional ratings waits are capped at two seconds and observe the page deadline. Failed rating requests retain the existing transient-failure/quota backoff rather than poisoning an in-flight promise indefinitely. All timers are cleared after completion.
Public cache reads/writes have 750 ms bounded awaits, and artwork KV reads have one-second bounded awaits. KV and Cache API operations are not cancellable: these limits stop waiting, not the underlying platform operation. Network fetches receive actual abort signals. The eight-second enrichment budget is not a strict end-to-end response limit: cache, artwork, and session/layout work also take time.
Partial/empty homepage results are cached briefly to prevent a failure storm, with a small availability notice in the UI; they never replace a healthy result for six hours. Search, reviews, and any healthy title/theaters sections remain usable.
These availability protections do not repair stalled ingestion. Check api/README.md before any recovery; never reset/jump an expired cursor just to silence the health warning.
Catalog crawling and indexing #
static/robots.txtexcludes cast filmographies, frontend API routes, artwork editors, and secondary catalog video/season pages. It does not block title pages, profiles, individual community reviews/lists, or individual submitted videos.- Empty title pages receive
X-Robots-Tag: noindex, nofollowand matching HTML metadata. Rating-only reviews or submitted TV descendant videos count as community content. Unknown presence/enrichment failures do not de-index titles. - Cast, season, and catalog-video pages have
noindex, nofollowmetadata too. Cast/recommendation/filmography/season expansion links use selectiverel="nofollow"; ordinary community/discovery title links remain followable. - Title canonical links and
og:urluse the numeric ID plus normalized title slug, without incoming alias slugs, leading zeros, or query parameters. Alias URLs still work for users; canonical tags consolidate indexing rather than preventing fetches. - Robots exclusions are voluntary and prevent compliant fetching, not access.
noindexrequires a fetch and cannot remove an already-indexed URL while robots blocks fetching it.nofollowis a hint, not an enforcement mechanism. Unwanted bots need Cloudflare edge rules; this change does not configure WAF rules or impose a spending cap.
Deploy the API before the frontend so the new presence method is available. Then refresh the production contract with pnpm contrail:update:prod as described above. Existing production auth permissions are unchanged; no D1 migration is needed because the probes use Contrail's existing queryable-field indexes. No global shared HTML caching is introduced.
Run the server-side regression tests with pnpm exec vitest run --project server. The SQL tests use Node's built-in SQLite, including query-plan assertions for identifier index usage.
Media artwork can be curated at /movie/[id]/edit or /tv/[id]/edit. Access is restricted by AT Protocol DID; additional curator DIDs can be configured with the comma- or whitespace-separated MEDIA_CURATOR_DIDS private runtime value.
Before the first deployment, configure the private runtime values without adding them to wrangler.jsonc:
pnpm exec wrangler secret put COOKIE_SECRET
pnpm exec wrangler secret put CLIENT_ASSERTION_KEY
pnpm exec wrangler secret put TMDB_ACCESS_TOKEN
pnpm exec wrangler secret put OMDB_API_KEY # optional
ORIGIN is configured as https://atmo.watch in wrangler.jsonc. Build, preview, or deploy with:
pnpm build
pnpm preview
pnpm deploy