Arquitectura

Stack, decisiones de diseño, capas de la app
Sistema operativo

Fuego Social — Architecture Decision Record

Antes de tocar cualquier feature, leer también SECURITY.md y BRAND.md.
Esos dos documentos son ley. Si una decision tecnica los rompe, se rediseña.

What we're building

Two-sided marketplace for asados. Hosts publish seats, venues, events. Guests book and pay. Commission model (12%).

Stack

  • Frontend: Astro 4 (static build for staging, SSR for production)
  • Styling: CSS custom properties — NO Tailwind, NO framework. Pure tokens.
  • DB: PostgreSQL (local dev) → Supabase / managed Postgres (production)
  • API: Node 18 HTTP server, fuegosocial-hub/api.js — port 4501
  • Hub: fuegosocial-hub/serve.js — internal docs portal, port 4500
  • Auth: JWT access (15 min) + opaque refresh (30d, rotated). Google OAuth pending.
  • Payments: Stripe (global) + MercadoPago (LATAM). Webhook signatures verified.
  • Deploy: Cloudflare Tunnel (Zero Trust) → VM. Cloudflare Pages for static frontend.

Capas de la app (cada request las atraviesa en orden)

``

Cliente

→ Cloudflare (TLS, WAF, edge rate limit) [pendiente conectar]

→ Cloudflared tunnel

→ Hub serve.js (4500, docs) | API api.js (4501)

| 1. CORS allowlist

| 2. Security headers

| 3. Global rate limit (per IP)

| 4. Authentication (JWT)

| 5. Per-route rate limit

| 6. Body size cap + JSON parse

| 7. Zod validation

| 8. Authorization (ownership)

| 9. Parameterised SQL

| 10. Audit log

| → DB (PostgreSQL)

`

Cada capa esta en fuegosocial-hub/lib/:

  • env.js — config tipada desde .env
  • db.js — pool unico, transacciones (tx())
  • security.js — headers, CORS, rate limit, body parser, audit, helpers
  • auth.js — JWT, refresh rotation, password hash, login attempts
  • validators.js — schemas zod por endpoint

The rule that protects everything (branding)

No component may contain:

  • A hardcoded color (use var(--color-*))
  • A hardcoded brand name or text (use i18n keys)
  • A hardcoded number like commission rate (use brand.config.ts)

The rule that protects everything (security)

Ningun endpoint puede:

  • Leer credenciales de identidad (user_id, guest_id, host_id) desde el body
  • Saltarse requireAuth / requireAdmin en escrituras
  • Concatenar SQL en lugar de usar parametros
  • Devolver e.message en respuestas 5xx
  • Aceptar input sin un schema zod

Detalle completo: ver SECURITY.md.

Branding change cost

Change brand name: 1 line in brand.config.ts

Change palette: 1 section in brand.config.ts

Change tagline: 1 line in brand.config.ts

Time to rebrand completely: < 5 minutes + 1 build

Language

  • Default: Spanish (Argentine)
  • Supported: ES, EN
  • URL pattern: /en/* for English, /* for Spanish
  • Text source: i18n/es.ts and i18n/en.ts — nothing hardcoded in components

Listing types

  1. seat — individual seats at a private asado
  1. table — full table reservation
  1. venue — rent the full quincho/space
  1. event — public event with tickets (3 to 600 people)

Booking flow (Airbnb-derived)

  1. Guest selects listing → date → seats
  1. Pays (Stripe holds funds)
  1. Host confirms (or instant-book)
  1. Address revealed after confirmation
  1. Event happens
  1. Both parties leave review (24h window)
  1. Host payout (minus commission) released

Review system (bidirectional — like Airbnb)

Guest reviews host: food, fire technique, hospitality, cleanliness, value

Host reviews guest: punctuality, respect, social vibe, would_invite_again

Neither party sees the other's review until both have submitted (or 14 days pass)

Database tables

Core: users, listings, listing_dates, bookings, reviews, messages, sessions, saved_listings, platform_metrics

Security additions (migrations/001_security.sql): audit_log, rate_limits, api_keys, webhook_events, login_attempts. Plus columns on users (password_hash, google_sub, locked_until, mfa_*) and sessions (refresh_token_hash, revoked_at, rotated_to).

Pages

/ landing, /explorar browse, /asado/[slug] detail, /ser-anfitrion host onboarding,

/publicar create listing, /login, /registro, /cuenta dashboard, /reserva/[id] booking detail,

/perfil/[id], /mensajes, /valorar/[bookingId], /admin, /como-funciona, /404. Cada una replicada en /en/*.

Responsive strategy

Mobile-first. Three genuine layouts — not squeezed desktop.

Mobile < 768: single col, bottom nav, booking = modal

Tablet 768-1024: 2-col grid, sidebar booking

Desktop > 1024: 4-col grid, sticky 1/3 sidebar

Escala (roadmap explicito)

EtapaUsuariosDBCachingCosto

|---|---|---|---|---|

Hoy0 → 10kPostgreSQL en VM€0 extra
Tracción10k → 200kSupabase / Railway managedRedis (Upstash)€25-100/mes
Escala Airbnb200k → 1M+RDS Aurora + read replicasRedis cluster + CDN imagenes€2k-10k/mes

Migracion entre etapas: la pool en lib/db.js solo cambia su connection string. Toda la lógica del codigo se mantiene.

Pendientes alineados con la doctrina

Producto

  • Google OAuth (estructura ya en DB con google_sub)
  • Stripe + MercadoPago (estructura en DB con webhook_events)
  • i18n pass completo en dashboards

SEO & LLM Visibility — Doctrina

El modelo de Airbnb: no blog. El contenido lo genera la plataforma.

SEO programático (prioridad alta)

Cada combinación de ciudad + tipo + amenidad es una URL indexable generada automáticamente.

Ejemplos:

  • /explorar/buenos-aires/palermo — asados en Palermo
  • /explorar/tipo/bife-de-chorizo — por corte
  • /hosts/destacados/buenos-aires — top hosts por ciudad
  • /guias/como-organizar-un-asado — contenido evergreen embebido en producto

Regla: nunca blog externo. Las guías viven dentro de /guias/* como páginas Astro estáticas, indexables, con schema.org.

Schema.org (structured data)

Todo listing debe tener markup de:

  • Event o FoodEstablishment según tipo
  • AggregateRating con reviews reales
  • Offer con precio y disponibilidad
  • Person (host) con nombre y foto

Esto activa rich snippets en Google y es la señal más fuerte para LLMs que leen la web.

Sitemap & hreflang

  • sitemap.xml generado en build con todas las URLs (listings, guías, perfiles públicos)
  • hreflang ES/EN en cada página
  • Robots.txt que permite todo excepto /cuenta, /mensajes, /admin

Visibilidad en LLMs (ChatGPT, Perplexity, Claude)

Los LLMs no se optimizan como Google — se trabaja con PR digital:

  1. Conseguir menciones en medios argentinos / de eventos antes del launch
  1. Publicar en ProductHunt, HackerNews al lanzar
  1. README del repo con descripción clara del producto (LLMs indexan GitHub)
  1. Reviews reales de usuarios = contenido que terceros citarán
  1. Structured data bien implementado (Perplexity y SearchGPT lo leen)

Regla: cada feature nueva debe preguntarse — ¿genera una URL indexable? ¿tiene schema.org? ¿crea contenido que otros van a citar?

Seguridad (ver SECURITY.md backlog)

  • CSP sin unsafe-inline
  • MFA TOTP (estructura ya en DB)
  • Anonimizacion de IPs en audit_log a 90d
  • WAF rules en Cloudflare (cuando se conecte el dominio)
  • Pen test externo antes de salir publico

Infra

  • Cloudflare Tunnel para fuegosocial.com
  • Backups automatizados con retencion de 30d
  • Replica de audit_log a otra region

Admin Dashboard — Doctrina y Arquitectura

Acceso

  • URL: https://pompas.saypeter.com/hub/admin
  • Autenticacion: email + password → JWT en sessionStorage (se borra al cerrar el tab, mas seguro que localStorage)
  • Solo usuarios con is_admin = true en la DB pueden acceder
  • El frontend verifica admin: true en la respuesta del login — si no es admin, acceso denegado antes de llamar cualquier endpoint

API Admin (/api/admin/*)

Todos los endpoints:

  1. Llaman auth.requireAdmin(req) — falla con 401 si no hay JWT, falla con 403 si is_admin = false
  1. Rate limit dedicado: ip:{ip}:admin — 60 req/min (mas estricto que el global de 120)
  1. Audit log en todas las mutaciones (admin.user.update, admin.listing.update)
  1. Nunca filtran detalles de errores internos al cliente
EndpointMetodoDescripcion

|---|---|---|

/api/admin/statsGETResumen: usuarios, listings, reservas, GMV, comision
/api/admin/usersGETLista paginada con busqueda (q=)
/api/admin/users/:idPATCHToggle is_active / is_admin
/api/admin/listingsGETTodos los listings (todos los estados) con filtros
/api/admin/listings/:idPATCHCambiar status (published/paused/archived)
/api/admin/bookingsGETTodas las reservas con filtro por status
/api/admin/auditGETAudit log con filtro por accion

Reglas de seguridad para toda feature admin nueva

  • [ ] requireAdmin en el handler — nunca solo requireAuth
  • [ ] Rate limit con bucket admin, no el global
  • [ ] Toda mutacion genera entrada en audit_log
  • [ ] El actor_id` siempre viene del JWT, nunca del body
  • [ ] Nunca exponer password_hash, mfa_secret, reset_token en responses
  • [ ] Paginacion maxima: 200 registros por request