# Design Decisions This document captures the key architectural decisions made while building the at-run serverless execution system and AtmosphereConf VOD app. Decisions are attributed to who drove them. ## Core Architecture ### Serverless on AT Protocol (Mainasara) The foundational idea: store JavaScript bundles as blobs on your PDS, execute them via runners. "Your code is your data." This enables decentralized compute where anyone can run a runner and execute bundles from any PDS. ### Deno Sandbox (Claude) Use Deno subprocess for sandboxed execution instead of Bun's unsafe `import()`. Deno's permission model (`--allow-net`, `--allow-read`, etc.) maps cleanly to bundle-declared permissions, providing security without complex VM isolation. ### Manifest + Endpoint Pattern (Claude) Bundle authors declare a `manifest()` with permissions and `endpoint()` functions with handlers. The runner extracts this metadata at load time and enforces permissions at execution time. ```typescript export const bundle = manifest({ name: "my-api", permissions: { net: ["api.example.com"] }, }) export const getData = endpoint({ handler: async () => { ... }, }) ``` ### Secrets Encryption (Claude) Secrets are encrypted with the runner's public key using X25519-XSalsa20-Poly1305. Bundle authors encrypt secrets for specific runners, stored on-chain. Runners decrypt at execution time and inject as environment variables. This keeps secrets safe even though bundles are public. ### Env Permissions Derived from Secrets (Mainasara) Rather than bundle authors declaring which env vars they need, derive `--allow-env` permissions automatically from the secrets configured for that bundle on each runner. Simpler and more secure. ## Task System ### Background Jobs with Concurrency Limiting (Mainasara) Heavy operations like thumbnail generation were killing the $5 VPS. The user requested a "job paradigm" - something that queues work and returns status. ### Task Abstraction (Claude) Implemented `task()` as a variant of `endpoint()` with: - `concurrency`: max parallel executions (prevents resource exhaustion) - `cacheTtl`: result caching (avoids redundant work) - Input-based deduplication (same args = same task) Clients get `{ status: "pending" | "running" | "complete", result?: ... }` and retry until complete. ```typescript export const videoMetadata = task({ concurrency: 2, cacheTtl: 86400, // 24h handler: async ({ uri }) => { ... }, }) ``` ### Client Polling (Claude) Web client uses `callTask()` helper that retries every 2 seconds until status is "complete". Simple and works with any HTTP client. ## Video Processing ### HLS Streaming (Mainasara) Use stream.place's existing HLS infrastructure. The VOD bundle fetches playlists, rewrites relative URLs to absolute, and serves to video players. ### URL Rewriting for Local FFmpeg (Claude) HLS playlists use relative URLs. For ffmpeg to process them locally, rewrite all segment URLs to absolute (`https://vod-beta.stream.place/xrpc/...`). ### Sprite Sheet Hover Previews (Mainasara) YouTube-style thumbnail previews on hover. User wanted this specific UX. ### Sprite Implementation (Claude) Generate 8 frames at even intervals, combine into 4x2 grid using ffmpeg's `xstack` filter. Output WebVTT with `#xywh=` coordinates for each frame's position. Web component parses VTT and cycles through frames on hover. ### 2x Quality Sprites (Mainasara) Initial 160x90 frames looked blurry. User requested 320x180 for sharper previews. ## Accelerator Architecture ### Offload to Serverless GPU (Mainasara) The $5 VPS couldn't handle ffmpeg thumbnail generation at scale. User suggested Modal/RunPod with GPU acceleration. ### Runner as Gateway, Modal as Compute (Claude) Keep the runner as a lightweight API gateway handling: - Task queue (deduplication, caching) - Request routing - Secrets management Offload heavy compute to Modal. The task just calls Modal's endpoint and returns the result. ### Frontend-Side Processing with MediaBunny (Mainasara) After debugging CUDA/ffmpeg image issues on Modal, decided to handle video processing from the frontend using mediabunny.dev instead of server-side acceleration. ## Web App ### YouTube-Style Layout (Mainasara) User requested a layout matching YouTube's player page: video on left, metadata and related content on right. ### Fixed Aspect Ratio Container (Claude) Use `aspect-ratio: 16/9` on the video container to prevent layout shift when the video loads. ### Creator Profiles from Bluesky (Mainasara) Show the video creator's Bluesky profile (avatar, display name) on the video page. ### Profile Endpoint (Claude) Add `getProfile` endpoint that fetches from `public.api.bsky.app` and returns normalized profile data. ## Deployment ### Docker with Bun + Deno + FFmpeg (Claude) Single Dockerfile that installs all three runtimes. Runner uses Bun for the HTTP server, spawns Deno for sandboxed execution. ### Volume Mount for Keys (Mainasara) Runner keys stored at `~/.at-run/` on the host, mounted read-only into the container. User specified this path. ### Dokploy Deployment (Mainasara) User's platform of choice for Docker deployments. ## Social Sharing ### Share Endpoints Pattern (Mainasara) For rich link previews in a static SPA, create server endpoints that return HTML with OG meta tags then redirect to the frontend. The share button copies a link to the server endpoint, not the SPA route. ### Simplified OG Images (Claude, with Mainasara's direction) Initially attempted satori-based OG image generation with custom fonts and layouts. Encountered WASM initialization issues in the Deno sandbox. Simplified to using original thumbnails/avatars directly as OG images, with titles formatted as "Streamhut | \". ## Runner Improvements ### Dynamic Imports for Sandbox Compatibility (Claude) Libraries that access `process.env` at import time break bundle extraction. Solution: use dynamic `import()` inside handlers so the code only runs after permissions are applied. ### Merge Env Permissions with Secrets (Claude) Runner was replacing bundle's declared env permissions with only secret keys. Fixed to merge both - bundles can declare env vars they need (e.g., `JEST_WORKER_ID` for satori) while also getting their secrets. ### Don't Cache "Latest" Version Resolution (Claude) The runner was caching `did/name/latest` -> specific AT URI. After deploying a new version, old URI was served until restart. Fixed by skipping cache when version is `"latest"`. ### Robust Version Sorting (Claude) Use `parseInt()` instead of `Number()` for parsing version strings. Handles edge cases like "1.2.3-beta" correctly. ## Lessons Learned 1. **Start simple, add complexity when needed**: Began with synchronous endpoints, added task queue only when resource exhaustion became a problem. 2. **Platform ffmpeg builds don't include CUDA**: Getting GPU-accelerated ffmpeg requires custom builds or specialized images. Sometimes it's easier to use a managed service. 3. **Env access at module load time breaks extraction**: The runner extracts bundle metadata in a minimal sandbox. Any code that runs at import time (like `Deno.env.get()`) must be deferred to handler execution. 4. **HLS + ffmpeg + seeking is tricky**: Accurate seeking in HLS streams requires `-ss` after `-i`, not before. Seek times must be clamped to video duration. 5. **User-Agent matters**: Some APIs return 403 without a proper User-Agent header. Always set one when making HTTP requests from serverless functions. 6. **Keep OG images simple**: Generated images with custom fonts require WASM, font fetching, and complex rendering. Using existing thumbnails/avatars is more reliable and the result is often better anyway.