01 — Arquitectura del sistema¶
Stack técnico (versiones reales en producción)¶
| Capa | Tecnología | Versión |
|---|---|---|
| Backend | Django + Django REST Framework | 4.2.7 / DRF 3.14.0 |
| Auth | djangorestframework-simplejwt | 5.3.0 |
| Base de datos | PostgreSQL | 15+ (probado en 17.5) |
| WSGI | Gunicorn | 21.2.0 |
| Reverse proxy | Nginx | 1.26+ |
| Frontend (cliente) | React + Vite + TypeScript + Tailwind | React 18, Vite 5 |
| Frontend (superadmin) | React + Vite + TypeScript + Tailwind | React 18, Vite 5 |
| Widget embebible | JS vanilla bundleado con Vite | — |
| Pagos | Stripe (Python SDK) | 7.4.0 |
| OS | Ubuntu 22.04 / 24.04 / Debian 12 | — |
| Python | 3.11 o 3.12 | — |
| Node | 18+ | — |
Decisión deliberada: no hay Celery, Redis, FastAPI ni microservicios. Un solo proceso Django sirve toda la API, el panel admin, el superadmin, los webhooks Stripe y los endpoints públicos del widget.
Servicios y puertos¶
| Servicio | Puerto | Comentario |
|---|---|---|
| Gunicorn (Django) | 8000 (loopback) | Único proceso de aplicación |
| PostgreSQL | 5432 (loopback) | Base principal |
| Nginx | 80 / 443 | Proxy + TLS + sirve estáticos |
No expongas el 8000 ni el 5432 a internet. Nginx es el único punto público.
Flujo de una request HTTP¶
┌──────────────────┐
│ Browser cliente │
└────────┬─────────┘
│ HTTPS
▼
┌────────────────────────────────────────────────┐
│ Nginx (vhost del SaaS) │
│ │
│ • /static/* → filesystem (alias) │
│ • /media/* → filesystem (uploads) │
│ • /v3/* → proxy_pass http://127.0.0.1:8000 │
│ • /api/* → proxy_pass http://127.0.0.1:8000 │
│ • /admin/* → proxy_pass http://127.0.0.1:8000 │
│ • / → sirve apps/dashboard/dist/ │
└────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Gunicorn (5 workers, 2 threads c/u) │
│ └── Django (blimx_admin.wsgi) │
│ ├── api/ │
│ │ ├── views.py (DashboardViewSet)│
│ │ ├── views_billing.py │
│ │ ├── views_superadmin.py │
│ │ └── widget/ ← endpoints públicos del chatbot │
│ └── authentication/ │
│ ├── jwt_auth.py │
│ └── views.py (login/register) │
└────────┬─────────────────────────────────┘
│
▼
┌─────────────────────┐ ┌──────────────────┐
│ PostgreSQL │◄─────►│ Stripe API │
│ (multi-tenant) │ │ (HTTPS saliente)│
└─────────────────────┘ └──────────────────┘
Modelo multi-tenant¶
┌──────────────┐
│ Account │ (cuenta de empresa que compra el SaaS)
│ (id UUID) │
└──────┬───────┘
│ 1:N
┌───────────────┼───────────────┐
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Bot │ │ Bot │ ... │ Bot │ (cada Bot = un Help Center)
│ slug= │ │ slug= │ │ slug= │
│ acme │ │ otro │ │ ... │
└───┬────┘ └────────┘ └────────┘
│ 1:N
├─────► KnowledgeCategory (categorías de FAQs por idioma)
├─────► KnowledgeBase (preguntas/respuestas)
├─────► TenantFrontendContent (ui_config + content del widget)
├─────► Subscription (estado Stripe del bot)
└─────► Invoice (historial de facturas)
- 1 Account representa al cliente final (la empresa que compra el plan).
- 1 Account → N Bots, donde cada Bot es un Help Center con su propio slug, dominio, idiomas, FAQs, plan, suscripción y branding.
- Slug del Bot (
bots.slug) es el identificador que se pasa al widget comodata-tenant.
Más detalle del schema en 07-base-datos.md.
Decisiones arquitectónicas¶
Por qué un solo backend Django¶
- Una sola unidad desplegable → un solo systemd unit, un solo venv, un solo nginx upstream.
- Migraciones nativas Django → cero scripts SQL custom, todo bajo
manage.py migrate. - JWT stateless → escala horizontal sin Redis ni sticky sessions.
- Stripe webhooks en el mismo proceso → no hay race conditions con la API principal.
Por qué no Celery¶
El SaaS entregado no genera trabajo background pesado. Los emails (verificación, reset, factura) se envían inline con EmailMessage.send(). Si el comprador necesita procesos async pesados (ej. importar 10k FAQs desde CSV), añadir Celery + Redis es trivial sin tocar el core.
Por qué Vite (no Webpack)¶
- Tiempo de build del dashboard: ~6 segundos.
- HMR instantáneo en desarrollo.
- Output ESM moderno; el bundle del widget pesa 24 KB minificado + gzip.
Por qué el widget se bundlea con Vite¶
El widget vive en static/v3/, importa 7 módulos JS (Launcher, Modal, FaqView, etc.). Sin bundle, el browser haría 20+ requests para abrirlo. Con node build.js se genera 1 JS + 1 CSS que el iframe carga.
Por qué bot_slug en vez de bot_id UUID en URLs públicas¶
- El slug es legible (
acme-helpcenter). - Lo controla el Account → puede branding-aware.
- El UUID se sigue usando internamente para FKs (más rápido).
Capa de seguridad¶
| Riesgo | Mitigación |
|---|---|
| SQL injection | ORM Django + queries parametrizadas en raw SQL |
| XSS en frontend | React escapa por defecto; nada de dangerouslySetInnerHTML con datos remotos |
CSRF en /api/ |
DRF con sesión deshabilitada → JWT stateless inmune |
| Secret leak | .env jamás se commitea (en .gitignore); SECRET_KEY distinto por entorno |
| Stripe webhook spoofing | Verificación de firma con stripe.Webhook.construct_event() |
| Brute force en login | django-ratelimit 4.1.0 (incluido en deps) |
| Permisos a archivos | /static/ servido vía alias con permisos restringidos al user nginx |
| HTTPS | Obligatorio en producción; Plesk emite cert Let's Encrypt automáticamente |