07 — Base de datos¶
Resumen¶
- Motor: PostgreSQL 15+ (probado en 17.5).
- Total tablas: 34 en schema
public(post-limpieza mayo 2026). - Tablas de producto Help Center + billing: ~20 (excl.
django_*,auth_*,token_blacklist_*). - Codificación: UTF-8.
- Convención de naming:
snake_case, plurales (bots,accounts,subscriptions). - Sin sufijo
_v3: los nombres en BD sontenant_frontend_content,knowledge_base, etc. (no*_v3).
Tablas core del SaaS¶
saas_users ←─────── (login al SaaS — el comprador y los miembros invitados)
│
│ 1:N invita / pertenece
▼
accounts ─────────► invoices (1:N por bot, cuenta agregada)
│
│ 1:N
▼
bots ─────────────┬────────► subscriptions (1:1 activa por bot)
│ │ │
│ │ ▼
│ │ plans (catálogo)
│ │
│ ├────────► tenant_backend_configs (1:1 extra JSON, lead email, flags)
│ │
│ ├────────► tenant_frontend_content (1:1 ui_config + content del widget)
│ │
│ ├────────► knowledge_categories (1:N categorías por bot)
│ │ │
│ │ ▼
│ │ knowledge_base (1:N FAQs por categoría)
│ │
│ └────────► bot_user_permissions (N:M con saas_users)
saas_users¶
Usuarios que se loguean al SaaS. Soporta login por email/password o por OAuth (Google).
id bigserial PK
email varchar UNIQUE NOT NULL
password varchar (hashed con Django auth)
first_name, last_name
is_active bool
is_verified bool (true tras verificar email)
account_id uuid FK → accounts.id (a qué account pertenece)
role varchar ('owner' | 'member' | 'superadmin')
created_at, updated_at
accounts¶
La cuenta de la empresa que compra el SaaS. Una Account agrupa N Bots.
id uuid PK (gen_random_uuid)
name varchar (nombre legal de la empresa)
email varchar UNIQUE
phone, company_name
country, state, city, address, postal_code, tax_id
stripe_customer_id varchar (customer.id de Stripe — uno por account)
content jsonb (metadata libre)
email_verification_data jsonb
closed_at, closed_by (soft-delete)
created_at, updated_at
bots¶
Cada Bot es un Help Center independiente con su propio slug, idiomas, FAQs, branding, plan y suscripción.
id uuid PK (gen_random_uuid)
account_id uuid FK → accounts.id
slug varchar UNIQUE (el identificador público, ej. 'acme-helpcenter')
name varchar
email varchar UNIQUE
domain varchar (dominio del cliente final, opcional)
plan_type varchar ('Free' | 'Starter' | 'Pro') — denormalizado para queries rápidas
logo_url, primary_color, secondary_color
phone, location
status varchar ('active' | 'paused' | 'deleted')
business_category_key, business_type_key
profile_name varchar (template de configuración inicial)
agent_role varchar ('general' | 'support' | ...)
extra jsonb (creation_progress, flags varios)
business_info jsonb
embed_snippet text (snippet HTML que el cliente pega en su web)
created_from_template_id varchar
stripe_customer_id varchar (puede heredar del account)
created_at, updated_at
plans¶
Catálogo de planes. Editable desde el panel /superadmin/.
id uuid PK
name varchar UNIQUE ('Free', 'Starter', 'Pro')
price numeric(10,2)
currency varchar(3) ('USD', 'EUR', ...)
billing_period varchar ('monthly' | 'yearly')
stripe_price_id varchar (vacío si gratis)
features jsonb (estructura: ver abajo)
translations jsonb ({ "es": {"name": "Pro", "description": "..."}, ... })
product_type varchar ('bot' por defecto)
is_active bool
created_at
Estructura canónica de features:
{
"limits": {
"faqs_per_language": 100,
"languages": 2,
"branding": true
},
"extras": ["email_support"]
}
El plan_policy_engine.py lee de aquí los limits que aplica al runtime. Más detalle en 11-policy-engine.md.
subscriptions¶
Estado Stripe de un bot. Hay como máximo 1 suscripción activa por bot por product_type.
id uuid PK
bot_id uuid FK → bots.id ON DELETE CASCADE
plan_id uuid FK → plans.id
bot_slug varchar (denormalizado para queries sin JOIN)
stripe_subscription_id varchar UNIQUE (sub_xxx de Stripe)
status varchar ('active' | 'trial' | 'past_due' | 'cancel_pending' | 'canceled')
current_period_start, current_period_end
trial_end
cancel_at_period_end bool
cancel_at, canceled_at
product_type varchar ('bot')
created_at, updated_at
UNIQUE(bot_id, product_type) WHERE status IN ('active','trial','past_due','cancel_pending')
invoices¶
Historial de facturas Stripe (espejo local para mostrar en el dashboard sin pegarle a la API de Stripe).
id uuid PK
bot_id uuid FK → bots.id
account_id uuid FK → accounts.id
stripe_invoice_id varchar UNIQUE
stripe_payment_intent_id varchar
amount_due, amount_paid numeric(10,2)
currency varchar(3)
status varchar ('paid' | 'open' | 'uncollectible' | 'void')
hosted_invoice_url text (URL pública de Stripe para verla/pagarla)
invoice_pdf text (URL de Stripe al PDF)
period_start, period_end
created_at, updated_at
tenant_backend_configs¶
Config operativa por bot (SSOT para extra JSON del Help Center).
id serial PK
bot_slug varchar UNIQUE
bot_name varchar
lead_notification_email varchar
extra jsonb (enabled_channels, brand_metadata, faq_verbatim, ...)
updated_at timestamp
Columnas legacy de era chatbot (system_prompts, conversation_logic, etc.) pueden existir en instalaciones antiguas; el runtime Help Center solo lee extra y lead_notification_email.
tenant_frontend_content¶
Contenido del widget para cada bot. Lo lee el endpoint público /v3/frontend/config.
id serial PK
bot_slug varchar UNIQUE (no es UUID — el widget pasa el slug)
ui_config jsonb (apiUrl, primaryColor, companyLogo, launcherPosition...)
content jsonb (texts, navItems, supported_languages, default_language, brand...)
updated_at
knowledge_categories¶
Categorías de FAQs por bot. La columna translations jsonb guarda los nombres traducidos.
id int PK
bot_id uuid FK → bots.id ON DELETE CASCADE
category_name varchar (interno: '<uuid>_<slug>_<nombre>')
display_name varchar (nombre limpio para mostrar)
description text
translations jsonb ({"en": "Pricing", "es": "Precios", ...})
bot_profile varchar
industry_specific bool
UNIQUE(bot_id, lower(category_name))
knowledge_base¶
Las FAQs (preguntas/respuestas) por bot, idioma y categoría.
id serial PK
bot_id uuid FK → bots.id ON DELETE CASCADE
category_id int FK → knowledge_categories.id
lang varchar(2) ('es' | 'en' | 'fr' | 'de' | 'pt')
question_text text NOT NULL
answer_text text NOT NULL
is_faq bool NOT NULL
embedding real[] (no usado en el demo; reservado para búsqueda semántica futura)
created_at, last_updated, bot_profile
bot_user_permissions¶
Quién puede operar qué bot. Permite invitar miembros con permisos granulares por bot.
id bigserial PK
saas_user_id bigint FK → saas_users.id
bot_id uuid FK → bots.id
permissions jsonb ({"faqs": "edit", "billing": "view", ...})
invited_by bigint FK → saas_users.id
created_at
Tablas auxiliares¶
| Tabla | Función |
|---|---|
prelaunch_codes |
Modo "invitation only" para early access |
invitation_requests |
Solicitudes pendientes de invitación |
social_auth_links |
Links a OAuth providers (Google) |
onboarding_intents |
Flow de onboarding antes de pagar |
stripe_webhook_events |
Idempotencia de webhooks Stripe |
billing_audit_log |
Audit log de cambios de plan/cancelaciones |
account_events |
Eventos de cuenta (creación, cierre, reactivación) |
websites |
Producto website (multi-producto Plan 2) |
users |
Usuarios de cuenta (owner, admin, bot_user) — login dashboard |
tenant_backend_configs |
Config JSON operativa por bot |
profile_configs |
Catálogo de perfiles del wizard |
business_categories, business_types |
Catálogo de industrias |
Inventario completo (34 tablas)¶
account_events, accounts, auth_group, auth_group_permissions, auth_permission,
billing_audit_log, bot_user_permissions, bots, business_categories, business_types,
django_admin_log, django_content_type, django_migrations, django_session,
invitation_requests, invoices, knowledge_base, knowledge_categories,
onboarding_intents, plans, prelaunch_codes, profile_configs, saas_users,
social_auth_links, stripe_webhook_events, subscriptions, system_settings,
system_settings_audit_log, tenant_backend_configs, tenant_frontend_content,
token_blacklist_blacklistedtoken, token_blacklist_outstandingtoken, users, websites
No quedan tablas de chat AI, flows, leads, bookings ni omnichannel en el schema entregable.
Queries útiles (operación)¶
-- Cuántos bots hay por plan
SELECT plan_type, COUNT(*) FROM bots WHERE status='active' GROUP BY plan_type;
-- FAQs por bot y por idioma
SELECT b.slug, kb.lang, COUNT(*) AS faqs
FROM knowledge_base kb
JOIN bots b ON b.id = kb.bot_id
GROUP BY b.slug, kb.lang
ORDER BY b.slug, kb.lang;
-- Suscripciones que vencen en los próximos 7 días
SELECT b.slug, s.status, s.current_period_end, p.name AS plan
FROM subscriptions s
JOIN bots b ON b.id = s.bot_id
JOIN plans p ON p.id = s.plan_id
WHERE s.current_period_end BETWEEN now() AND now() + interval '7 days'
ORDER BY s.current_period_end;
-- Ingresos del último mes (sumando facturas pagadas)
SELECT SUM(amount_paid)/100.0 AS revenue_usd
FROM invoices
WHERE status='paid'
AND created_at >= date_trunc('month', now() - interval '1 month')
AND created_at < date_trunc('month', now());
Índices¶
Los índices críticos los crean las migraciones. Si haces tuning manual, asegúrate de que existen:
bots(slug)— UNIQUEbots(account_id)subscriptions(bot_id, status)— paraapply_bot_filtersubscriptions(bot_slug)— partial WHERE not nullknowledge_base(bot_id)knowledge_categories(bot_id)tenant_frontend_content(bot_slug)— UNIQUE