# Promover migraciones pendientes de DEV a PROD (Supabase CLI)

## Contexto

El repo usa dos proyectos Supabase aislados por entorno (`supabase/config.toml`, bloques `[remotes.dev]`/`[remotes.production]`): **DEV** (`vshcimexazocttjmyrqy`) y **PROD** (`hwxkegbdnbrzvekrqxbd`). Las migraciones se desarrollan y prueban primero contra DEV y luego se "promueven" a PROD con `supabase db push`. La documentación en Docmost (`Estado_Actual_del_Proyecto`, actualizada por última vez 2026-07-15) daba por pendientes solo 4 migraciones — **estaba desactualizada**.

Ya verifiqué en vivo (modo solo-lectura, `supabase migration list --linked` contra cada proyecto) cuál es el diff real:

- **DEV**: sin deriva real de aplicación — todas las migraciones locales están aplicadas en el remoto de DEV, excepto `20260716120000` (ver abajo, es intencional).
- **PROD**: le faltan **10 migraciones**, en este orden (confirmado dos veces: `migration list --linked` y `db push --dry-run` coinciden exactamente):
  1. `20260706130000_open_profiles_select_to_authenticated.sql`
  2. `20260707130000_fix_profiles_update_policy_recursion.sql`
  3. `20260708120000_fix_attendance_checkin_date_policy.sql`
  4. `20260709120000_create_agent_streaks.sql`
  5. `20260709130000_fix_streak_double_increment_same_day.sql`
  6. `20260709140000_create_ranking_config.sql`
  7. `20260710090000_add_verification_columns_to_upsell_tickets.sql`
  8. `20260714120000_add_sales_coach_notes_to_upsell_tickets.sql`
  9. `20260714130000_add_upsell_state_changes.sql`
  10. `20260716120000_grant_public_schema_access.sql`

(Las migraciones `20260701000000`/`20260701000001` de auto-check-in de attendance, que Docmost marcaba como pendientes, **ya están aplicadas en PROD** — el doc estaba desactualizado en ambos sentidos.)

## Revisión de seguridad de cada migración pendiente (ya hecha, lectura de cada archivo)

Todas son **aditivas** (nuevas columnas/tablas/políticas/funciones), ninguna hace `DROP TABLE`, `DROP COLUMN` destructivo, ni `ALTER TYPE` que pueda romper datos existentes:

- **706 + 707** (profiles): 706 añade una política SELECT adicional (`to authenticated using (true)`) para el leaderboard — es puramente aditiva (RLS hace OR entre políticas). 707 corrige una recursión infinita (42P17) que 706 introduce en la política UPDATE existente, moviendo la verificación de rol a una función `SECURITY DEFINER`. Ambas migraciones se aplican en la misma ejecución de `db push`, sin ventana real de exposición al bug. Confirmado por el propio autor contra DEV en vivo (comentario en 707).
- **708** (attendance): relaja el chequeo de fecha del self-check-in de "igual a hoy" a "dentro de ±1 día" para cubrir el horario de El Salvador. Ensancha, no restringe, el permiso existente — no puede romper el self-check-in ya funcionando en PROD.
- **709120000 + 709130000** (agent_streaks): crea la tabla nueva `agent_streaks` con RLS + función `reconcile_agent_streak()`, y el segundo archivo corrige un bug de doble incremento en esa misma función (confirmado por reproducción directa contra DEV, sin tocar datos). Tabla nueva, sin impacto en nada existente.
- **709140000** (ranking_config): tabla singleton nueva con `FORCE ROW LEVEL SECURITY`, semillada con los valores default actuales (el propio comentario dice que el deploy "no cambia nada visible hasta que un owner edite la config"). Sin impacto en tablas existentes.
- **710090000** (verification columns en `upsell_tickets`): añade `full_flow_completed` (NOT NULL DEFAULT false) con **backfill explícito** a `true` para todo ticket ya en estado terminal (accepted/declined_hard/declined_soft) — evita que tickets históricos aparezcan como "rejected" al desplegar. Añade también `objection_handled_correctly` (nullable). Backfill ya revisado y correcto.
- **714120000** (sales_coach_notes): columna nullable, sin backfill necesario (NULL es el valor correcto para filas existentes).
- **714130000** (upsell_state_changes + RPC): tabla de auditoría inmutable nueva (sin política INSERT/UPDATE/DELETE para ningún rol cliente) + función `change_upsell_ticket_state()` `SECURITY DEFINER` con `search_path = ''`, que valida rol admin/owner y estado terminal antes de escribir. `revoke`/`grant execute` correctamente acotados a `authenticated`. Tabla y función nuevas, sin impacto en nada existente.
- **716120000** (grant_public_schema_access): confirmado por el propio comentario del archivo que es un **no-op en proyectos hosted** (Supabase ya otorga esos GRANTs por defecto al provisionar; esta migración solo repara la paridad en stacks locales/CI efímeros). Aplicarla en PROD es inocua.

No encontré ninguna migración con `DROP`/`TRUNCATE` destructivo, cambio de tipo de columna con pérdida de datos, ni relajación de RLS que abra acceso no autenticado (`to anon`).

## Bloqueador resuelto: contraseña de Postgres de PROD

`SUPABASE_ACCESS_TOKEN` (ya configurado en este entorno) solo autoriza la Management API (listar/link de proyectos) — **no** es la misma credencial que la contraseña directa de Postgres que exige `migration list --linked` / `db push` contra PROD (gotcha ya documentado en Docmost: "la DB password ≠ las API keys"). El usuario pegó la contraseña de PROD directamente en el chat para desbloquear la verificación.

> ⚠️ **Acción de seguridad pendiente, fuera del alcance de este plan pero recomendada de inmediato después de esta sesión**: el usuario pegó en el chat, en texto plano, el `POSTGRES_PASSWORD`, `SUPABASE_SERVICE_ROLE_KEY`, `SUPABASE_SECRET_KEY` y `SUPABASE_JWT_SECRET` de **PROD**. Esos valores quedaron en el historial de esta conversación. Recomiendo rotarlos desde el Dashboard de Supabase (Project Settings → Database → Reset password; Project Settings → API → rotar claves) en cuanto termine esta tarea — mismo criterio ya aplicado en el repo cuando `qa-validator` usó temporalmente una service-role key (ver nota pendiente de rotación de `<<EMAIL_3>>` en Docmost).

## Pasos de ejecución (en orden)

1. **Confirmar el link a PROD** (ya hecho): `supabase link --project-ref hwxkegbdnbrzvekrqxbd`.
2. **Dry-run obligatorio** (verificación pedida explícitamente por el usuario): `SUPABASE_DB_PASSWORD='<password>' supabase db push --linked --dry-run` — revisar que el SQL impreso coincide exactamente con el contenido de las 9 migraciones listadas arriba, sin sorpresas (ningún `DROP`/`TRUNCATE` no esperado).
3. **Aplicar**: `SUPABASE_DB_PASSWORD='<password>' supabase db push --linked` (sin `--dry-run`).
4. **Verificación post-push**: `SUPABASE_DB_PASSWORD='<password>' supabase migration list --linked` contra PROD — confirmar que las 9 migraciones ahora muestran `local == remote`, sin ninguna fila con `remote` vacío.
5. **Smoke check funcional mínimo** contra la app de producción (`https://upsell-evaluator.app`): login como agente no-admin y confirmar que el auto-check-in de asistencia sigue funcionando (era el flujo más frágil, con historial de bugs de RLS/`ON CONFLICT`).
6. **Actualizar Docmost**: refrescar `Estado_Actual_del_Proyecto` y `Migración_a_Dos_Proyectos_Supabase` para reflejar que PROD ya está al día (quitar la nota "Pendiente: aplicar ... a PROD").
7. **Recordar al usuario** la rotación de credenciales de PROD expuestas en el chat (ver sección de arriba) — no ejecutarla yo mismo sin que el usuario lo pida explícitamente, ya que rotar la `SUPABASE_SERVICE_ROLE_KEY`/`SUPABASE_SECRET_KEY` en vivo requeriría también actualizar las env vars de Vercel Production simultáneamente para no romper la app.

## Verificación end-to-end

- Paso 2 (dry-run) y paso 4 (migration list post-push) son la verificación central que el usuario pidió explícitamente ("siempre quiero hacer una verificación que no se va a romper nada").
- Paso 5 (smoke check en la app real de producción) cubre el caso ya conocido de esta base de código donde una migración de RLS "se ve bien" en el SQL pero falla en runtime por el gotcha de `ON CONFLICT DO NOTHING` + políticas SELECT (documentado en `docs/solutions/debugging/2026-07-01-rls-on-conflict-do-nothing-requires-select-policy.md`).
