Fuego Social — Architecture Decision Record
Antes de tocar cualquier feature, leer tambiénSECURITY.mdyBRAND.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.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
- 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:
- Event
oFoodEstablishmentsegú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:
- 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 = 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:
- 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
- [ ] requireAdmin
en el handler — nunca solorequireAuth
- [ ] 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