Arquitectura Técnica
Stack y decisiones de diseño.
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/requireAdminen escrituras
- Concatenar SQL en lugar de usar parametros
- Devolver
e.messageen 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
- seat — individual seats at a private asado
- table — full table reservation
- venue — rent the full quincho/space
- event — public event with tickets (3 to 600 people)
Booking flow (Airbnb-derived)
- Guest selects listing → date → seats
- Pays (Stripe holds funds)
- Host confirms (or instant-book)
- Address revealed after confirmation
- Event happens
- Both parties leave review (24h window)
- 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)
| Etapa | Usuarios | DB | Caching | Costo |
|---|---|---|---|---|
| Hoy | 0 → 10k | PostgreSQL en VM | — | €0 extra |
| Tracción | 10k → 200k | Supabase / Railway managed | Redis (Upstash) | €25-100/mes |
| Escala Airbnb | 200k → 1M+ | RDS Aurora + read replicas | Redis 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:
EventoFoodEstablishmentsegún tipo
AggregateRatingcon reviews reales
Offercon 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.xmlgenerado en build con todas las URLs (listings, guías, perfiles públicos)
hreflangES/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:
- Conseguir menciones en medios argentinos / de eventos antes del launch
- Publicar en ProductHunt, HackerNews al lanzar
- README del repo con descripción clara del producto (LLMs indexan GitHub)
- Reviews reales de usuarios = contenido que terceros citarán
- 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 = trueen la DB pueden acceder
- El frontend verifica
admin: trueen la respuesta del login — si no es admin, acceso denegado antes de llamar cualquier endpoint
API Admin (/api/admin/*)
Todos los endpoints:
- Llaman
auth.requireAdmin(req)— falla con 401 si no hay JWT, falla con 403 siis_admin = false
- Rate limit dedicado:
ip:{ip}:admin— 60 req/min (mas estricto que el global de 120)
- Audit log en todas las mutaciones (
admin.user.update,admin.listing.update)
- Nunca filtran detalles de errores internos al cliente
| Endpoint | Metodo | Descripcion |
|---|---|---|
/api/admin/stats | GET | Resumen: usuarios, listings, reservas, GMV, comision |
/api/admin/users | GET | Lista paginada con busqueda (q=) |
/api/admin/users/:id | PATCH | Toggle is_active / is_admin |
/api/admin/listings | GET | Todos los listings (todos los estados) con filtros |
/api/admin/listings/:id | PATCH | Cambiar status (published/paused/archived) |
/api/admin/bookings | GET | Todas las reservas con filtro por status |
/api/admin/audit | GET | Audit log con filtro por accion |
Reglas de seguridad para toda feature admin nueva
- [ ]
requireAdminen el handler — nunca solorequireAuth
- [ ] Rate limit con bucket
admin, no el global
- [ ] Toda mutacion genera entrada en
audit_log
- [ ] El
actor_idsiempre viene del JWT, nunca del body
- [ ] Nunca exponer password_hash, mfa_secret, reset_token en responses
- [ ] Paginacion maxima: 200 registros por request