← index

ARCHITECTURE

docs/ARCHITECTURE.md

Arquitectura del sistema

Visión de cómo está organizado Metabolics.app a nivel de sistema. Para setup local, ver DEVELOPMENT.md. Para deploy, ver DEPLOY.md.

Diagrama de alto nivel

┌──────────────────────────────────────────────────────────────┐
│                    Browser / PWA / Mobile                    │
│              (Nutricionista o Paciente)                      │
└──────────────────────────┬───────────────────────────────────┘
                           │ HTTPS
                           ▼
┌──────────────────────────────────────────────────────────────┐
│                 Cloudflare (proxy + WAF)                    │
│         (DDoS, headers de seguridad, caché)                 │
└──────────────────────────┬───────────────────────────────────┘
                           │ HTTPS
                           ▼
┌──────────────────────────────────────────────────────────────┐
│                      Traefik (Coolify)                       │
│            Routing por Host + Let's Encrypt                  │
└──────────────────────────┬───────────────────────────────────┘
                           │
                           ▼
┌──────────────────────────────────────────────────────────────┐
│                SvelteKit (adapter-node)                      │
│  ┌─────────────────┬─────────────────┬──────────────────┐   │
│  │   (marketing)   │     (app)       │    (patient)     │   │
│  │  landing/pricing│   nutricionista │  app paciente    │   │
│  │   público       │  role=nutritionist│ role=patient   │   │
│  └────────┬────────┴────────┬────────┴────────┬─────────┘   │
│           │                 │                  │             │
│  ┌────────▼─────────────────▼──────────────────▼──────────┐ │
│  │         Capas compartidas                              │ │
│  │  • Hooks SSR (Supabase Auth)                           │ │
│  │  • Modules feature-based (9 módulos)                   │ │
│  │  • Services cross-cutting (auth, email)                │ │
│  │  • UI (shadcn-svelte + Tailwind 4)                     │ │
│  │  • Utils (cn, format)                                  │ │
│  └────────┬───────────────────────────────────────────────┘ │
└───────────┼─────────────────────────────────────────────────┘
            │ drizzle-orm
            ▼
┌──────────────────────────────────────────────────────────────┐
│            PostgreSQL (Supabase local / managed)             │
│  • 9 módulos mapeados 1:1 a tablas                          │
│  • auth.users (Supabase) + profiles (extensión)              │
│  • RLS policies (Supabase, separadas de Drizzle)             │
└──────────────────────────────────────────────────────────────┘
            │
            ▼
┌──────────────────────────────────────────────────────────────┐
│                  Servicios externos                          │
│  • Resend (email transaccional)                              │
│  • Umami (analytics self-hosted en subdomain)               │
│  • Paddle (v2.0, no MVP)                                    │
└──────────────────────────────────────────────────────────────┘

Las 3 interfaces (route groups)

SvelteKit route groups ((marketing), (app), (patient)) viven en la misma app, comparten Drizzle schema, pero tienen layouts y auth separados.

Group URL base Auth Layout
(marketing) /, /pricing, /about No Header + footer simple
(app) /dashboard, /patients, /meal-plans, /agenda, /payments, /chat, /gamification, /public-page, /settings Sí, role=nutritionist o admin Sidebar desktop + topbar
(patient) /home, /plan, /progress, /messages Sí, role=patient Topbar + bottom nav mobile-first

El gating se hace en cada +layout.server.ts del group, no en routes individuales.

Los 9 módulos

Ver MODULES.md para el detalle. Resumen:

# Módulo Carpeta Tablas clave
1 Gestión de pacientes modules/patients/ profiles, nutritionist_profiles, patients, anthropometric_measurements, consultations
2 Planificación alimentaria modules/meal-plans/ foods, food_categories, recipes, recipe_ingredients, meal_plan_templates, meal_plans, meal_plan_items
3 Agenda y gestión de citas modules/appointments/ appointments
4 Pagos y facturación modules/payments/ payments
5 Comunicación profesional–paciente modules/communication/ chat_messages
6 App del paciente (diario) modules/patient-app/ patient_meal_logs
7 Gamificación modules/gamification/ patient_streaks, patient_points, achievements, patient_achievements
8 Página web pública modules/public-pages/ professional_pages
9 Planes y acceso modules/subscriptions/ subscriptions

Cada módulo es feature-based y autocontenido: components/, services/, schema.ts (Zod), types.ts, index.ts.

Flujo de datos

Lectura (queries)

Componente .svelte
    → $modules/<x>/services/<x>.service.ts
    → $server/db (cliente Drizzle)
    → PostgreSQL
    ← row data tipado
    ← DTO (mapeado a types.ts del módulo)

Los componentes nunca importan $server/db directamente. Siempre van por el service del módulo.

Escritura (mutations)

Form (sveltekit-superforms)
    → action en +page.server.ts
    → schema Zod (mismo del módulo) valida
    → service mutation
    → Drizzle INSERT/UPDATE/DELETE
    → redirect o return fail(...)

Auth

Request llega
    → hooks.server.ts (sequence: supabase, sessionHydrate)
    → locals.session, locals.user, locals.supabase
    → (app)/+layout.server.ts o (patient)/+layout.server.ts chequean rol
    → si OK: locals.user extendido a AppUser (con profile row)

Decisiones de arquitectura clave

Drizzle > Supabase client para queries

  • Type-safety end-to-end: el schema es TypeScript, las queries heredan tipos.
  • Menos acoplamiento: si migramos de Supabase, la lógica de queries no cambia.
  • RLS no bloquea: como usamos Drizzle con conexión directa a Postgres, no necesitamos que Supabase haga cumplir RLS para queries — pero sí lo mantenemos para defensa en profundidad y para queries vía PostgREST si las necesitamos.
  • Trade-off: perdemos algunas features del Supabase client (realtime subscriptions) — agregar @supabase/supabase-js directo si hace falta.

Supabase solo para auth + storage + admin

  • @supabase/ssr maneja la sesión en cookies.
  • El service-role client (supabase-admin.ts) solo se usa para bypass de RLS en admin tasks (ej. signup cleanup, admin operations).
  • Storage de archivos (PDFs generados, fotos) usa Supabase Storage en el futuro.

Single schema file

Decisión deliberada para el MVP. Con ~25 tablas es manejable en un solo archivo. Si crece a >50 tablas, evaluar split por dominio (no por tabla).

Modular feature-based

Cada módulo tiene su propio services/ y components/. Esto previene que src/lib/components/ se vuelva un cajón de sastre. Los componentes que se comparten entre módulos viven en src/lib/components/ui/ (shadcn-svelte).

Tailwind 4 CSS-based config

Sin tailwind.config.ts con JS — todo el theme va en CSS con @theme. Más simple, mejor integración con CSS variables que shadcn-svelte necesita para los colores.

adapter-node para Coolify

Mismo patrón que finanple.app. Output a build/, Traefik + Coolify manejan el routing, Let's Encrypt automático.

Lo que NO está en la arquitectura (todavía)

  • No hay backend separado. Todo vive en SvelteKit endpoints (+server.ts files o actions). Si crece mucho, considerar extraer a Hono o similar.
  • No hay queue system. Tareas async (envío de emails, generación de PDFs) se hacen sincrónicamente por ahora. Migrar a BullMQ/Cron cuando sea necesario.
  • No hay caching layer. Si el dashboard se vuelve lento, agregar Redis (que ya viene con Coolify).
  • No hay CDN para assets. Cloudflare proxy cachea el HTML, los assets se sirven desde Coolify. Si crecen, migrar a R2.
  • No hay service worker / PWA manifest todavía. Para "app del paciente" en MVP, el responsive + "Add to home screen" es suficiente. Service worker viene cuando agreguemos offline.