Files
recap/START9_PACKAGING_GAMEPLAN.md
T
2026-04-09 15:03:31 -05:00

33 KiB

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
  2. What Changes for StartOS
  3. Challenge 1: Keeping yt-dlp Updated
  4. Challenge 2: YouTube Authentication on a Headless Server
  5. Challenge 3: Server IP Bot Detection
  6. StartOS 0.4.0 Packaging Structure
  7. Differences from v0.3.5 Packaging (Workout Log)
  8. Implementation Phases
  9. Persistent Data Contract
  10. Ongoing Maintenance Plan
  11. Risk Assessment
  12. Multi-User Distribution: API Keys and Clean Installs
  13. Appendix A: File-by-File Packaging Checklist
  14. 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:

#!/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:

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

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

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

#!/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

# 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

# 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.