An MVP learning-resource aggregator for sport and training skills. It includes:
- Supabase schema, RLS, multi-sport seed taxonomy, local collection, cron cleanup, and Edge Functions.
- Next.js public SEO pages and an admin moderation queue.
- Expo mobile app with browsing, level filters, saved resources, and completed state.
- Public link suggestions with contributor attribution across web and mobile.
- Shared Zod schemas, prompt templates, and tests for the suggestion pipeline.
The local Postgres volume carries all agent-collected content (currently ~70 links / ~160 link-skill relations across all categories). It is NOT a clean repro from seed.sql — that file only seeds categories, skills, and trusted_sources. If you wipe the DB you lose everything the nightly collection has gathered.
Before running any of the following, dump first:
npx supabase db resetsupabase db resetdocker volume rm supabase_db_skillsaggregatordocker compose down -vagainst the supabase stack- Any new migration that
drops tables, columns, or constraints carrying live data
Backup command (always safe to run, fast, idempotent):
scripts/db-backup.shUse npm run db:migrate:safe instead of bare supabase db reset when applying local migrations.
If the catalog gets wiped anyway, the .collection/logs/nightly-*.log files preserve every candidate_scored + suggestion_submitted event from past nightly runs (R20 design). Run npm run db:replay-logs to rebuild — takes ~5 seconds, idempotent, no LLM/YouTube calls needed.
The production collection path is now Option A: the collector still runs on the local machine, but writes directly to hosted Supabase (vqxsaabskkkjdljxiyqi) as the single source of truth. Local Supabase remains for development.
- Hosted nightly collection sources local tuning from
apps/web/.env.local, then hosted credentials from.env.hosted. - Direct SQL reads/writes use hosted Postgres through
COLLECT_DB_URL(Supabase session pooler) when set; otherwise the collector falls back to the localsupabase_db_skillsaggregatorcontainer. - Hosted runs default
COLLECT_SKIP_EVENT_PERSIST=1: full JSON logs stay in.collection/logs, smallagent_runsrows remain in Postgres, and the noisyagent_run_eventstable stays empty. - Collection scripts gather resources and POST them to
submit-suggestionwithrequested_status: "auto_approved"when the internal score passes. submit-suggestionstores public and unauthenticated requests aspending;auto_approvedis honored only for internal requests carryingx-internal-tokenthat matchesINTERNAL_FUNCTION_TOKEN.- Moderators apply accepted suggestions through
apply-suggestion, which writes links and relations in Postgres. Hostedapply-suggestionis deployed with gateway JWT verification disabled and protected by the same internal-token guard. - The
notify_revalidation()trigger posts directly to the Vercel/api/revalidateroute using Vaultrevalidate_urlandrevalidate_secret; there is no separaterevalidate-webEdge Function. - Cloud collection functions
link-searcher,link-checker, andtriangulateare dormant for this phase. They stay in the repo for a future deployed-agent mode, but migration0006_disable_cron.sqlunschedules their automatic cron jobs.
To switch back to deployed-agent collection later, re-enable the cron jobs, deploy the dormant functions, configure live model/API secrets, and keep the server-side moderation policy explicit.
Collection rate-limit tuning + escalation playbook lives at docs/collection-tuning.md. apps/web/.env.local is gitignored, so that doc is the canonical record of what's currently configured and why.
npm install
npm run dev:web
npm run dev:mobile- Node.js 20.11+ and npm 10+.
- Docker Desktop running before
supabase start. - Supabase CLI:
brew install supabase/tap/supabase. - Hosted collection direct SQL:
brew install libpq(scripts/nightly-collect.shadds Homebrew's keg-onlylibpq/binpath). yt-dlp:brew install yt-dlpfor the video collector workstream.- Ollama with a local scoring model:
ollama pull qwen2.5:7b.
Copy .env.example values into the relevant app and Supabase environments.
NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEYSUPABASE_SERVICE_ROLE_KEYINTERNAL_FUNCTION_TOKEN(required for hosted/adminapply-suggestionand internalauto_approvedsubmit-suggestion calls)COLLECT_TARGET(hostedfor nightly production,localfor local Supabase dev)COLLECT_DB_URL(hosted Postgres/session-pooler URL; for this project the working host isaws-1-ap-southeast-2.pooler.supabase.com)SUPABASE_DB_PASSWORD(optional fallback; collector can deriveCOLLECT_DB_URLfrom it plus hostedSUPABASE_URL)COLLECT_SKIP_EVENT_PERSIST(defaults to1for hosted,0for local)NEXT_PUBLIC_BASE_URLBASE_URLALLOWED_ORIGINSSUGGEST_TURNSTILE_SITE_KEY(optional public flag for the suggest form)SUGGEST_TURNSTILE_SECRET_KEY(required by the Edge Function when Turnstile is enabled)SUPABASE_AUTH_GOOGLE_CLIENT_ID/SUPABASE_AUTH_GOOGLE_SECRETfor local Google OAuthEXPO_PUBLIC_WEB_BASE_URLfor mobile profile linksDEMO_MODE(set to1only for local demo without Supabase)REVALIDATE_SECRETSUPABASE_FUNCTIONS_URLOLLAMA_BASE_URLOLLAMA_MODELANTHROPIC_API_KEYOPENAI_API_KEYPERPLEXITY_API_KEYYOUTUBE_API_KEY
- Create a Supabase project, copy the URL, anon key, and service role key into
.env.local, and keepDEMO_MODEunset for any real environment. - Run the migrations and seed locally with
npm run db:migrate:safe. - Add moderators in both Supabase Auth and
public.moderators; admin access is fail-closed and no longer trustsMODERATOR_EMAILSor user-editable auth metadata. Public signup is enabled for contributor profiles, but moderator access still requires the allowlist row. The helper does both:
npm run add:moderator -- --email you@example.comManual equivalent:
insert into public.moderators (email)
values ('you@example.com')
on conflict (email) do update set is_active = true;Also create the matching Auth user with email_confirm: true through the Admin API:
curl -X POST "$SUPABASE_URL/auth/v1/admin/users" \
-H "apikey: $SUPABASE_SERVICE_ROLE_KEY" \
-H "Authorization: Bearer $SUPABASE_SERVICE_ROLE_KEY" \
-H "Content-Type: application/json" \
-d '{"email":"you@example.com","email_confirm":true}'- Store cron and webhook secrets in Supabase Vault, not database GUCs:
select vault.create_secret('https://YOUR-PROJECT.supabase.co/functions/v1', 'supabase_functions_url');
select vault.create_secret('YOUR_SERVICE_ROLE_KEY', 'service_role_key');
select vault.create_secret('https://YOUR_WEB_ORIGIN/api/revalidate', 'revalidate_url');
select vault.create_secret('YOUR_REVALIDATE_SECRET', 'revalidate_secret');- Configure public auth providers when using contributor login outside the local email flow. Google OAuth needs matching redirect URLs in Google Cloud and Supabase:
http://localhost:3000/auth/callback
skillsaggregator://auth/callback
https://YOUR-WEB-ORIGIN/auth/callback
- For future deployed-agent mode, set Edge Function secrets:
supabase secrets set \
ANTHROPIC_API_KEY=... \
OPENAI_API_KEY=... \
PERPLEXITY_API_KEY=... \
YOUTUBE_API_KEY=...supabase start
npm run db:migrate:safe
supabase functions serveDeploy functions after configuring secrets:
supabase functions deploy link-searcher
supabase functions deploy link-checker
supabase functions deploy submit-suggestion
supabase functions deploy apply-suggestion
supabase functions deploy triangulatelink-searcher, link-checker, and triangulate are dormant in the local-first mode; deploying them is only needed when testing the future cloud-agent path.
- Start Docker Desktop.
- Install tools if needed:
brew install supabase/tap/supabase
brew install yt-dlp- Start and seed local Supabase:
supabase start
npm run db:migrate:safe- Copy the local keys printed by
supabase startinto.env.local:
NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321
NEXT_PUBLIC_SUPABASE_ANON_KEY=<local anon key>
SUPABASE_URL=http://127.0.0.1:54321
SUPABASE_SERVICE_ROLE_KEY=<local service role key>
SUPABASE_FUNCTIONS_URL=http://127.0.0.1:54321/functions/v1
NEXT_PUBLIC_BASE_URL=http://localhost:3000
BASE_URL=http://localhost:3000
ALLOWED_ORIGINS=http://localhost:3000
REVALIDATE_SECRET=local-dev-secret
EXPO_PUBLIC_WEB_BASE_URL=http://localhost:3000- Add a moderator in Auth and the moderator table:
npm run add:moderator -- --email you@example.comThe SQL-only table row is not enough for admin access; the matching auth.users row must exist before magic-link OTP can be sent.
insert into public.moderators (email)
values ('you@example.com')
on conflict (email) do update set is_active = true;- Serve local functions and the web app:
supabase functions serve --env-file .env.local
npm run dev:web- Run article collection against local Supabase:
ollama pull qwen2.5:7b
npm run collect:articles -- --dry-run --max-per-domain 2 --skill forehand-clear
npm run collect:articlesVideo collection can target one category across the seeded taxonomy:
node scripts/run-collection.mjs --category padel --all
node scripts/run-collection.mjs --category surfing --skill pop-upHosted nightly collection is wrapped by target-specific aliases:
npm run collect:hosted:smoke
npm run collect:hosted
npm run collect:localscripts/nightly-collect.sh defaults to COLLECT_TARGET=hosted. It sources apps/web/.env.local first for tuning and INTERNAL_FUNCTION_TOKEN, then .env.hosted so hosted SUPABASE_URL, service role, and COLLECT_DB_URL win. Use COLLECT_TARGET=local when you intentionally want the old local-container path.
Weekly source discovery expands the trusted source graph before nightly collection:
npm run discover:sources -- --category padel
npm run discover:sourcesIt uses PERPLEXITY_API_KEY for candidate discovery, validates YouTube channels with
yt-dlp, auto-trusts high-confidence sources, and sends borderline SOURCE_ADD
suggestions to the moderation queue.
- Open
/admin/loginand authenticate with an allowlisted moderator email. - Run local collection, then open the admin moderation queue.
- Approve a pending suggestion and confirm the public skill page updates.
- Open the Expo app, browse the same skill, save a resource, and mark it completed.
The external hypothesis checks in docs/hypothesis_validation.md require live API keys and network access. Run:
npm run h1
npm run h2
npm run h3
npm run h4Each command writes structured artifacts under .validation/ and appends a row to docs/hypothesis_validation.md.
Deferred coverage still includes browser E2E for the admin queue, Maestro mobile flows, and live LLM regression checks; the database tests now cover RLS, dedupe, and apply-suggestion link add/detach behavior.