Caso de uso · Herramientas internas

Una admin desde la que el equipo de ops puede entregar trabajo, no una hoja de cálculo que alguien tiene que mantener sincronizada

Una sola app admin Next.js por equipo, recortada a los workflows que se repiten. Acceso por rol encima de la tabla de usuarios existente. Audit log en cada escritura, así la pregunta "quién cambió qué" deja de necesitar arqueología en Slack. Tablas rápidas, bulk actions, CSV en entrada y en salida. Una capa de agentes AI para los workflows que se parecen a "lee el ticket, encuentra al cliente, reembolsa la última factura".

El problema

El equipo de ops es una pipeline ETL humana y la empresa no para de cargarla

El producto funcionó, así que el equipo creció. Customer success lleva un Google Sheet que es espejo de la tabla de clientes; la hoja va dos días por detrás del tiempo real. Finance exporta las facturas a CSV cada viernes y las reconcilia a mano. Soporte lee un ticket, abre cinco pestañas para encontrar al cliente, y le manda un DM a un engineer para hacer un refund. Cada política nueva acaba como hilo en #ops con una lista de customer IDs para actualizar uno por uno. El equipo crece; el trabajo manual crece con él; los engineers que podrían automatizarlo están ocupados con el producto. Construimos la admin que encaja con los workflows reales del equipo. Cada acción repetida pasa a ser un botón o una bulk operation. Cada lookup entre pestañas pasa a ser un panel lateral. Cada hilo de Slack que termina en `por favor actualizad estos IDs` pasa a ser un formulario. Cada escritura va por audit log. El equipo de ops deja de traducir entre sistemas y empieza a hacerlos andar.

Cómo lo abordamos

Seis pasos de las hojas de cálculo a una admin que el equipo usa cada día

Audit de workflows antes que esquema. Esquema antes que pantallas. Pantallas antes que bulk actions. Bulk actions antes que agentes AI. El orden importa porque cada paso siguiente depende de las abstracciones que produjo el paso anterior.

  1. 01

    Auditar los workflows reales del equipo

    Nos sentamos una hora con cada rol operativo: customer success, soporte, finance, growth, el founder. Cada sesión acaba con una lista de 5 a 15 acciones repetidas, ordenadas por frecuencia y dolor. La salida es un único documento: 40-70 workflows en total, la mitad duplicados entre roles, los 12 primeros cubren el 80% del tiempo del equipo. El documento es el spec del resto del encargo; cada cosa del build se remite a una fila de workflow de esta lista.

  2. 02

    Esquema y accesos

    Modelamos las entidades que la admin tiene que leer y escribir: usuarios, cuentas, facturas, tickets, feature flags, notas internas. Donde el esquema del producto ya las cubre, la admin lee de ahí. Donde no las cubre (audit log, notas internas, transcripts de agentes), añadimos tablas nuevas con prefijo `internal_` para que no se cuelen nunca en la superficie del producto. Cada tabla tiene policies RLS que dejan solo a los roles internos correctos ver las filas correctas; un engineer de admin no lee PII de clientes fuera de su región asignada.

  3. 03

    La shell de la admin

    Una app Next.js en `admin.tudominio.com`, auth separada del producto, detrás del SSO elegido (Google Workspace, Microsoft Entra, Okta). Sidebar con los workflows del paso 1, una pestaña por rol, una búsqueda global a través de usuarios, facturas y tickets. La shell sale en la segunda semana con los tres primeros workflows cableados; el equipo empieza a usarla en seguida y da feedback en la semana tres.

  4. 04

    Bulk actions y CSV in/out

    Cada vista de tabla soporta multi-selección con un diálogo de confirmación. El bulk update pide una justificación (una línea, guardada en audit log) y corre en background con una barra de progreso; el equipo puede cerrar la pestaña. Import CSV para cada operación que el equipo hace en lote; export CSV desde cada vista de tabla con un solo botón. El Google Sheet que era espejo de la tabla de clientes es lo primero que desaparece.

  5. 05

    Audit log en cada escritura

    Cada escritura vía admin aterriza en una tabla `audit_log` con actor, entidad, diff antes/después, dirección IP, user agent. El log es consultable y exportable; finance lo lee durante los audits y security lo lee después de cualquier incidente. El código de producto no se toca; el audit log es una propiedad del camino de escritura de la admin.

  6. 06

    Capa de agentes AI

    Los workflows que se parecen a 'lee el ticket, encuentra al cliente, ejecuta la acción' reciben un agente. Usamos Anthropic Claude con tools que envuelven las mismas acciones admin que haría un humano. Cada acción del agente pasa por el mismo audit log; el agente corre como system user con nombre y scope RLS explícito. El agente ahorra tiempo al equipo en el trabajo rutinario y produce un transcript que un humano puede leer cuando algo parece raro; el volante se queda en manos del equipo.

Qué entregamos

Documento de audit de workflows

Un spec con 40-70 workflows repetidos del equipo, ordenados por frecuencia y dolor, mapeados a roles. El documento al que se remite el resto del build; firmado por el equipo antes de que arranque el código. Se actualiza cada trimestre para que la admin siga pegada a lo que el equipo hace de verdad.

Esquema admin

Un conjunto de tablas `internal_` (audit log, notas internas, asignación de roles, transcripts de agentes, búsquedas guardadas, dashboards) con policies RLS. El esquema vive en el mismo proyecto Supabase del producto pero se nombra de modo que no pueda colarse a la superficie del producto.

Integración SSO

SSO cableado en la admin a través del provider elegido por el cliente (Google Workspace, Entra, Okta). Las nuevas contrataciones tienen acceso a la admin la misma semana en que entran; las bajas pierden el acceso el día que se van. Sin segunda contraseña que recordar; sin cuentas compartidas.

Sistema de roles y permisos

Roles (admin, ops, soporte, finance, read-only) con scopes de permiso explícitos. Policies RLS que leen el rol en cada query. La matriz de permisos vive en un archivo de config que el equipo puede leer; el equipo sabe quién puede hacer qué leyendo el documento, no preguntándole al engineer.

Shell admin con los 12 workflows top

Una app admin Next.js con los 12 workflows de mayor frecuencia cableados como pantallas de primera clase. La shell usa el design system; las pantallas nuevas caen en los patrones existentes en vez de inventar layout página por página. El equipo hace login el primer día y empieza a trabajar.

Framework de bulk actions

Un patrón reutilizable para tablas multi-select: diálogo de confirmación, captura de justificación, run en background, fila de audit log por cada cambio, UI de progreso. Cada workflow que pida bulk recibe la misma UX; el equipo la aprende una sola vez.

Import y export CSV

Import CSV type-safe para cada entidad que el equipo necesita actualizar en lote, con validación por fila y dry-run de preview. Export CSV desde cada vista de tabla. El patrón es un solo componente; las tablas nuevas lo reciben gratis.

Explorador de audit log

Una pantalla que enseña el audit log con filtros por actor, entidad, tiempo y acción. Finance la abre antes de los audits; security la abre después de los incidentes. Exportable como CSV con un solo botón.

Sistema de notas internas

Notas pegadas a usuarios, cuentas, facturas y tickets; visibles solo para el staff interno; markdown soportado; las menciones disparan un DM de Slack. Sustituye al CRM paralelo que el equipo estaba llevando en Notion o Linear.

Integración de agentes AI

Dos a cuatro agentes cableados a los workflows donde el equipo repite la misma cadena de lecturas y escrituras. Cada agente tiene nombre, scope, audit-log. El equipo puede pasarle un ticket al agente y revisar el transcript; el agente nunca actúa sin rastro.

Dashboard operativo

Un dashboard con los cuatro u ocho números que el founder pregunta cada lunes (MRR, clientes activos, churn, sign-ups, tickets abiertos, refunds en los últimos 7 días). El dashboard lee del mismo esquema admin; los números son los mismos números con los que el equipo trabaja cada día.

Runbook de la admin

Un README corto en el repo que explica cómo está montada la admin, cómo añadir un workflow nuevo y cómo quitar uno cuando el equipo deja de usarlo. El documento que impide que la admin se convierta en el siguiente lío.

Cinco archivos que componen una admin que el equipo usa de verdad

Los cinco archivos de abajo componen la admin: el middleware SSO que vigila cada ruta, el helper de query basado en roles que aplica RLS en lectura, el runner de bulk action con audit log, el wrapper de agente que usa Claude con tools, y el esquema admin que define qué puede tocar un rol admin.

Un build de admin interno es un problema de traducción antes que un problema de UI. El equipo ya lleva el negocio; el trabajo simplemente ocurre en las herramientas equivocadas. Las hojas de cálculo son lentas y sin versión; el SQL ad hoc es rápido y poco seguro; los hilos de Slack no sobreviven a las personas que los escribieron. La admin convierte esos workflows en acciones tipadas, auditadas, recortadas por rol, que todo el equipo puede usar sin perder la velocidad de hacerlas en una hoja.

Los cinco archivos de abajo son la armadura que deja el encargo. El middleware SSO que vigila la admin, el helper de query basado en roles que aplica RLS en cada lectura, el runner de bulk action con audit log, el wrapper de agente Claude, y el esquema admin que define qué puede tocar un rol admin.

1. El middleware SSO

Cada ruta admin corre detrás de SSO. El middleware lee la sesión, busca el rol del usuario en la base de datos, y deja pasar la request o devuelve 403. Sin endpoints públicos. Sin flujo de "olvidé mi contraseña". El identity provider del cliente es la fuente de verdad.

// middleware.ts
import { NextResponse, type NextRequest } from 'next/server'
import { getServerSession } from '@/lib/auth/session'
import { lookupAdminRole } from '@/lib/admin/roles'

const ALLOWED_DOMAINS = ['adamarant.com']

export async function middleware(req: NextRequest): Promise<NextResponse> {
  const session = await getServerSession(req)
  if (!session) {
    return NextResponse.redirect(new URL('/auth/sign-in', req.url))
  }

  const email = session.user.email ?? ''
  const domain = email.split('@')[1]
  if (!ALLOWED_DOMAINS.includes(domain)) {
    return new NextResponse('forbidden', { status: 403 })
  }

  const role = await lookupAdminRole(session.user.id)
  if (!role) {
    return new NextResponse('not provisioned', { status: 403 })
  }

  const requestHeaders = new Headers(req.headers)
  requestHeaders.set('x-admin-role', role)
  requestHeaders.set('x-admin-user-id', session.user.id)

  return NextResponse.next({ request: { headers: requestHeaders } })
}

export const config = { matcher: ['/((?!auth|api/auth|_next).*)'] }

2. El helper de query basado en roles

El helper lee el rol admin de las cabeceras de la request, abre un cliente Supabase recortado a ese rol, y corre la query. La JWT de Supabase lleva el claim de rol; las policies de la base de datos lo aplican. El engineer de admin no se salta la RLS aunque quisiera.

// lib/admin/db.ts
import { createClient } from '@supabase/supabase-js'
import { headers } from 'next/headers'
import { signRoleJwt } from './role-jwt'

interface AdminContext {
  role: 'admin' | 'ops' | 'support' | 'finance' | 'read_only'
  userId: string
}

export async function adminContext(): Promise<AdminContext> {
  const h = await headers()
  const role = h.get('x-admin-role') as AdminContext['role'] | null
  const userId = h.get('x-admin-user-id')
  if (!role || !userId) throw new Error('missing admin context')
  return { role, userId }
}

export async function adminDb(): Promise<ReturnType<typeof createClient>> {
  const ctx = await adminContext()
  const jwt = signRoleJwt({ sub: ctx.userId, role: ctx.role })
  return createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!, {
    global: { headers: { Authorization: `Bearer ${jwt}` } },
  })
}

3. El runner de bulk action con audit log

Una bulk action coge una lista de IDs, una función a aplicar por ID, y una justificación. Corre en lotes con control de concurrencia, captura errores por fila, y escribe una fila de audit antes y después de cada cambio. La UI muestra el progreso; el equipo cierra la pestaña y recibe un ping en Slack cuando el run termina.

// lib/admin/bulk.ts
import { adminContext, adminDb } from './db'
import { sendSlack } from '@/lib/integrations/slack'

interface BulkRunOptions<T> {
  entityType: string
  ids: string[]
  justification: string
  apply: (id: string, db: Awaited<ReturnType<typeof adminDb>>) => Promise<T>
  concurrency?: number
}

export async function runBulk<T>(opts: BulkRunOptions<T>): Promise<{ ok: number; failed: number }> {
  const ctx = await adminContext()
  const db = await adminDb()
  const concurrency = opts.concurrency ?? 5

  let ok = 0
  let failed = 0

  for (let i = 0; i < opts.ids.length; i += concurrency) {
    const batch = opts.ids.slice(i, i + concurrency)
    const results = await Promise.allSettled(
      batch.map(async (id) => {
        const before = await db.from(opts.entityType).select('*').eq('id', id).single()
        const next = await opts.apply(id, db)
        const after = await db.from(opts.entityType).select('*').eq('id', id).single()
        await db.from('audit_log').insert({
          actor_user_id: ctx.userId,
          actor_role: ctx.role,
          entity_type: opts.entityType,
          entity_id: id,
          action: 'bulk_update',
          before: before.data,
          after: after.data,
          justification: opts.justification,
        })
        return next
      }),
    )
    for (const r of results) {
      if (r.status === 'fulfilled') ok += 1
      else failed += 1
    }
  }

  await sendSlack({
    channel: '#ops',
    text: `bulk run on ${opts.entityType}: ${ok} ok, ${failed} failed (actor=${ctx.userId})`,
  })

  return { ok, failed }
}

4. El wrapper de agente Claude

El agente es una sola función. Coge una tarea, un set de tools y un contexto. Cada tool es un pequeño wrapper auditado alrededor de una acción admin. El agente elige tools, los llama, y escribe un transcript que el equipo puede leer. El agente nunca tiene más permisos de los que su rol permite.

// lib/admin/agent.ts
import Anthropic from '@anthropic-ai/sdk'
import { adminContext, adminDb } from './db'

const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY })

interface AgentTool {
  name: string
  description: string
  inputSchema: Record<string, unknown>
  run: (input: Record<string, unknown>) => Promise<string>
}

export async function runAgent(
  task: string,
  tools: AgentTool[],
): Promise<{ transcript: string; result: string }> {
  const ctx = await adminContext()
  const db = await adminDb()
  const transcript: string[] = []

  const response = await anthropic.messages.create({
    model: 'claude-opus-4-7',
    max_tokens: 4096,
    messages: [{ role: 'user', content: task }],
    tools: tools.map((t) => ({ name: t.name, description: t.description, input_schema: t.inputSchema as Anthropic.Tool.InputSchema })),
  })

  // Por brevedad: una vuelta de tool use. En la práctica se hace loop hasta que el modelo devuelve solo texto.
  for (const block of response.content) {
    if (block.type === 'tool_use') {
      const tool = tools.find((t) => t.name === block.name)
      if (!tool) continue
      const result = await tool.run(block.input as Record<string, unknown>)
      transcript.push(`tool=${tool.name} input=${JSON.stringify(block.input)} result=${result}`)
    } else if (block.type === 'text') {
      transcript.push(`text=${block.text}`)
    }
  }

  await db.from('agent_transcripts').insert({
    actor_user_id: ctx.userId,
    actor_role: ctx.role,
    task,
    transcript: transcript.join('\n'),
  })

  return { transcript: transcript.join('\n'), result: transcript[transcript.length - 1] ?? '' }
}

5. El esquema admin

Cinco tablas. admin_roles lista al equipo. audit_log registra cada escritura. internal_notes guarda las notas que el equipo pega a las entidades. saved_searches deja al equipo guardar los conjuntos de filtros que reutiliza. agent_transcripts guarda cada run de agente. Policies RLS en cada tabla; solo los roles correctos ven las filas correctas.

-- supabase/schema/admin.sql
create table if not exists admin_roles (
  user_id uuid primary key references auth.users(id) on delete cascade,
  role text not null check (role in ('admin', 'ops', 'support', 'finance', 'read_only')),
  created_at timestamptz default now()
);

create table if not exists audit_log (
  id uuid primary key default gen_random_uuid(),
  actor_user_id uuid references admin_roles(user_id),
  actor_role text not null,
  entity_type text not null,
  entity_id text not null,
  action text not null,
  before jsonb,
  after jsonb,
  justification text,
  created_at timestamptz default now()
);
create index audit_log_entity_idx on audit_log (entity_type, entity_id, created_at desc);

create table if not exists internal_notes (
  id uuid primary key default gen_random_uuid(),
  entity_type text not null,
  entity_id text not null,
  author_user_id uuid references admin_roles(user_id),
  body text not null,
  created_at timestamptz default now()
);

create table if not exists saved_searches (
  id uuid primary key default gen_random_uuid(),
  owner_user_id uuid references admin_roles(user_id),
  name text not null,
  entity_type text not null,
  filters jsonb not null,
  created_at timestamptz default now()
);

create table if not exists agent_transcripts (
  id uuid primary key default gen_random_uuid(),
  actor_user_id uuid references admin_roles(user_id),
  actor_role text not null,
  task text not null,
  transcript text not null,
  created_at timestamptz default now()
);

alter table admin_roles enable row level security;
alter table audit_log enable row level security;
alter table internal_notes enable row level security;
alter table saved_searches enable row level security;
alter table agent_transcripts enable row level security;

create policy "admin_roles read self" on admin_roles for select
  using (auth.uid() = user_id or current_setting('request.jwt.claim.role') = 'admin');

create policy "audit_log read by role" on audit_log for select
  using (current_setting('request.jwt.claim.role') in ('admin', 'finance'));

6. Lo que esto compone

El middleware SSO deja fuera a la gente equivocada. El helper de roles deja a la gente correcta dentro de su carril. El runner de bulk hace al equipo rápido y a los cambios trazables. El agente hace el trabajo que el equipo le pasaría a un becario, con un transcript que el equipo puede leer. El esquema hace que todo viva en un sitio que no se cuela en el producto.

El equipo de ops deja de ser una pipeline ETL humana. El Google Sheet que era espejo de la tabla de clientes está borrado. La reconciliación CSV del viernes corre desde la admin en 12 segundos. Los hilos de Slack que cerraban con por favor actualizad estos IDs pasan a ser formularios con diálogo de confirmación y fila de audit. El engineer que escribía un script cada semana vuelve a trabajar en el producto. El equipo que había crecido convirtiéndose en una empresa de mano de obra vuelve a ser una empresa de software.

Preguntas frecuentes

¿En qué se diferencia esto de Retool o Forest Admin?

Retool y Forest van muy bien cuando el equipo consigue meter sus workflows en una UI CRUD genérica; muchos lo consiguen. Construimos una admin custom cuando el equipo tiene workflows que cruzan varias entidades, cuando los agentes AI vendrían bien, cuando audit y compliance tienen que estar apretados, o cuando la admin va a sobrevivir más que una licencia Retool. Hemos entregado las dos formas; la llamada de scoping decide cuál encaja.

¿La admin sustituye a nuestras herramientas internas existentes?

Parcialmente, a propósito. El primer build sustituye las hojas de cálculo de más dolor y los workflows manuales más repetidos. Linear, Slack, Notion se quedan donde le gustan al equipo. La admin es el sitio donde el equipo escribe de vuelta al producto, no un project management que compite con lo que el equipo ya usa.

¿Cómo evitamos que esto se convierta en el siguiente lío?

Tres cosas. El audit de workflows es el spec; no añadimos una pantalla sin una fila de workflow en el documento. El runbook explica cómo quitar pantallas que el equipo deja de usar. Cada trimestre rehacemos el audit y podamos; las pantallas que ningún rol usa se archivan. La admin es una herramienta viva, no un cementerio de features a medias.

¿Y los accesos para contractors y partners externos?

El mismo SSO, con el contractor invitado a un rol con scope estrecho y caducidad a 90 días. El rol vive en la config de permisos; la caducidad la aplica un cron diario. Los partners externos pasan por el mismo flujo; el encargo le deja al equipo el patrón para gestionar a los dos.

¿Cuánto tarda?

De seis a diez semanas para el primer cutover. El audit de workflows lleva una semana. Esquema y SSO llevan una semana. La shell con los tres primeros workflows sale en la semana tres. Los nueve workflows restantes salen entre las semanas cuatro y siete. Audit log, bulk actions y CSV salen en paralelo. La integración de agentes lleva dos semanas si está en el alcance; muchos equipos la añaden en un encargo de follow-up tras un mes de uso.

¿Construís también los dashboards, o solo los workflows?

Los dos. El mismo esquema que mueve los workflows mueve el dashboard. Construimos los cuatro u ocho números que el founder pregunta cada lunes en el primer build; añadimos más a petición. Los dashboards que nadie abre se archivan en la revisión trimestral.

¿Qué pasa con los agentes que actúan sobre datos de cliente?

Un agente que toca datos de cliente corre bajo la misma RLS que un humano en su rol. No puede acceder a más de lo que el rol permite. Cada acción pasa por el audit log con el nombre del agente como actor. El equipo puede pausar a cada agente con un solo toggle; el toggle está en el runbook.

¿Reemplazáis a nuestro engineer de operations?

No. La admin hace que el trabajo del engineer de operations sea duradero. En vez de escribir un script cada semana, el engineer añade una pantalla o una bulk action; el workflow pasa a ser infraestructura compartida. Los engineers de operations suelen sentirse liberados: el trabajo ad hoc se comprime en una superficie más pequeña y más interesante.

Define el alcance de tu admin interno

Una llamada de scoping, un audit de workflows en la primera semana, un alcance fijo y un número que mantenemos. De seis a diez semanas desde el kickoff a una admin que el equipo usa cada día.