# MaxEditor — SaaS / Companion App: Arquitectura

**Última actualización:** 25 abril 2026
**Estado:** Diseño aprobado, pendiente de implementación
**Documento relacionado:** [SAAS_WORKPLAN.md](SAAS_WORKPLAN.md)

---

## 1. Visión general

MaxEditor evoluciona de una app local monolítica (Django + React corriendo en `localhost`) a un sistema **híbrido cliente-servidor** con dos partes claramente separadas:

1. **Servidor central** en `altoque.tv/maxeditor` (HawkHost / cPanel)
   - Sirve la **interfaz** (React build estático)
   - Maneja **autenticación** y **suscripciones** (Stripe en fase pública)
   - Almacena **metadatos** de proyectos, secuencias, transcripts y configuración
   - Coordina jobs (estado, control, progreso)
   - **Nunca recibe archivos de video**

2. **Companion App** en la PC de cada usuario
   - Pequeño ejecutable Windows (`MaxEditor Setup.exe`, ~150 MB con FFmpeg)
   - Levanta un servidor local en `https://localhost:7432`
   - Ejecuta **todo el procesamiento de video** (FFmpeg, Whisper, OpenCV, torch)
   - Almacena los videos del usuario en su disco
   - Se comunica con el server para auth, metadatos y control de jobs

---

## 2. ¿Por qué este diseño?

| Beneficio | Cómo se logra |
|---|---|
| **Cero coste de procesamiento en el server** | FFmpeg corre en la máquina del cliente |
| **Privacidad total de los videos** | Los archivos nunca salen de la PC del usuario |
| **Actualizaciones instantáneas de UI** | Al subir un nuevo build al server, todos lo ven al recargar |
| **Lógica de negocio protegida** | IA, prompts y algoritmos viven solo en el server |
| **Control de licencias / pagos** | Sin login activo no carga el editor |
| **Escalable a miles de usuarios** | El server solo maneja JSON, no video |

---

## 3. Diagrama de capas

```
┌──────────────────────────────────────────────────────────────┐
│  altoque.tv (HawkHost / cPanel)                              │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  Frontend estático (React build)                       │  │
│  │  - Login / Signup                                      │  │
│  │  - Editor UI completo                                  │  │
│  │  - Settings (API keys, preferencias)                   │  │
│  └────────────────────────────────────────────────────────┘  │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  Django API (uWSGI + nginx)                            │  │
│  │  - /api/auth/  (login, register, refresh, me)          │  │
│  │  - /api/billing/  (Stripe — fase 2)                    │  │
│  │  - /api/editor/projects/...  (CRUD metadata)           │  │
│  │  - /api/editor/.../*-status/  (job status)             │  │
│  │  - /api/editor/.../*-control/  (pause/cancel)          │  │
│  │  - /api/editor/.../*-stream/  (SSE progress)           │  │
│  │  - /api/companion/version-check/                       │  │
│  └────────────────────────────────────────────────────────┘  │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  PostgreSQL (cPanel)                                   │  │
│  │  Users, Subscriptions, Projects, Clips, Transcripts,   │  │
│  │  Sequences, ExportJobs (metadata), AuditLogs           │  │
│  └────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────┘
                          ▲
                          │ HTTPS + JWT
                          ▼
┌──────────────────────────────────────────────────────────────┐
│  PC del usuario (Windows v1)                                 │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  Browser (Chrome/Edge) → altoque.tv/maxeditor         │  │
│  │  Carga UI + autentica + envía requests al server      │  │
│  └────────────────────────────────────────────────────────┘  │
│                          ▲                                    │
│                          │ HTTPS local (cert auto-firmado)    │
│                          ▼                                    │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  MaxEditor Companion (FastAPI + uvicorn local)        │  │
│  │  Puerto: 7432                                          │  │
│  │  Endpoints locales:                                    │  │
│  │  - /health                                             │  │
│  │  - /auth/token (DPAPI cifrado)                         │  │
│  │  - /file/pick, /file/save-dialog                       │  │
│  │  - /ffmpeg/probe                                       │  │
│  │  - /ffmpeg/extract-part                                │  │
│  │  - /ffmpeg/auto-frame                                  │  │
│  │  - /ffmpeg/clip-stabilization                          │  │
│  │  - /ffmpeg/sequence-clip-preparation                   │  │
│  │  - /ffmpeg/sequence-stabilization                      │  │
│  │  - /ffmpeg/sequence-audio-enhancement                  │  │
│  │  - /ffmpeg/export                                      │  │
│  │  - /ffmpeg/cancel                                      │  │
│  │  - /transcribe/audio (whisper local)                   │  │
│  │  - /preview/stream/{path}  (range-aware)               │  │
│  └────────────────────────────────────────────────────────┘  │
│                          ▲                                    │
│                          ▼                                    │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  Disco local del usuario                               │  │
│  │  C:\Users\<user>\MaxEditor\                            │  │
│  │  ├── videos\ (originales)                              │  │
│  │  ├── proxies\ (timeline media)                         │  │
│  │  ├── exports\                                          │  │
│  │  └── cache\                                            │  │
│  └────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────┘
```

---

## 4. Flujo de sesión completo

```
1. Usuario hace doble clic en MaxEditor.exe
2. Companion app:
   a. Verifica que FFmpeg está instalado (lo descarga si falta)
   b. Genera token de sesión aleatorio (32 bytes)
   c. Levanta FastAPI en https://localhost:7432
   d. Abre browser en https://altoque.tv/maxeditor

3. Frontend (en altoque.tv) al cargar:
   a. Llama a https://localhost:7432/health
      → Si falla: muestra "Instala MaxEditor companion" con link de descarga
      → Si OK: continúa
   b. Llama a https://localhost:7432/auth/token
      → Si hay JWT válido y no expirado: salta al editor
      → Si no: muestra login

4. Login:
   a. Usuario ingresa email + password
   b. POST altoque.tv/api/auth/login
   c. Server verifica credenciales + (fase 2) suscripción activa en Stripe
   d. Retorna JWT (24h)
   e. Frontend lo envía a localhost:7432/auth/token (cifrado con DPAPI)

5. Editor cargado:
   a. Frontend usa "API adapter" que decide:
      - Llamadas de metadata → altoque.tv/api/editor/...
      - Llamadas de FFmpeg → https://localhost:7432/ffmpeg/...
   b. Cada request al server lleva el JWT en header
   c. Cada request al companion lleva X-Companion-Token

6. Trabajo del usuario:
   - Importar video → companion lo guarda en C:\Users\<user>\MaxEditor\videos\
                    → server recibe solo metadata (path, fps, duración, hash)
   - Transcripción → companion corre whisper localmente
                    → server guarda los timings palabra por palabra
   - Auto-frame, stabilization, etc. → companion ejecuta FFmpeg
                                      → server guarda solo resultados / progreso
   - Export → companion corre FFmpeg final
            → archivo queda en C:\Users\<user>\MaxEditor\exports\

7. Cierre:
   - Companion detecta que el browser cerró (heartbeat)
   - Se queda en system tray (icono junto al reloj)
   - Próxima vez: abrir desde el tray reabre el browser
```

---

## 5. Separación de responsabilidades por endpoint

### 5.1 Endpoints que **se quedan en el server**

| Categoría | Endpoints |
|---|---|
| **Auth (nuevo)** | `/api/auth/login`, `/register`, `/refresh`, `/logout`, `/me` |
| **Billing (fase 2)** | `/api/billing/checkout`, `/portal`, `/webhook` |
| **Companion** | `/api/companion/version-check` |
| **Proyectos (CRUD)** | `/api/editor/projects/`, `/projects/<id>/`, `/bootstrap/` |
| **Clips/Secuencias** | `/clips/<id>/`, `/projects/<id>/sequence/` |
| **Transcripts (datos)** | `/projects/<id>/word-timings/`, `/transcription-status/` |
| **AI Suggestions** | Todos los `/generate-suggestion/`, `/refine-suggestion/`, `/depurate-sequence/` |
| **Job control** | Todos los `*-status/`, `*-control/`, `*-stream/` |
| **Export tracking** | `/exports/<id>/`, `/exports/<id>/stream/` |
| **OpenAI trace** | `/projects/<id>/openai-trace/` |
| **Settings** | `/api/core/settings/`, `/depuration-prompt-versions/` |

### 5.2 Endpoints que **se mueven a la companion app**

| Operación | Endpoint local |
|---|---|
| Health check | `GET /health` |
| Token storage | `GET/POST /auth/token` |
| File picker | `POST /file/pick`, `POST /file/save-dialog` |
| Probe video metadata | `POST /ffmpeg/probe` |
| Ingest video | `POST /ffmpeg/append-video` |
| Extract part | `POST /ffmpeg/extract-part` |
| Auto-frame | `POST /ffmpeg/auto-frame` |
| Clip stabilization | `POST /ffmpeg/clip-stabilization` |
| Sequence clip prep | `POST /ffmpeg/sequence-clip-preparation` |
| Sequence media prep | `POST /ffmpeg/sequence-media-preparation` |
| Sequence stabilization | `POST /ffmpeg/sequence-stabilization` |
| Audio enhancement | `POST /ffmpeg/sequence-audio-enhancement` |
| Video export | `POST /ffmpeg/export` |
| Batch export | `POST /ffmpeg/export-batch` |
| Cancel any job | `POST /ffmpeg/cancel` |
| Transcribe (local Whisper) | `POST /transcribe/audio` |
| Preview stream | `GET /preview/stream/{path}` |
| Timeline proxy | `GET /preview/timeline/{path}` |
| Open folder | `POST /file/open-folder` |

### 5.3 Endpoints **híbridos**

| Operación | Server | Companion |
|---|---|---|
| Iniciar transcripción | Crea job + status | Ejecuta whisper |
| Iniciar export | Crea ExportJob (BD) | Ejecuta FFmpeg |
| Auto-frame | Recibe resultados, los persiste | Detecta frames |
| Subscription check | Verifica Stripe | Cachea estado |

---

## 6. Estado del código actual vs estado objetivo

### Backend Django

| Área | Estado actual | Estado objetivo |
|---|---|---|
| Auth | ❌ No existe | ✅ JWT + suscripciones |
| Modelo User | ❌ Default Django sin uso | ✅ Custom con `Subscription` |
| FFmpeg en `services.py` | ✅ ~12K LOC | ⚠️ Refactorizar: extraer en módulos portables |
| Endpoints de control/status | ✅ Ya existen | ✅ Mantener |
| Modelo `VideoAsset` con paths | ⚠️ Path absoluto local | ✅ Path relativo + dueño + hash |
| Media serving (Range) | ✅ `media_serve.py` | ❌ Eliminar (lo hace el companion) |
| Storage (SQLite) | ⚠️ Solo dev | ✅ PostgreSQL (cPanel) |
| Deploy | ❌ Solo `runserver` | ✅ uWSGI + nginx |

### Frontend React

| Área | Estado actual | Estado objetivo |
|---|---|---|
| Cliente API centralizado | ❌ `fetch()` directo | ✅ `api.js` con JWT auto-attached |
| Detección de modo (dev/companion) | ❌ No existe | ✅ Hook `useCompanionMode()` |
| Login / Signup pages | ❌ No existe | ✅ Nuevas páginas |
| ProtectedRoute | ❌ No existe | ✅ HOC para rutas privadas |
| Settings de API keys | ❌ Workspace-level | ✅ Per-user, guardadas localmente |
| Build production | ✅ Vite build funciona | ✅ Configurar para altoque.tv |
| Manejo de 401 | ❌ No existe | ✅ Auto-redirect a login |

### Companion App (todo nuevo)

| Componente | Estado |
|---|---|
| Servidor FastAPI local | ❌ No existe |
| Empaquetado PyInstaller | ❌ No existe |
| Instalador Inno Setup | ❌ No existe |
| Cert local autofirmado | ❌ No existe |
| Tray icon | ❌ No existe |
| Endpoints FFmpeg locales | ❌ No existe (lógica reutilizable de `services.py`) |
| Auto-update check | ❌ No existe |
| DPAPI credentials | ❌ No existe |

---

## 7. Estrategia de migración del código de FFmpeg

El módulo [backend/apps/editor/services.py](../backend/apps/editor/services.py) tiene ~12 000 líneas con toda la lógica de procesamiento. **No se reescribe**: se **extrae** a un paquete Python portable que pueda usarse tanto en el companion como (opcionalmente) en el server actual durante la transición.

```
backend/apps/editor/services.py     (HOY: monolítico)
                ↓
         ┌──────┴──────┐
         ▼             ▼
   media_engine/   editor_data/
   (FFmpeg, ML)    (modelos, jobs, AI)
   → al companion  → se queda en server
```

**`media_engine/` se desarrolla como paquete pip-installable**, así:
- En el server actual sigue funcionando (modo legacy de transición)
- En el companion se importa igual
- Se pueden hacer tests aislados

---

## 8. Modelo de datos nuevo (server)

```python
# accounts/models.py — NUEVO
class User(AbstractUser):
    email = models.EmailField(unique=True)
    stripe_customer_id = models.CharField(max_length=255, blank=True)
    api_keys_local_only = models.BooleanField(default=True)
    USERNAME_FIELD = 'email'

class Subscription(models.Model):
    user = models.OneToOneField(User, on_delete=CASCADE)
    stripe_subscription_id = models.CharField(max_length=255, blank=True)
    plan = models.CharField(choices=[('trial','Trial'),('pro','Pro'),('lifetime','Lifetime')])
    status = models.CharField(choices=[('active','Active'),('past_due','Past Due'),('canceled','Canceled')])
    current_period_end = models.DateTimeField(null=True)
    created_at = models.DateTimeField(auto_now_add=True)

# Modificaciones a editor/models.py
class Project(models.Model):
    # NUEVO:
    owner = models.ForeignKey(User, on_delete=CASCADE)
    # Lo demás se mantiene

class VideoAsset(models.Model):
    # CAMBIO:
    # Antes: file = FileField(upload_to='videos/')
    # Ahora: el archivo vive solo en el companion, server guarda referencia
    local_path = models.TextField()           # path en la PC del usuario
    file_hash = models.CharField(max_length=64)  # SHA-256 para verificar
    file_size = models.BigIntegerField()
    duration_seconds = models.FloatField(null=True)
    fps = models.FloatField(null=True)
    width = models.IntegerField(null=True)
    height = models.IntegerField(null=True)
    codec = models.CharField(max_length=64, blank=True)
    # Nada de FileField — el server NO almacena el archivo
```

---

## 9. Seguridad y protección

### 9.1 Capas de protección

| Capa | Mecanismo |
|---|---|
| Acceso al editor sin login | JWT obligatorio en cada carga (verificado en frontend al iniciar) |
| Acceso al server sin app | CORS restrictivo a `https://altoque.tv` + verificación de origen |
| Companion sin licencia | El server no emite JWT si no hay suscripción activa |
| API keys del usuario | Cifradas en disco con DPAPI, nunca tocan el server |
| Token JWT en disco | Cifrado con DPAPI (atado al usuario+máquina Windows) |
| Lógica IA / prompts | Solo en el server, nunca baja al cliente |
| Localhost accesible desde otras tabs | Token de sesión aleatorio en header `X-Companion-Token` |
| Mixed content (HTTPS→HTTP) | Cert local autofirmado para `https://localhost:7432` |
| Replay de tokens | JWT corto (24h) + refresh con verificación de suscripción |

### 9.2 Lo que **es inevitable**

- El JS del frontend es inspeccionable. **Mitigación:** minificar + obfuscar pero no es blindaje.
- Un usuario técnico podría replicar requests al companion. **Mitigación:** sin sesión válida, el server no responde.

---

## 10. Versión "oficina" vs "SaaS público"

Mismo código base, diferente configuración:

| Aspecto | Oficina (v1) | SaaS público (v2) |
|---|---|---|
| Auth | Email/password creado por admin | Self-registration tras pago |
| Signup público | ❌ Deshabilitado | ✅ `/signup` → Stripe Checkout → activa cuenta |
| Pagos | ❌ No hay | ✅ Stripe Checkout |
| Suscripciones | Todos los usuarios "active" forever (`lifetime`) | Trial + Pro mensual/anual + Lifetime |
| Webhook Stripe | ❌ No | ✅ Activa/desactiva acceso automáticamente |
| API keys (OpenAI, etc.) | **Compartidas** del server (tu .env) | **BYOK** — cada usuario pone las suyas (ver §13) |
| Marketing site | ❌ No | ✅ Landing en altoque.tv |
| Versión companion | Misma | Misma |
| Trial | N/A | 7 días sin tarjeta |
| Dispositivos por cuenta | 2 (oficina + casa) | 2 (configurable por plan) |

---

## 11. Decisiones técnicas confirmadas

| Decisión | Valor |
|---|---|
| Plataforma v1 | Solo Windows |
| Pagos | Stripe |
| Hosting | HawkHost (cPanel) |
| Companion stack | Python + FastAPI + PyInstaller + Inno Setup |
| Server stack | Django (existente) + djangorestframework-simplejwt |
| Frontend | React (existente) + axios |
| Base de datos | PostgreSQL en producción (SQLite sigue en dev local) |
| Puerto local | 7432 (con fallback a 7433, 7434) |
| Almacenamiento de videos | **Solo local en cliente**, nunca en server |
| Auto-update v1 | Notificación + descarga manual |
| Auto-update v2 | Squirrel/NSIS auto-update silencioso |

---

## 12. Decisiones pendientes (no bloquean inicio)

- [ ] ¿Mac y Linux en v1.5 o v2?
- [ ] ¿Auto-start del companion con Windows? (default: opcional)
- [ ] ¿Trial sin tarjeta? (recomendado: sí)
- [ ] ¿Multi-seat para empresas? (v3)
- [ ] ¿Firma de código (~$300/año EV cert)? (recomendado para v1.1 público)
- [ ] ¿Plan híbrido "créditos incluidos" además de BYOK? (ver §13)

---

## 13. API keys por usuario (BYOK) — diseño v2

### 13.1 Problema

MaxEditor consume servicios externos de pago (OpenAI / Whisper API / etc.). En la **v1 oficina** todos los usuarios comparten **una sola key** (la tuya, en `.env` del server) porque:
- Controlas el coste
- Es un grupo cerrado de confianza
- Simplifica la implementación inicial

En la **v2 SaaS público** esto ya no es viable: si 100 usuarios usan tu key, te arruina la cuenta. Cada usuario debe pagar su propio uso de la API externa.

### 13.2 Modelo BYOK (Bring Your Own Key)

Cada usuario configura sus propias credenciales en su perfil:

```python
# accounts/models.py — añadir en v2
class UserAPIKey(models.Model):
    user = models.ForeignKey(User, on_delete=CASCADE, related_name='api_keys')
    provider = models.CharField(choices=[
        ('openai', 'OpenAI'),
        ('anthropic', 'Anthropic'),
        ('whisper_api', 'Whisper API'),
        ('elevenlabs', 'ElevenLabs'),
    ])
    encrypted_key = models.BinaryField()       # Fernet-cifrado con MASTER_KEY del server
    key_hint = models.CharField(max_length=8)  # últimos 4 chars para mostrar ("...4f2a")
    is_valid = models.BooleanField(default=False)  # validado al guardar
    last_validated_at = models.DateTimeField(null=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        unique_together = [('user', 'provider')]
```

### 13.3 Flujo BYOK

1. Usuario va a Settings → API Keys
2. Pega su key de OpenAI
3. Frontend → POST `/api/account/api-keys/` con la key en plano (sobre HTTPS)
4. Server hace **ping de validación** al provider (request mínimo)
5. Si OK: cifra con Fernet (`MASTER_KEY` en server `.env`), guarda en DB, marca `is_valid=True`
6. Cualquier endpoint que use OpenAI → descifra la key del usuario actual y la usa
7. Si la key falla en runtime → marca `is_valid=False` y notifica al usuario

### 13.4 Plan híbrido (opcional, recomendado)

Algunos usuarios no quieren lidiar con conseguir sus propias keys. Solución:

| Plan | Comportamiento |
|---|---|
| **BYOK** ($X/mes) | Usuario pone sus keys, tú no pagas el uso externo |
| **Bundled** ($Y/mes, más caro) | Tu key, pero con cuota mensual (ej: 1000 minutos de transcripción) |
| **Lifetime BYOK** ($Z una vez) | Compra única, requiere keys propias |

El server lleva contadores de uso (`UsageRecord`) por usuario+provider para poder:
- Cobrar plan Bundled (cortar al exceder cuota)
- Mostrar al usuario cuánto consume al mes
- Detectar abuso

### 13.5 Seguridad de las keys

- **Cifrado en reposo:** Fernet (AES-128 + HMAC) con `MASTER_KEY` en variables de entorno del server (NO en git)
- **En tránsito:** HTTPS obligatorio
- **Nunca en logs:** filtros de logging que enmascaren strings tipo `sk-...`
- **Solo se descifra al momento de usar**, jamás se devuelve al frontend
- **El frontend solo ve el `key_hint`** (ej: `sk-...4f2a`) para que el usuario sepa cuál tiene guardada
- **Rotación:** endpoint para reemplazar la key (re-cifra y re-valida)

### 13.6 Migración v1 → v2

Cuando llegue v2:
1. Crear tabla `UserAPIKey`
2. Mantener `OPENAI_API_KEY` global como **fallback** para usuarios oficina existentes (flag `user.uses_shared_keys=True`)
3. Nuevos usuarios SaaS → forzados a configurar BYOK antes de poder usar funciones que requieran key
4. Usuarios oficina existentes pueden migrar voluntariamente a BYOK desde Settings

---

**→ Siguiente paso:** ver [SAAS_WORKPLAN.md](SAAS_WORKPLAN.md) para el plan de trabajo detallado fase por fase.
