Ir al contenido

08 · Architecture

Versión: 0.1 Última actualización: 2026-04-23 Status: 🟡 Draft

Stack + estructura técnica de Academia Agentes. Referencia las decisiones tomadas antes (Astro + Svelte + Supabase por razones discutidas en conversación de diseño).


  • Astro para content-heavy rendering (docs, lessons)
  • Svelte para islands interactivos (player, quiz, dashboard)
  • Tailwind CSS para styling
  • shadcn-svelte o componentes custom para UI primitives

Rationale (ver conversación docs-site):

  • App es 80% contenido + 20% interactividad → Astro es ideal
  • Islands arquitectura = ship JS solo donde hace falta
  • Performance brutal (sub-1s TTI) importante para mobile
  • Costo DX: aprender Astro + Svelte (~4 días curva, OK tradeoff)
  • PostgreSQL managed
  • Auth con Google SSO + email (para Luis inicial, multi-tenant ready)
  • Storage para small assets (pero audio en R2)
  • Realtime subscriptions si hace falta para progreso tiempo real (nice-to-have)
  • Row-level security (RLS) desde día 1

Rationale:

  • Ya conocido por el equipo (Sistemia, Plania lo usan)
  • Auth sin código
  • Costos predecibles
  • SQL en vez de NoSQL weirdness
  • CF Pages para Astro site + app
  • CF R2 para audio files (cheaper than S3, sin egress fees)
  • CF Workers (si necesitamos edge logic) — opcional
  • CF Access para privatización selectiva
  • Claude Code CLI con Max OAuth (ya setup)
  • claude -p en print mode para automation
  • --max-budget-usd como safety net
  • Generación de: lesson script, quiz, exercise, transcript
  • gemini-2.5-pro-preview-tts multi-speaker (primary)
  • gemini-2.5-flash-tts (fallback + cheap layers)
  • Same tooling que Guerra de Tokens (reuso de scripts)
  • Evaluators como skills en apps/web/src/skills/evaluators/
  • Invocados por pipeline o por web app UI
  • Cada exercise/capstone tiene su evaluator prompt

academia-agentes/
├── docs/ ← Markdown source (esta carpeta)
├── docs-site/ ← Astro + Starlight renderer de docs
│ └── [ya existe, deployed]
├── apps/
│ ├── web/ ← App principal (Astro + Svelte)
│ │ ├── src/
│ │ │ ├── pages/
│ │ │ ├── components/ ← .svelte components
│ │ │ ├── layouts/
│ │ │ ├── content/ ← Astro content collections (lessons)
│ │ │ ├── skills/ ← evaluator agents, helpers
│ │ │ └── styles/
│ │ ├── astro.config.mjs
│ │ └── package.json
│ │
│ └── api/ ← CF Worker for webhooks, scheduled, etc.
│ └── src/
├── content/ ← Lesson source markdown (canonical)
│ ├── courses/
│ │ ├── b0-bridge/
│ │ │ ├── course.yaml ← metadata
│ │ │ ├── lesson-01/
│ │ │ │ ├── source.md ← content source
│ │ │ │ ├── script.txt ← TTS-ready
│ │ │ │ ├── audio.mp3 ← generated
│ │ │ │ ├── transcript.vtt
│ │ │ │ ├── slides.json
│ │ │ │ ├── quiz.json
│ │ │ │ ├── exercise.md
│ │ │ │ └── evaluator.md
│ │ │ └── ...
│ │ └── b1-claude-code-pro/
│ │ └── ...
│ └── shared/
│ ├── voices/ ← voice configs
│ ├── stings/ ← audio transitions
│ └── templates/ ← cert HTML templates
├── pipeline/ ← Scripts de generación
│ ├── generate_lesson.sh
│ ├── build_audio.py
│ ├── build_slides.py
│ ├── publish_lesson.py
│ └── refresh_cycle.py ← monthly check vs sources
├── supabase/ ← DB migrations + seeds
│ ├── migrations/
│ ├── seed.sql
│ └── config.toml
└── scripts/
├── setup-local.sh
└── deploy.sh

-- Extends Supabase auth.users
users_profile (
id UUID PRIMARY KEY REFERENCES auth.users(id),
display_name TEXT,
username TEXT UNIQUE,
email TEXT,
preferred_name TEXT,
avatar_url TEXT,
bio TEXT,
public_profile BOOL DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
)
user_placement (
user_id UUID PRIMARY KEY REFERENCES users_profile,
lane_primary TEXT, -- 'builder', 'operator', 'cross'
domain TEXT, -- 'legal', 'finance', etc. (for operator)
experience_level TEXT,
claude_code_level TEXT,
time_per_day_min INT,
preferred_modality TEXT,
primary_goal TEXT,
has_project BOOL,
wants_certificates BOOL,
placement_completed_at TIMESTAMPTZ,
placement_version TEXT
)
courses (
id TEXT PRIMARY KEY, -- 'b1', 'o1-g', 'o1-l', etc.
title TEXT NOT NULL,
lane TEXT, -- 'bridge', 'builder', 'operator', 'cross'
variant TEXT, -- 'generic', 'legal', 'finance', etc.
level TEXT, -- 'entry', 'intermediate', 'advanced'
duration_weeks INT,
lessons_count INT,
description TEXT,
status TEXT, -- 'planned', 'in_production', 'published', 'archived'
published_at TIMESTAMPTZ,
last_refreshed_at TIMESTAMPTZ,
content_version TEXT
)
lessons (
id TEXT PRIMARY KEY, -- 'b1-l1', 'b1-l2', etc.
course_id TEXT REFERENCES courses,
order_index INT,
title TEXT,
objective TEXT,
duration_minutes INT,
audio_url TEXT, -- R2 URL
transcript_url TEXT,
slides_url TEXT, -- JSON file
source_md_path TEXT, -- repo path
published_at TIMESTAMPTZ,
last_updated_at TIMESTAMPTZ
)
paths (
id TEXT PRIMARY KEY, -- 'b-i', 'o-i', 'c-i', etc.
title TEXT,
lane TEXT,
level TEXT,
required_courses TEXT[], -- ['b0', 'b1', 'b3', 'b4']
description TEXT,
status TEXT
)
user_progress (
user_id UUID REFERENCES users_profile,
lesson_id TEXT REFERENCES lessons,
audio_consumed BOOL DEFAULT false,
audio_consumed_at TIMESTAMPTZ,
quiz_passed BOOL DEFAULT false,
exercise_submitted BOOL DEFAULT false,
exercise_score FLOAT,
lesson_complete BOOL DEFAULT false,
notes TEXT, -- learner's personal notes
updated_at TIMESTAMPTZ DEFAULT now(),
PRIMARY KEY (user_id, lesson_id)
)
user_enrollment (
user_id UUID REFERENCES users_profile,
course_id TEXT REFERENCES courses,
enrolled_at TIMESTAMPTZ DEFAULT now(),
completed_at TIMESTAMPTZ,
paused BOOL DEFAULT false,
PRIMARY KEY (user_id, course_id)
)
quiz_attempts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users_profile,
lesson_id TEXT REFERENCES lessons,
question_id TEXT,
answer TEXT,
correct BOOL,
attempt_number INT,
created_at TIMESTAMPTZ DEFAULT now()
)
exercise_submissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users_profile,
lesson_id TEXT REFERENCES lessons,
submission_type TEXT, -- 'file', 'commit', 'text', etc.
submission_data JSONB, -- flexible
score FLOAT,
feedback TEXT,
evaluator_version TEXT,
manual_reviewed BOOL DEFAULT false,
submitted_at TIMESTAMPTZ DEFAULT now(),
graded_at TIMESTAMPTZ
)
capstone_submissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users_profile,
course_id TEXT REFERENCES courses,
repo_url TEXT,
writeup TEXT,
self_assessment JSONB,
agent_scores JSONB, -- 4-axis
agent_feedback TEXT,
final_score FLOAT,
passed BOOL,
manual_reviewed BOOL,
reviewer_notes TEXT,
submitted_at TIMESTAMPTZ DEFAULT now(),
graded_at TIMESTAMPTZ
)
-- Ya detallado en 14-credentials-system.md
-- Resumen:
certificates (
id UUID PRIMARY KEY,
user_id UUID REFERENCES users_profile,
type TEXT, -- 'micro' | 'path' | 'mastery'
course_id TEXT, -- for micro
path_id TEXT, -- for path
lane_id TEXT, -- for mastery
issued_at TIMESTAMPTZ,
scores JSONB,
artifacts JSONB,
verification_slug TEXT UNIQUE,
pdf_url TEXT,
badge_url TEXT,
status TEXT DEFAULT 'active'
)

┌─────────────────────────────────────────────────────────────┐
│ 1. fetch_sources.py │
│ Reads: curriculum.yaml (qué lesson toca hoy) │
│ Pulls: sources listed for that lesson │
│ Output: content/{course}/{lesson}/sources/ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 2. generate_lesson.sh │
│ Runs: claude -p with Max OAuth + --max-budget-usd 5 │
│ Input: sources + principles.md + audio-formats.md │
│ Output: source.md, script.txt, quiz.json, exercise.md, │
│ evaluator.md, slides.json │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 3. build_audio.py │
│ Input: script.txt │
│ TTS: Gemini multi-speaker (Maestro + Chombi voices) │
│ Output: audio.mp3 (16-bit mp3, 128kbps) │
│ Post: normalize to -16 LUFS, add intro/outro stings │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 4. build_slides.py │
│ Input: slides.json (timestamps + display types) │
│ Generates: static SVG diagrams (mermaid), syntax-hl code │
│ Output: slides/*.svg │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 5. publish_lesson.py │
│ Uploads: audio → R2, slides → R2 │
│ Writes: lessons row in Supabase (with URLs) │
│ Triggers: re-build de Astro app │
│ Notifies: learners subscribed (email + telegram) │
└─────────────────────────────────────────────────────────────┘
  • Daily 5 AM PDT: generate lesson del día (si curriculum tiene una)
  • Weekly Sunday 6 AM: generate Weekly Synthesis
  • Monthly 1st 4 AM: refresh_cycle (check lessons vs current docs)
  • On-demand: Changelog Express (triggered by tracked RSS/releases)
  • --max-budget-usd 5 en cada claude -p call
  • OAuth token vs API key (usa Max, no cobra)
  • Retry logic con exponential backoff
  • Failure notifications a Telegram del admin
  • If fail 3x consecutive: alert + pause cron

/ Dashboard (default post-login)
/onboarding Placement flow
/catalog Course catalog
/courses/:course_id Course overview
/courses/:course_id/lessons/:lesson_id Lesson player
/courses/:course_id/exercise/:lesson_id Exercise submission
/courses/:course_id/capstone Capstone submission
/certificates My certificates
/verify/:cert_slug Public verification (no auth)
/u/:username Public portfolio (if opted-in)
/profile Settings + placement re-take
  • <AudioPlayer> — audio + synced transcript + slides
  • <QuizPanel> — 5-question multiple choice with feedback
  • <ExerciseForm> — submission upload
  • <ProgressDashboard> — current course, streak, XP
  • <Certificate> — cert viewer + share actions

Rest de pages son estáticas (Astro) — solo islands donde hay state.


Aunque Luis es el primer user, arquitectura multi-user desde commit 1:

  • auth.users (Supabase) en vez de hardcoded user
  • ✅ Todas las tablas con user_id FK
  • ✅ Row-level security (RLS) policies
  • ✅ User-facing URLs usan user_id implícito (via session)
  • ✅ No shortcuts (“solo Luis” hardcoded anywhere)
-- Ejemplo: user_progress solo visible para el user o admin
CREATE POLICY "users see own progress" ON user_progress
FOR ALL USING (auth.uid() = user_id);
-- certificates: públicamente visibles (para verification URL)
CREATE POLICY "certificates public read" ON certificates
FOR SELECT USING (true);

EnvBranchURLPurpose
Localanylocalhost:4321Dev
PreviewPR branchesauto-generated CF Pages previewReview
Prodmainacademia-agentes.xyz (future)Live
  • git push main → auto-deploy via GitHub integration a CF Pages (once configured)
  • Supabase migrations: supabase db push manual para prod hasta que confirme
  • R2 bucket: 1 bucket per env (academia-agentes-prod, academia-agentes-staging)
  • .env.local (gitignored) para dev
  • CF Pages secrets UI para prod
  • Supabase has Vault for sensitive keys

  • CF Workers Analytics para API traffic
  • Supabase built-in logs para DB
  • Sentry (future) para error tracking
  • Plausible o Umami (no Google Analytics)
  • Eventos: lesson_started, lesson_completed, quiz_attempted, etc.
  • Monthly report: tokens usados, TTS usage, R2 storage, CF bandwidth
  • Alert si costo mensual > umbral

  • Docs site ✅ (live)
  • Content generation pipeline working
  • App web con 1 course funcional end-to-end (B1)
  • Auth con 1 user (Luis)
  • Capstone submission + evaluator
  • Placement system activo
  • 3-4 courses producidos
  • Certificates auto-emitted
  • Progress dashboard
  • Multi-user invite system
  • Public portfolios
  • Paths activos
  • Refresh cycle mensual corriendo
  • Cohort mode (si hay demand)
  • Community features (discussion, peer review)
  • Cross-device sync perfecto
  • Mobile apps (Capacitor wrapper probably)

  • ¿Convex en vez de Supabase para real-time? — por ahora Supabase
  • ¿Svelte 5 con runes stable? — sí, Svelte 5 released 2024
  • ¿Content collections de Astro o simple file system? — content collections
  • ¿Embed Claude Code diff/session viewer in evaluator feedback?
  • ¿Backup strategy para DB? — Supabase auto-backup + weekly export a R2