# YouTube Summarizer: StartOS 0.4.0 Packaging Game Plan

**Date:** April 9, 2026
**Status:** Feasibility Assessment & Implementation Roadmap
**Target:** StartOS 0.4.0 Beta (s9pk package format)
**Reference:** Workout Log packaging for StartOS 0.3.5

---

## Executive Summary

Packaging the YouTube Summarizer for StartOS 0.4.0 is **feasible** but presents unique challenges that a typical web app does not. The app is more than a simple CRUD service -- it downloads audio from YouTube (requiring yt-dlp to always be current), authenticates against YouTube to bypass bot detection, transcribes audio via the Gemini API, and manages podcast RSS feeds. Running this on a headless remote server introduces three core challenges that require thoughtful solutions:

1. **yt-dlp must stay current** -- YouTube changes its anti-bot signatures regularly, so a stale yt-dlp binary means broken downloads within days or weeks.
2. **YouTube authentication without a local browser** -- The current app relies on cookies from your local browser or a cookies.txt file. A remote server has no browser session.
3. **Server/datacenter IP reputation** -- YouTube is more aggressive about blocking downloads from non-residential IPs.

All three are solvable. This document lays out the approach for each, plus the full packaging plan.

---

## Table of Contents

1. [Current App Architecture](#1-current-app-architecture)
2. [What Changes for StartOS](#2-what-changes-for-startos)
3. [Challenge 1: Keeping yt-dlp Updated](#3-challenge-1-keeping-yt-dlp-updated)
4. [Challenge 2: YouTube Authentication on a Headless Server](#4-challenge-2-youtube-authentication-on-a-headless-server)
5. [Challenge 3: Server IP Bot Detection](#5-challenge-3-server-ip-bot-detection)
6. [StartOS 0.4.0 Packaging Structure](#6-startos-040-packaging-structure)
7. [Differences from v0.3.5 Packaging (Workout Log)](#7-differences-from-v035-packaging-workout-log)
8. [Implementation Phases](#8-implementation-phases)
9. [Persistent Data Contract](#9-persistent-data-contract)
10. [Ongoing Maintenance Plan](#10-ongoing-maintenance-plan)
11. [Risk Assessment](#11-risk-assessment)
12. [Multi-User Distribution: API Keys and Clean Installs](#12-multi-user-distribution-api-keys-and-clean-installs)
13. [Appendix A: File-by-File Packaging Checklist](#appendix-a-file-by-file-packaging-checklist)
14. [Appendix B: Reusable Packaging Roadmap for Future Apps](#appendix-b-reusable-packaging-roadmap-for-future-apps)

---

## 1. Current App Architecture

The YouTube Summarizer is a Node.js application with these components:

**Backend (server/index.js -- ~1800 lines):**
- Express.js server on port 3001
- Calls yt-dlp as a child process to download audio from YouTube
- Downloads podcast episodes directly via HTTP
- Uploads audio to Google Gemini File API for transcription
- Runs topic analysis via Gemini (multiple model fallback chain)
- Manages subscriptions (YouTube channels + podcast RSS feeds)
- Auto-queue system: checks subscriptions for new content, queues for approval
- Cookie management: supports cookies.txt file and --cookies-from-browser flag
- yt-dlp auto-update: checks GitHub releases every 24h, updates via self-update / brew / pip
- History storage: JSON files on disk (not a database)
- Health check endpoint at /api/health
- Heartbeat/auto-sleep system (shuts down when no browser connected for 30s)

**Frontend (public/index.html):**
- Single-page app, served as static files by Express
- Split-screen layout: video embed + topic summaries
- Subscription management UI
- Cookie upload/test UI
- Settings panel for API key and model selection

**External Dependencies:**
- yt-dlp (system binary, called via child_process)
- ffmpeg (required by yt-dlp for audio extraction)
- Node.js 20+
- Google Gemini API (requires API key)

**Data Storage (all in /history/ directory):**
- Individual summary JSON files (one per processed video/podcast)
- subscriptions.json (channel/feed list)
- _meta.json (folder structure for organizing summaries)
- seen-list.json, skip-list.json (dedup tracking)
- auto-queue.json (pending items from subscription checks)
- cookies.txt (YouTube authentication)
- .env (Gemini API key, cookie browser preference)

---

## 2. What Changes for StartOS

### Things That Work As-Is
- The Express server, Gemini API integration, and frontend are platform-agnostic
- Podcast RSS feed parsing (direct HTTP, no auth needed)
- History/subscription JSON storage
- Health check endpoint

### Things That Need Adaptation

| Current Behavior | StartOS Adaptation |
|---|---|
| Runs on macOS with Homebrew | Docker container on Alpine Linux (ARM64) |
| yt-dlp installed via brew | yt-dlp installed via pip in container, self-updates at runtime |
| Cookies from local Firefox/Chrome | cookies.txt uploaded via web UI (already supported) + OAuth2 |
| Auto-sleep when no browser connected | Remove sleep logic; service runs continuously |
| Hardcoded port 3001 | Configurable, default 3001, mapped via StartOS interfaces |
| .env file for config | StartOS config UI (manifest config spec) |
| macOS .app bundle / launcher | Not needed; StartOS manages service lifecycle |
| LAN mode dialog | Not needed; StartOS handles network exposure (Tor + LAN) |

---

## 3. Challenge 1: Keeping yt-dlp Updated

### The Problem
YouTube frequently changes its download mechanisms and anti-bot measures. A yt-dlp version that works today may stop working within 1-2 weeks. The app already handles this with a multi-strategy auto-update (self-update, brew, pip), but inside a Docker container on StartOS, brew is not available and we need a reliable update path.

### The Solution: Runtime Self-Update with Persistent Storage

**Strategy:**
1. Install yt-dlp via pip at Docker **build time** (gives a known-good starting point)
2. Store the yt-dlp binary on the **persistent volume** (`/data/bin/yt-dlp`) so updates survive container restarts
3. On each container start, check if the persistent binary exists and is newer than the built-in one; use whichever is newer
4. The existing auto-update logic runs `yt-dlp -U` (self-update) which writes to the pip site-packages, but we can also add a dedicated update mechanism that downloads the latest binary directly from GitHub releases to `/data/bin/yt-dlp`

**Implementation in docker_entrypoint.sh:**
```bash
#!/bin/sh
set -eu

DATA_DIR="/data"
BIN_DIR="$DATA_DIR/bin"
YTDLP_LOCAL="$BIN_DIR/yt-dlp"

mkdir -p "$BIN_DIR" "$DATA_DIR/history" "$DATA_DIR/config"

# Use persistent yt-dlp if available, otherwise fall back to system
if [ -x "$YTDLP_LOCAL" ]; then
  export PATH="$BIN_DIR:$PATH"
  echo "Using persistent yt-dlp: $($YTDLP_LOCAL --version)"
else
  echo "Using system yt-dlp: $(yt-dlp --version)"
fi

# Start the Node.js server
exec node /app/server/index.js
```

**Auto-update enhancement in server code:**
- Modify `autoUpdateYtdlp()` to: (1) try `yt-dlp -U`, (2) try `pip install -U yt-dlp`, (3) as a last resort, download the latest binary directly from `https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp` and save to `/data/bin/yt-dlp`
- Add a scheduled check: on server startup and every 12 hours, check for updates
- Expose update status in the health check endpoint so StartOS can surface it

**Why this works on StartOS:**
- `/data` is the persistent StartOS volume; it survives container restarts and package upgrades
- The container image provides a baseline yt-dlp that works at build time
- Runtime updates keep it fresh without rebuilding the entire package
- If a runtime update breaks something, removing `/data/bin/yt-dlp` reverts to the built-in version

### Also: ffmpeg
ffmpeg is a build-time dependency (Alpine package `ffmpeg`). It rarely needs updates for this use case, so installing it in the Dockerfile is sufficient. It is updated whenever you rebuild and push a new package version.

---

## 4. Challenge 2: YouTube Authentication on a Headless Server

### The Problem
YouTube increasingly requires authentication to download content. The current app supports two methods:
1. **cookies.txt file** -- a Netscape-format cookie export from a browser
2. **--cookies-from-browser** -- reads cookies directly from a local browser's cookie store

On a remote StartOS server, there is no local browser, so method 2 is unavailable.

### The Solution: Three-Tier Authentication

**Tier 1: OAuth2 Device Flow (Primary -- Best Option)**

yt-dlp now supports OAuth2 authentication with a device code flow, which is perfect for headless servers:

```bash
yt-dlp --username oauth --password '' <URL>
```

How it works:
- On first use, yt-dlp prints a code and a URL (https://www.google.com/device)
- The user opens that URL on any device (phone, laptop) and enters the code
- A refresh token is cached in yt-dlp's cache directory
- All subsequent downloads use the cached token -- no further interaction needed

For StartOS integration:
- Mount yt-dlp's cache directory on the persistent volume (`/data/ytdlp-cache/`)
- Add a "Setup YouTube Auth" action in the StartOS manifest that triggers the OAuth flow and displays the device code in the service logs or a dedicated endpoint
- Build a simple UI page in the web frontend that shows the device code and instructions
- The token persists across restarts because it lives on `/data`

**Tier 2: cookies.txt Upload (Fallback)**

The app already has a full cookies.txt management system via the web UI:
- Upload endpoint: POST /api/cookies/upload
- Test endpoint: POST /api/cookies/test
- Status endpoint: GET /api/cookies/status
- Delete endpoint: POST /api/cookies/delete

This works on StartOS as-is. The cookies.txt file should be stored on the persistent volume (`/data/cookies.txt`) instead of the project root.

**Important caveat:** YouTube cookies expire in approximately 3-5 days. This means the user would need to re-export and re-upload cookies regularly. This is why OAuth2 is the recommended primary method.

**Tier 3: No Authentication (Limited)**

Some YouTube content can be downloaded without any authentication. The app already handles this gracefully -- it attempts download without cookies if cookie-based download fails. This will work for some videos but not all, especially age-restricted or bot-flagged content.

### Implementation Priority
1. Add OAuth2 support to the server code (new endpoint to initiate flow, display code, check status)
2. Move cookies.txt storage to persistent volume
3. Remove --cookies-from-browser logic (not applicable on remote server)
4. Add a clear "Authentication Setup" section in the web UI that guides through OAuth2 first, cookies.txt as backup

---

## 5. Challenge 3: Server IP Bot Detection

### The Problem
YouTube maintains IP reputation databases. Residential ISP IPs (like your home connection) are generally trusted, but datacenter IPs are frequently flagged. A Start9 server running at home on your residential IP should actually be fine -- this is a significant advantage of self-hosted infrastructure.

### Assessment: Low Risk for Start9 Home Servers

**Start9 servers typically run at home on a residential IP.** This means:
- The server's IP is a normal residential ISP address
- YouTube treats these the same as any home computer
- Bot detection is primarily triggered by datacenter/cloud IPs (AWS, GCP, Azure, etc.)
- Rate limiting may still occur with heavy usage, but this is the same as running the app on your Mac

**When it could be a problem:**
- If you access the Start9 server through a VPN and route yt-dlp traffic through the VPN
- If your ISP uses CGNAT (carrier-grade NAT) which shares IPs with many users
- If you process a very high volume of downloads in a short period

### Mitigation Strategies (Built Into the App)

The app already has several mitigations that carry over directly:

1. **Retry with exponential backoff** -- the download logic already retries with 30s, 60s, 120s delays when rate-limited
2. **Cookie/OAuth authentication** -- authenticated requests are less likely to trigger bot detection
3. **yt-dlp auto-update** -- newer yt-dlp versions include workarounds for the latest YouTube anti-bot measures

**Additional measures to add for StartOS:**
1. **Rate limiting between downloads** -- when processing auto-queue items from subscriptions, add configurable delays between downloads (e.g., 30-60 seconds between videos)
2. **Download scheduling** -- spread subscription checks and downloads across the day rather than bursting
3. **Proxy support (optional)** -- add a config option for users to specify a SOCKS5 or HTTP proxy if they want to route yt-dlp traffic through a specific connection. This is an advanced option, not required for most users.
4. **Browser impersonation** -- yt-dlp supports `--impersonate chrome` which mimics Chrome's TLS fingerprint. Add this as a default argument.

### Summary
For a home Start9 server, YouTube bot detection is **not a significant concern**. The residential IP is the same one you'd be using from your Mac. Combined with OAuth2 authentication and yt-dlp staying current, this should work reliably.

---

## 6. StartOS 0.4.0 Packaging Structure

### Key Changes from 0.3.5

StartOS 0.4.0 is a complete rewrite. The major packaging-relevant changes:

| Aspect | 0.3.5 | 0.4.0 |
|---|---|---|
| Container runtime | Podman | LXC (Linux Containers) |
| Package format | s9pk (same name, different internals) | s9pk with signatures, partial downloads, multi-arch |
| Dev tooling | Shell/make-based | TypeScript SDK available |
| Networking | Tor + LAN | Same, with improved LAN port forwarding |
| Backups | Not compatible across versions | Fresh backups required after migration |

### Package Files Needed

Based on the workout-log template and v0.4.0 documentation:

```
start9/
  0.4/
    manifest.yaml        # Service metadata, config spec, interfaces
    Dockerfile           # Multi-stage build for ARM64
    docker_entrypoint.sh # Service startup script
    healthcheck.sh       # Health check script
    Makefile             # Build automation
    instructions.md      # User-facing documentation
    LICENSE              # License file
    icon.png             # Service icon
    DEPLOY.md            # Deploy/sideload instructions
```

### Draft manifest.yaml

```yaml
id: youtube-summarizer
title: YouTube Summarizer
version: 0.1.0.1
release-notes: >-
  Initial StartOS package. YouTube/podcast audio download,
  transcription via Gemini, and topic analysis.
license: Proprietary
wrapper-repo: https://github.com/user/youtube-summarizer-startos
upstream-repo: https://github.com/user/youtube-summarizer
support-site: https://github.com/user/youtube-summarizer/issues
marketing-site: https://github.com/user/youtube-summarizer
build: ["make image-arm"]

description:
  short: Download, transcribe, and summarize YouTube videos and podcasts.
  long: >-
    YouTube Summarizer downloads audio from YouTube videos and podcast feeds,
    transcribes them using Google Gemini, and produces structured topic
    summaries. Supports subscriptions with automatic new episode detection,
    organized history with folders, and a responsive web interface.

assets:
  license: LICENSE
  icon: icon.png
  instructions: instructions.md
  docker-images: image.tar

main:
  type: docker
  image: main
  entrypoint: docker_entrypoint.sh
  args: []
  mounts:
    main: /data

health-checks:
  main:
    name: API health
    success-message: YouTube Summarizer is responding.
    type: docker
    image: main
    entrypoint: healthcheck.sh
    args: []
    inject: true

config:
  get:
    type: docker
    image: main
    system: false
    entrypoint: sh
    args:
      - -c
      - cat /data/config/startos-config.json 2>/dev/null || echo '{}'
  set:
    type: docker
    image: main
    system: false
    entrypoint: sh
    args:
      - -c
      - cat > /data/config/startos-config.json

dependencies: {}

volumes:
  main:
    type: data

interfaces:
  main:
    name: Web Interface
    description: Browser UI for YouTube Summarizer.
    tor-config:
      port-mapping:
        80: "3001"
    lan-config:
      443:
        ssl: true
        internal: 3001
    ui: true
    protocols: [tcp, http, https]

backup:
  create:
    type: docker
    image: main
    system: false
    entrypoint: sh
    args:
      - -c
      - |
        set -eu
        rm -rf /backup/*
        cp -a /data/. /backup/
    mounts:
      main: /data
      BACKUP: /backup
  restore:
    type: docker
    image: main
    system: false
    entrypoint: sh
    args:
      - -c
      - |
        set -eu
        cp -a /backup/. /data/
    mounts:
      main: /data
      BACKUP: /backup

actions:
  update-ytdlp:
    name: Update yt-dlp
    description: Downloads the latest version of yt-dlp for YouTube compatibility.
    warning: This may take a minute. Service will continue running.
    implementation:
      type: docker
      image: main
      system: false
      entrypoint: sh
      args:
        - -c
        - pip install --upgrade yt-dlp && yt-dlp --version
      inject: true
```

### Draft Dockerfile

```dockerfile
FROM node:20-alpine AS builder

WORKDIR /app

# Copy server package files and install deps
COPY server/package.json server/package-lock.json ./server/
RUN cd server && npm ci --production

# Copy application files
COPY server/index.js ./server/
COPY public/ ./public/
COPY assets/ ./assets/

FROM node:20-alpine AS runner

WORKDIR /app

RUN apk add --no-cache \
    dumb-init \
    curl \
    python3 \
    py3-pip \
    ffmpeg \
  && pip3 install --break-system-packages yt-dlp \
  && addgroup -S appgroup -g 1001 \
  && adduser -S appuser -u 1001 -G appgroup

# Copy app from builder
COPY --from=builder --chown=appuser:appgroup /app ./

# Copy StartOS scripts
COPY start9/0.4/docker_entrypoint.sh /usr/local/bin/docker_entrypoint.sh
COPY start9/0.4/healthcheck.sh /usr/local/bin/healthcheck.sh
RUN chmod +x /usr/local/bin/docker_entrypoint.sh /usr/local/bin/healthcheck.sh

# Create data directory
RUN mkdir -p /data && chown -R appuser:appgroup /data

ENV NODE_ENV=production \
    PORT=3001

EXPOSE 3001

ENTRYPOINT ["dumb-init", "--", "/usr/local/bin/docker_entrypoint.sh"]
```

### Draft docker_entrypoint.sh

```bash
#!/bin/sh
set -eu

DATA_DIR="/data"
HISTORY_DIR="$DATA_DIR/history"
CONFIG_DIR="$DATA_DIR/config"
BIN_DIR="$DATA_DIR/bin"
CACHE_DIR="$DATA_DIR/ytdlp-cache"
COOKIES_PATH="$DATA_DIR/cookies.txt"

# Create directory structure
mkdir -p "$HISTORY_DIR" "$CONFIG_DIR" "$BIN_DIR" "$CACHE_DIR"

# Use persistent yt-dlp binary if it exists and is executable
if [ -x "$BIN_DIR/yt-dlp" ]; then
  export PATH="$BIN_DIR:$PATH"
fi

# Point yt-dlp cache to persistent storage (for OAuth tokens, etc.)
export XDG_CACHE_HOME="$CACHE_DIR"

# Load config from StartOS config file if it exists
if [ -f "$CONFIG_DIR/startos-config.json" ]; then
  # Extract Gemini API key from config
  GEMINI_KEY=$(cat "$CONFIG_DIR/startos-config.json" | python3 -c "import sys,json; print(json.load(sys.stdin).get('gemini_api_key',''))" 2>/dev/null || echo "")
  if [ -n "$GEMINI_KEY" ]; then
    export GEMINI_API_KEY="$GEMINI_KEY"
  fi
fi

# Also check for .env in data dir
if [ -f "$DATA_DIR/.env" ]; then
  export $(grep -v '^#' "$DATA_DIR/.env" | xargs)
fi

export PORT="${PORT:-3001}"
export HOSTNAME="0.0.0.0"

echo "Starting YouTube Summarizer..."
echo "  yt-dlp version: $(yt-dlp --version 2>/dev/null || echo 'not found')"
echo "  ffmpeg: $(ffmpeg -version 2>/dev/null | head -1 || echo 'not found')"
echo "  Data dir: $DATA_DIR"

exec node /app/server/index.js
```

---

## 7. Differences from v0.3.5 Packaging (Workout Log)

The workout-log package was a relatively straightforward Next.js app with a SQLite database. YouTube Summarizer is significantly more complex:

| Aspect | Workout Log (0.3.5) | YouTube Summarizer (0.4.0) |
|---|---|---|
| App framework | Next.js + Prisma + SQLite | Express.js + JSON files |
| External binaries | None | yt-dlp, ffmpeg |
| External APIs | None | Google Gemini API |
| Authentication | App-level (user/pass) | YouTube OAuth + cookies |
| Data storage | Single SQLite DB | Multiple JSON files + temp audio |
| Network access | Inbound only | Inbound + outbound (YouTube, Gemini, RSS) |
| Binary updates | None needed | yt-dlp must stay current |
| Temp file management | None | Large audio files downloaded and cleaned up |
| Container size | Small (~100MB) | Larger (~300-500MB with ffmpeg + yt-dlp + Python) |

### Key Differences in Packaging Approach

1. **Outbound network access:** The workout-log only needed to accept inbound connections. YouTube Summarizer needs outbound access to YouTube, Google Gemini API, and podcast RSS feeds. StartOS containers have outbound network access by default, so this works, but it's worth verifying in the 0.4.0 LXC environment.

2. **Larger image size:** Adding Python, pip, yt-dlp, and ffmpeg significantly increases the Docker image. Expect 300-500MB vs. ~100MB for workout-log. This is acceptable but worth minimizing where possible.

3. **Runtime binary management:** The entrypoint must handle a mutable binary (yt-dlp) that updates at runtime. The workout-log had no such requirement.

4. **Temporary file cleanup:** Audio files are downloaded to /tmp during processing and should be cleaned up. Need to ensure the container's /tmp has sufficient space or use the persistent volume with cleanup logic.

5. **Config complexity:** Workout-log had no configuration. YouTube Summarizer needs at minimum a Gemini API key, and optionally proxy settings, download rate limits, and authentication preferences. This maps to the StartOS config spec in the manifest.

---

## 8. Implementation Phases

### Phase 1: Prepare the App for Headless Operation (Estimated: 1-2 days)

Code changes to server/index.js before any packaging work:

1. **Remove auto-sleep logic** -- the heartbeat/sleep system is designed for a desktop app that shuts down when you close the browser. On StartOS, the service should run continuously.

2. **Move all data paths to a configurable base directory** -- currently paths are relative to the project root. Change to use an environment variable (e.g., `DATA_DIR=/data`) so the persistent volume can be targeted.

3. **Add OAuth2 authentication flow** -- new endpoints:
   - POST /api/auth/oauth/start -- initiates OAuth device flow, returns device code
   - GET /api/auth/oauth/status -- checks if OAuth token has been cached
   - The web UI gets a new "Authentication" section in Settings

4. **Enhance yt-dlp update logic for Linux/container** -- remove Homebrew strategy, add direct binary download from GitHub releases as a fallback.

5. **Add download rate limiting** -- configurable delay between auto-queue downloads (default 30s).

6. **Add browser impersonation flag** -- pass `--impersonate chrome` to yt-dlp by default when available.

7. **Remove macOS-specific code** -- the .app bundle creation, osascript dialogs, LAN mode prompt, etc. are not needed.

### Phase 2: Create the StartOS Package Scaffold (Estimated: 1 day)

1. Create `start9/0.4/` directory with all packaging files (see Section 6)
2. Write Dockerfile with multi-stage build
3. Write docker_entrypoint.sh
4. Write healthcheck.sh
5. Write manifest.yaml with full config spec
6. Write instructions.md
7. Copy icon.png
8. Create Makefile

### Phase 3: Build, Test, and Iterate (Estimated: 2-3 days)

1. Build ARM64 Docker image: `make -C start9/0.4 image-arm`
2. Smoke test locally:
   - `docker load -i start9/0.4/image.tar`
   - Run container with a volume mount and verify /api/health responds
   - Test yt-dlp download of a known video
   - Test Gemini API transcription
   - Test OAuth flow
   - Test subscription check
3. Package with start-sdk: `make -C start9/0.4 package`
4. Sideload on StartOS and test end-to-end:
   - Install service
   - Set up Gemini API key via config
   - Run OAuth authentication
   - Process a YouTube video
   - Process a podcast episode
   - Test subscription auto-discovery
   - Test backup/restore
5. Iterate on issues found

### Phase 4: Documentation and Polish (Estimated: 1 day)

1. Write comprehensive instructions.md for StartOS users
2. Update DEPLOY.md with StartOS 0.4.0 specific steps
3. Update START9_PACKAGING_LOG.md with the complete process
4. Verify backup/restore works correctly
5. Test yt-dlp auto-update from within the running service
6. Document the config spec clearly

---

## 9. Persistent Data Contract

Everything under `/data` persists across container restarts and package upgrades:

```
/data/
  history/                  # All summary JSON files
    subscriptions.json      # Channel/feed subscriptions
    _meta.json              # Folder organization
    seen-list.json          # Dedup tracking
    skip-list.json          # Deleted item tracking
    auto-queue.json         # Pending queue items
    *.json                  # Individual summary records
  config/
    startos-config.json     # StartOS-managed configuration
  cookies.txt               # YouTube cookie file (if uploaded)
  .env                      # Environment overrides (Gemini key, etc.)
  bin/
    yt-dlp                  # Updated yt-dlp binary (runtime-managed)
  ytdlp-cache/              # yt-dlp cache (OAuth tokens, etc.)
```

**Migration contract:** Any future package version must preserve this layout. If the schema of JSON files changes, handle migration in the entrypoint script or a dedicated migration step.

---

## 10. Ongoing Maintenance Plan

### Regular Maintenance (Monthly)

- **Rebuild and push package** -- even if the app code hasn't changed, rebuilding picks up the latest Alpine packages, Node.js patches, and yt-dlp version at build time
- **Check yt-dlp compatibility** -- verify that the runtime auto-update is working by checking the version via the health endpoint

### When YouTube Breaks Things

YouTube periodically makes changes that break yt-dlp. When this happens:

1. Users can trigger "Update yt-dlp" from StartOS Actions menu
2. If the action doesn't fix it, rebuild and push a new package with the latest yt-dlp
3. If yt-dlp itself hasn't released a fix yet, the nightly build channel may have it -- consider switching the auto-update to use `yt-dlp --update-to nightly` temporarily

### When StartOS Updates

- Test the package on new StartOS versions before they're widely deployed
- The 0.4.0 beta may have breaking changes as it stabilizes; pin to specific beta versions during testing
- Keep the wrapper repo and packaging log updated

### Gemini API Changes

- The app already handles model fallbacks (tries multiple models)
- Google periodically deprecates Gemini model versions; update the model list in server/index.js when this happens
- API key management is handled via StartOS config, so no package rebuild needed for key rotation

---

## 11. Risk Assessment

| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| YouTube breaks yt-dlp | High (happens regularly) | Downloads fail until yt-dlp updates | Runtime auto-update + manual action button |
| OAuth tokens expire | Medium | Need to re-authenticate | Clear UI instructions; cookies.txt as backup |
| Gemini API changes/deprecates models | Low-Medium | Transcription fails | Model fallback chain already implemented |
| StartOS 0.4.0 beta has breaking changes | Medium | Package may need rework | Stay on documented APIs; test frequently |
| Docker image too large | Low | Slow install/update | Multi-stage build; Alpine base; minimize layers |
| Container can't reach YouTube (network) | Low (home network) | Downloads fail | StartOS allows outbound; verify in LXC |
| Temp audio files fill disk | Low | Processing fails | Cleanup in /tmp; use TMPDIR on volume if needed |
| cookies.txt expires quickly | High (3-5 days) | Auth fails | OAuth2 as primary; clear messaging about cookie limits |

---

## 12. Multi-User Distribution: API Keys and Clean Installs

### Gemini API Key Management

The Gemini API key must never be baked into the Docker image or package. Each user provides their own key after installation.

**How it works:**

1. The `.env` file (which contains your personal API key) is excluded from the Docker image via `.dockerignore`
2. On fresh install, the service starts with no API key configured
3. The user enters their key via the StartOS config UI, which writes it to `/data/config/startos-config.json` on the persistent volume
4. The entrypoint script reads this file and sets `GEMINI_API_KEY` as an environment variable before starting the server
5. The web UI settings panel also lets users enter/change the key at any time (this writes to the server's in-memory state and persists to the config file)

The app already supports this pattern: it reads `GEMINI_API_KEY` from the environment first, then falls back to the `.env` file. On StartOS the environment variable takes precedence, and the `.env` file simply doesn't exist in a fresh install.

### Clean Installs: No Inherited Data

The Docker image contains only application code -- no user data. Everything personal lives on the `/data` volume, which starts empty for every new install. This means:

- No inherited history (processed videos/podcasts)
- No inherited subscriptions
- No inherited cookies or authentication tokens
- No inherited API keys
- No inherited folder organization or skip/seen lists

**The `.dockerignore` ensures nothing personal leaks into the image:**

```
history/
cookies.txt
.env
*.s9pk
image.tar
node_modules/
.DS_Store
GET-STARTED.*
build-guide-pdf.py
create-app.sh
setup.sh
Start Summarizer.command
start9/
```

**First-run flow for a new user:**
1. Install the package from StartOS
2. Open service config, enter Gemini API key
3. Open the web UI
4. Set up YouTube OAuth authentication (one-time device code flow)
5. Add subscriptions to channels/feeds they want to follow
6. Start summarizing -- their library builds from scratch

---

## Appendix A: File-by-File Packaging Checklist

When creating the StartOS package, create/modify these files:

**New files to create (in start9/0.4/):**

- [ ] `manifest.yaml` -- set id, title, version, interfaces, config spec, actions, backup
- [ ] `Dockerfile` -- multi-stage build, Alpine, Node 20, Python, yt-dlp, ffmpeg
- [ ] `docker_entrypoint.sh` -- data dir setup, yt-dlp path, env loading, server start
- [ ] `healthcheck.sh` -- curl to /api/health
- [ ] `Makefile` -- image-arm, package, verify, clean targets
- [ ] `instructions.md` -- setup guide, OAuth instructions, cookie upload, troubleshooting
- [ ] `DEPLOY.md` -- build + sideload steps for StartOS 0.4.0
- [ ] `LICENSE` -- appropriate license file
- [ ] `icon.png` -- app icon (already exists in assets/)

**App code modifications (in server/index.js):**

- [ ] Add DATA_DIR environment variable support for all file paths
- [ ] Remove auto-sleep/heartbeat shutdown logic
- [ ] Remove macOS-specific code (osascript, LAN mode, brew update strategy)
- [ ] Add OAuth2 device flow endpoints
- [ ] Add download rate limiting for auto-queue
- [ ] Add --impersonate chrome flag to yt-dlp calls
- [ ] Move cookies.txt path to DATA_DIR
- [ ] Update yt-dlp update strategies for Linux container
- [ ] Add proxy support (optional config)
- [ ] Bind to 0.0.0.0 (currently does this via HOSTNAME env)

**Documentation updates:**

- [ ] START9_PACKAGING_LOG.md -- full process documentation (like workout-log)
- [ ] VERSIONING.md -- update with youtube-summarizer version policy
- [ ] 0.4/README.md -- migration notes and packaging intent

---

## Appendix B: Reusable Packaging Roadmap for Future Apps

This section generalizes the process so it can be reused for packaging any app for StartOS.

### Step 1: Assess the App

Before packaging, answer these questions:
1. What language/runtime does the app use? (Determines base Docker image)
2. Does it need external binaries? (yt-dlp, ffmpeg, etc. -- add to Dockerfile)
3. Does it need outbound network access? (API calls, downloads -- verify in StartOS)
4. What is the persistent data? (Database, files, config -- maps to /data volume)
5. Does it have a health endpoint? (Required for StartOS health checks)
6. Does it need configuration? (Maps to StartOS config spec)
7. Does it have any platform-specific code? (macOS, Windows -- must be removed/adapted)

### Step 2: Create the Wrapper

1. Copy the template from an existing wrapper (this project or workout-log)
2. Edit manifest.yaml first -- it defines everything
3. Write the Dockerfile -- multi-stage build, Alpine, minimal layers
4. Write docker_entrypoint.sh -- data init, env setup, exec app
5. Write healthcheck.sh -- simple curl to health endpoint
6. Write instructions.md -- user-facing setup guide

### Step 3: Build and Test Locally

```bash
# Build ARM64 image
make -C start9/0.4 image-arm

# Load and test
docker load -i start9/0.4/image.tar
docker run -it --rm -v ./test-data:/data -p 3001:3001 <image-name>

# Verify health
curl http://localhost:3001/api/health
```

### Step 4: Package and Sideload

```bash
# Must be in a git repo
git init && git add . && git commit -m "Initial packaging"

# Build s9pk
make -C start9/0.4 package

# Verify
make -C start9/0.4 verify

# Sideload via StartOS web UI
```

### Step 5: Document Everything

- Update START9_PACKAGING_LOG.md with the full process
- Record all issues encountered and how they were resolved
- Note any StartOS version-specific quirks
- Keep the manifest and Dockerfile well-commented

### Template Variables for New Projects

| Variable | Description | Example |
|---|---|---|
| `<PKG_ID>` | StartOS package identifier | `youtube-summarizer` |
| `<APP_PORT>` | Internal container port | `3001` |
| `<DATA_PATH>` | Persistent volume mount | `/data` |
| `<HEALTH_PATH>` | Health endpoint path | `/api/health` |
| `<PROJECT_ROOT>` | Absolute path to repo | `/Users/macpro/Projects/youtube-summarizer` |

---

*This document should be updated as the implementation progresses. Each phase completed should be annotated with actual outcomes, issues discovered, and any deviations from the plan.*
