DeleteUser permanently removes a user and all owned data, scoped via withUser. The users-row ON DELETE CASCADE reaches videos, transcripts, summaries → sink_deliveries, video_connections, and the user_identities map (cascades bypass RLS, so a scoped connection still wipes child rows). summary_actions carries user_id but has NO FK to users (migration 002), so it is deleted explicitly in the same scoped transaction. Idempotent. Tapir-side only (decision 2026-06-03): Dex identity is left untouched; secrets live in the SecretStore and are removed by the account handler. DisplayName returns the registered name for the account page. Test proves deletion removes every row for the target user across all isolated tables (incl. video_connections AND user_identities) and leaves another user's rows fully intact. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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
.featurefiles 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 own YouTube account: 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. Unset TAPIR_POLL_INTERVAL = single pass; set it (e.g. 15m) to 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).
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.