mathiasandClaude Opus 4.8 72cb25111f
CI / Lint / Test / Vet (push) Successful in 12s
CI / Build & Import (push) Successful in 12s
test(store): address migrations by version, not step count (#8)
The up/down migration tests stepped a hard-coded number of Steps(-N)/Steps(+N)
down from HEAD and back. The counts assumed a specific latest migration, so
adding one shifted every count by one and unrelated tests (010/011/014) went
red with confusing off-by-one symptoms — a papercut on every new migration.

Drive the schema to an exact version with m.Migrate(version) via two helpers
(headVersion, migrateTo). Each test now steps to just below its target by
version, asserts the down effect, steps up to the target, asserts the up
effect, then restores to the captured HEAD. A migration added on top changes
HEAD but shifts no count, so no test needs editing.

Verified by adding a throwaway migration 017 on top: all four tests stayed
green with zero edits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QbdxXWxLefS5AwLN5eyze
2026-07-02 14:49:02 +02:00

tapir

Watches a user's YouTube/Vimeo subscriptions and, when a subscribed channel posts a new video, summarizes it into highlights and takeaways using a local-first AI stack with an optional, per-user BYO-AI fallback. Standalone-first; feeding a personal knowledge base ("brain") is one optional sink, not the reason Tapir exists. Written in Go.

Status

Stage-0 demo slice built. The engine, ports, domain, and adapters (llm, summarizer, captions-first youtube, Postgres store) are implemented and green, with the tapir CLI (auth / run / list / show) wiring the end-to-end loop for a single user. The remaining work to open the Stage-0 clock is the live run on real subscriptions — see "Running the Stage-0 demo" below. The guardrail docs (vision, decisions, architecture, data model, behavior specs) remain the source of intent.

Read these first (the guardrails)

Doc What it is
VISION.md Product vision, principles, and the staged Definition of Success. Stage 0 ("useful to me") is the gate before any multi-user work.
DECISIONS.md Architecture Decision Records (append-only). Why Go, why no Supabase, standalone-first, captions-first, etc.
docs/architecture/architecture.md C4 context + container diagrams, key sequence diagrams, and the Clean Architecture layering (Mermaid).
docs/data-model.md Entities and the per-user isolation model (Stage 0 / Stage 1 scope).
docs/use-cases/ Gherkin .feature files — the BDD behavior spec that seeds the test suite.

Approach

  • Clean Architecture / ports & adapters. A provider- and sink-agnostic engine depends only on interfaces (VideoSource, Summarizer, Sink, SecretStore). YouTube, Vimeo, the AI router, the user store, and the brain sink are adapters. "Standalone vs homelab" is a wiring choice, not two codebases.
  • TDD/BDD. The .feature files are the living behavior spec; the use-case core is tested through fake adapters. Behavior is specified as executable scenarios, not prose that drifts.
  • Trunk-Based Development. Commit directly to main, one logical change per commit, every commit deployable (see ADR-009). CI is the quality gate.

Running the Stage-0 demo

Tapir runs on your YouTube account(s): authorize once, then run the watch→summarize→deliver loop. All configuration is via TAPIR_* environment variables — copy .env.example to .env and fill it in (no secrets are committed; at demo time source them from op, e.g. op run -- ...).

# 1. configure (UUID user id, gateway URL+key, Postgres DSN, YouTube OAuth app,
#    summarizer model). See .env.example for every variable.
cp .env.example .env && $EDITOR .env
set -a && . ./.env && set +a          # export them into the shell

go build -o bin/tapir ./cmd/tapir

# 2. one-time: authorize YouTube. Opens a consent URL, captures the redirect on
#    TAPIR_OAUTH_REDIRECT_ADDR, and stores the refresh token via the SecretStore
#    (a 0600 file at Stage 0). The token is never logged.
./bin/tapir auth

# 3. run: detect new videos across your subscriptions, summarize, deliver to the
#    store. Single pass; set TAPIR_DISCOVERY_INTERVAL (e.g. 2h) for the serve loop.
./bin/tapir run

Live prerequisites at demo time: the LiteLLM gateway reachable (TAPIR_GATEWAY_URL + a valid key — resolve from op, the documented sk-local-123 is stale), a Postgres DSN (TAPIR_DB_DSN, migrations apply on first connect), and a registered YouTube OAuth client whose authorized redirect URI matches TAPIR_OAUTH_REDIRECT_ADDR. The summarizer model (TAPIR_SUMMARIZER_MODEL, default koala/phi4-mini) is overridable; pick the final alias when the gateway is reachable (see docs/homelab-integration.md).

Web surface (tapir serve)

tapir serve starts the HTMX+Templ web UI on :8080. Users log in via Dex OIDC (local password or Google); a new Dex subject is routed to /register to create a Tapir account. Stage 1 is multi-user: each user connects their own YouTube account from the browser and manages their own summaries under DB-enforced RLS isolation. When TAPIR_DISCOVERY_INTERVAL is set (e.g. 2h), the serve process runs a scheduled discovery pass for every registered user automatically — no CronJob required. In auto mode only videos published within TAPIR_AUTO_SUMMARIZE_WINDOW (default ~7d, ADR-020) are summarised automatically; older videos are listed and summarised on demand, so a large back-catalogue doesn't keep re-driving the caption rate gate. See docs/homelab-integration.md for the full config reference.

Headless on koala

koala has no browser and no interactive op session, so the two interactive edges are handled without changing any code:

Secrets via an op service account (no op signin). Create a 1Password service account with read on the HomeLab vault, export its token, and keep one op-style env file tapir.env — secret values as op:// refs, the rest as literals — then prefix every command with op run:

export OP_SERVICE_ACCOUNT_TOKEN=ops_...        # the service-account token

cat > tapir.env <<'EOF'
TAPIR_USER_ID=<your-uuid>
TAPIR_GATEWAY_URL=http://koala:30401/v1
TAPIR_GATEWAY_KEY=op://HomeLab/LITELLM_MASTER_KEY/password
TAPIR_SUMMARIZER_MODEL=koala/phi4-mini
TAPIR_DB_DSN=op://HomeLab/TAPIR_DB_DSN/password
TAPIR_YT_CLIENT_ID=op://HomeLab/TAPIR_YT_OAUTH/client_id
TAPIR_YT_CLIENT_SECRET=op://HomeLab/TAPIR_YT_OAUTH/client_secret
TAPIR_YT_TOKEN_REF=youtube/refresh_token
TAPIR_SECRETS_FILE=/home/mathias/.config/tapir/secrets.json
TAPIR_OAUTH_REDIRECT_ADDR=localhost:8080
EOF
chmod 600 tapir.env

(Create the TAPIR_DB_DSN and TAPIR_YT_OAUTH items in the HomeLab vault first; LITELLM_MASTER_KEY already exists. sk-local-123 is stale.)

tapir auth over an SSH tunnel. The auth command binds a listener on localhost:8080 on koala and prints the consent URL to stdout — it never opens a browser. Forward that port to your laptop, run auth, and approve in your laptop's browser; the redirect returns through the tunnel:

# from your laptop:
ssh -L 8080:localhost:8080 koala
# now on koala (one-time):
op run --env-file tapir.env -- ./bin/tapir auth
#   → copy the printed URL into your laptop browser, approve; token is stored.

The Google OAuth client must be a Desktop/Web type with authorized redirect http://localhost:8080/callback (matching TAPIR_OAUTH_REDIRECT_ADDR), the YouTube Data API enabled, and your account added as a test user.

Everything else is already non-interactive — once the token is stored, run headless:

op run --env-file tapir.env -- ./bin/tapir run     # detect → summarize → deliver
op run --env-file tapir.env -- ./bin/tapir list     # read summaries back
op run --env-file tapir.env -- ./bin/tapir show <video-id>

Postgres (postgres18) is ClusterIP-only; from the koala host reach it with kubectl port-forward -n databases svc/postgres18 5432:5432 and point TAPIR_DB_DSN at localhost:5432 (migrations apply on first connect).

Conventions

Reuses homelab conventions: Go, Dex for identity, ESO + 1Password for secrets, Postgres for persistence. No new auth or secrets system (ADR-002).

Next

First implementation step: scaffold the Go service (engine + interfaces + the copied llm package), captions-first, with the store and brain sink adapters — decomposable into independent units suitable for a Claude Code swarm. Tracked as the first build issue.

S
Description
Watches a user's YouTube/Vimeo subscriptions and summarizes new videos (highlights + takeaways) via local-first AI with optional BYO-AI fallback. Standalone-first; brain is one optional sink. Go.
Readme
4.5 MiB
Languages
Go 91.3%
Python 4.5%
templ 4%
Dockerfile 0.2%