Saltar a contenido

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 son tenant_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) — UNIQUE
  • bots(account_id)
  • subscriptions(bot_id, status) — para apply_bot_filter
  • subscriptions(bot_slug) — partial WHERE not null
  • knowledge_base(bot_id)
  • knowledge_categories(bot_id)
  • tenant_frontend_content(bot_slug) — UNIQUE