Un admin tool da cui il team di ops può spedire lavoro, non un foglio Excel che qualcuno deve tenere allineato
Una sola app admin Next.js per team, ritagliata sui workflow che si ripetono. Accesso per ruolo sopra l'user table esistente. Audit log su ogni scrittura, così la domanda "chi ha cambiato cosa" smette di richiedere archeologia su Slack. Tabelle veloci, bulk action, CSV in entrata e in uscita. Uno strato di agenti AI per i workflow che assomigliano a "leggi il ticket, trova il cliente, rifondi l'ultima fattura".
Il problema
Il team di ops è una pipeline ETL umana e l'azienda continua a caricarla
Il prodotto ha funzionato, allora il team è cresciuto. Customer success fa girare un Google Sheet che fa da specchio alla tabella clienti; il foglio è indietro di due giorni rispetto al tempo reale. Il finance esporta le fatture in CSV ogni venerdì e le riconcilia a mano. Il supporto legge un ticket, apre cinque tab per trovare il cliente, e manda un DM a un engineer per fare un rimborso. Ogni nuova policy finisce come thread su #ops con una lista di customer ID da aggiornare uno per uno. Il team cresce; il lavoro manuale cresce con lui; gli engineer che potrebbero automatizzarlo sono occupati col prodotto. Costruiamo l'admin tool che combacia con i workflow reali del team. Ogni azione ripetuta diventa un bottone o una bulk operation. Ogni lookup tra tab diventa un pannello laterale. Ogni thread Slack che chiude in `aggiornate questi ID per favore` diventa un form. Ogni scrittura passa da audit log. Il team di ops smette di tradurre tra sistemi e ricomincia a farli girare.
Come lo affrontiamo
Sei passi dai fogli di calcolo a un admin tool che il team usa ogni giorno
Audit dei workflow prima dello schema. Schema prima delle schermate. Schermate prima delle bulk action. Bulk action prima degli agenti AI. L'ordine conta perché ogni passo successivo dipende dalle astrazioni che il passo precedente ha prodotto.
- 01
Audit dei workflow reali del team
Ci sediamo un'ora con ogni ruolo operativo: customer success, supporto, finance, growth, founder. Ogni sessione finisce con una lista di 5-15 azioni ripetute, ordinate per frequenza e dolore. L'output è un documento unico: 40-70 workflow totali, metà duplicati tra ruoli, i primi 12 coprono l'80% del tempo del team. Il documento è la spec del resto dell'ingaggio; ogni cosa nel build si riconduce a una riga di workflow su questa lista.
- 02
Schema e accessi
Modelliamo le entità che l'admin tool deve leggere e scrivere: utenti, account, fatture, ticket, feature flag, note interne. Dove lo schema del prodotto le copre già, l'admin legge da lì. Dove non le copre (audit log, note interne, transcript degli agenti), aggiungiamo tabelle nuove col prefisso `internal_` così non sgocciolano mai nella superficie del prodotto. Ogni tabella ha policy RLS che permettono solo ai ruoli interni giusti di vedere le righe giuste; un engineer di admin non legge i PII di clienti fuori dalla sua regione assegnata.
- 03
La shell admin
Una app Next.js su `admin.tuodominio.com`, auth separata dal prodotto, dietro lo SSO scelto (Google Workspace, Microsoft Entra, Okta). Sidebar con i workflow dello step 1, una tab per ruolo, una ricerca globale tra utenti, fatture e ticket. La shell esce nella seconda settimana con i primi tre workflow cablati; il team comincia a usarla subito e dà feedback in settimana tre.
- 04
Bulk action e CSV in/out
Ogni vista a tabella supporta la multi-selezione con un dialog di conferma. Il bulk update chiede una giustificazione (una riga, conservata in audit log) e gira in background con una progress bar; il team può chiudere la tab. Import CSV per ogni operazione che il team fa in batch; export CSV da ogni vista a tabella con un bottone solo. Il Google Sheet che faceva da specchio alla tabella clienti è la prima cosa a sparire.
- 05
Audit log su ogni scrittura
Ogni scrittura via admin atterra in una tabella `audit_log` con attore, entità, diff prima/dopo, indirizzo IP, user agent. Il log è interrogabile ed esportabile; finance lo legge durante gli audit e security lo legge dopo ogni incidente. Il codice del prodotto non si tocca; l'audit log è una proprietà del path di scrittura admin.
- 06
Strato di agenti AI
I workflow che assomigliano a 'leggi il ticket, trova il cliente, esegui l'azione' prendono un agente. Usiamo Anthropic Claude con tool che avvolgono le stesse azioni admin che farebbe un umano. Ogni azione dell'agente passa dallo stesso audit log; l'agente gira come system user nominato con scope RLS esplicito. L'agente fa risparmiare tempo al team sul lavoro di routine e produce un transcript che un umano può leggere quando qualcosa sembra strano; il volante resta in mano al team.
Cosa consegniamo
Documento di audit dei workflow
Una spec con 40-70 workflow ripetuti dal team, ordinati per frequenza e dolore, mappati ai ruoli. Il documento a cui il resto del build fa riferimento; firmato dal team prima che parta il codice. Aggiornato trimestralmente perché l'admin tool resti aderente a ciò che il team fa davvero.
Schema admin
Un set di tabelle `internal_` (audit log, note interne, assegnazione ruoli, transcript agenti, ricerche salvate, dashboard) con policy RLS. Lo schema vive nello stesso progetto Supabase del prodotto ma è nominato in modo che non possa scolare nella superficie del prodotto.
Integrazione SSO
SSO cablato nell'admin via il provider scelto dal cliente (Google Workspace, Entra, Okta). I nuovi assunti hanno accesso all'admin la stessa settimana in cui entrano; chi esce perde l'accesso il giorno in cui se ne va. Niente seconda password da ricordare; niente account condivisi.
Sistema di ruoli e permessi
Ruoli (admin, ops, supporto, finance, read-only) con scope di permesso espliciti. Policy RLS che leggono il ruolo a ogni query. La matrice dei permessi vive in un file di config che il team può leggere; il team sa chi può fare cosa leggendo il documento, non chiedendo all'engineer.
Shell admin con i primi 12 workflow
Un'app admin Next.js con i 12 workflow a maggior frequenza cablati come schermate di prima classe. La shell usa il design system; le schermate nuove si infilano nei pattern esistenti invece di inventare il layout pagina per pagina. Il team fa login il primo giorno e comincia a lavorare.
Framework per le bulk action
Un pattern riutilizzabile per le tabelle multi-select: dialog di conferma, raccolta giustificazione, run in background, riga di audit log per ogni cambio, UI di progresso. Ogni workflow che vuole il bulk riceve la stessa UX; il team la impara una volta sola.
Import ed export CSV
Import CSV type-safe per ogni entità che il team deve aggiornare in batch, con validazione per riga e dry-run di preview. Export CSV da ogni vista a tabella. Il pattern è un componente solo; le tabelle nuove lo ricevono gratis.
Esploratore di audit log
Una schermata che mostra l'audit log con filtri per attore, entità, tempo e azione. Finance la apre prima degli audit; security la apre dopo gli incidenti. Esportabile in CSV con un bottone solo.
Sistema di note interne
Note attaccate a utenti, account, fatture e ticket; visibili solo allo staff interno; markdown supportato; le menzioni triggerano un DM Slack. Sostituisce il CRM parallelo che il team stava facendo girare su Notion o Linear.
Integrazione di agenti AI
Due-quattro agenti cablati sui workflow in cui il team ripete la stessa catena di letture e scritture. Ogni agente è nominato, ha uno scope, viene audit-loggato. Il team può passare un ticket all'agente e rivedere il transcript; l'agente non agisce mai senza traccia.
Dashboard operativa
Una dashboard con i quattro-otto numeri che il founder chiede ogni lunedì (MRR, clienti attivi, churn, sign-up, ticket aperti, rimborsi negli ultimi 7 giorni). La dashboard legge dallo stesso schema admin; i numeri sono gli stessi numeri con cui il team lavora ogni giorno.
Runbook dell'admin tool
Un README breve nel repo che spiega come l'admin è organizzato, come aggiungere un nuovo workflow e come rimuoverne uno quando il team smette di usarlo. Il documento che impedisce all'admin tool di crescere fino al prossimo casino.
Cinque file che compongono un admin tool che il team usa davvero
I cinque file qui sotto compongono l'admin: il middleware SSO che presidia ogni route, l'helper di query basato sui ruoli che applica RLS in lettura, il runner di bulk action con audit log, il wrapper di agente che usa Claude con i tool, e lo schema admin che definisce cosa un ruolo admin può toccare.
Un build di admin interno è un problema di traduzione prima che di UI. Il team fa già girare l'azienda; il lavoro semplicemente succede negli strumenti sbagliati. I fogli di calcolo sono senza versione e lenti; l'SQL ad hoc è veloce e poco sicuro; i thread Slack non sopravvivono alle persone che li hanno scritti. L'admin tool trasforma quei workflow in azioni tipizzate, tracciate, ritagliate per ruolo, che tutto il team può usare senza perdere la velocità di farle nel foglio.
I cinque file qui sotto sono l'ossatura che l'ingaggio lascia in piedi. Il middleware SSO che presidia l'admin, l'helper di query basato sui ruoli che applica RLS a ogni lettura, il runner di bulk action con audit, il wrapper dell'agente Claude, e lo schema admin che definisce cosa un ruolo admin può toccare.
1. Il middleware SSO
Ogni route admin gira dietro SSO. Il middleware legge la sessione, cerca il ruolo dell'utente nel database, e o lascia passare la richiesta o torna 403. Niente endpoint pubblici. Niente flusso "ho dimenticato la password". L'identity provider del cliente è la fonte di verità.
// 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. L'helper di query basato sui ruoli
L'helper legge il ruolo admin dagli header della request, apre un client Supabase ritagliato su quel ruolo, e gira la query. La JWT Supabase porta il claim di ruolo; le policy del database lo applicano. L'engineer di admin non aggira la RLS nemmeno volendo.
// 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. Il runner di bulk action con audit log
Una bulk action prende una lista di ID, una funzione da applicare per ID, e una giustificazione. Gira in batch con controllo di concorrenza, intercetta gli errori per riga, e scrive una riga di audit prima e dopo ogni cambio. La UI mostra il progresso; il team chiude la tab e riceve un ping Slack quando il run finisce.
// 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. Il wrapper di agente Claude
L'agente è una funzione sola. Prende un task, un set di tool e un contesto. Ogni tool è un piccolo wrapper tracciato attorno a un'azione admin. L'agente sceglie i tool, li chiama, e scrive un transcript che il team può leggere. L'agente non ha mai più permessi di quanti il suo ruolo gli consenta.
// 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 })),
})
// Per brevità: un giro di tool use. In pratica si fa il loop fino a quando il modello torna solo testo.
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. Lo schema admin
Cinque tabelle. admin_roles elenca il team. audit_log registra ogni scrittura. internal_notes conserva le note che il team attacca alle entità. saved_searches lascia al team salvare i set di filtri che riusa. agent_transcripts conserva ogni run di agente. Policy RLS su ogni tabella; solo i ruoli giusti vedono le righe giuste.
-- 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. Cosa compone questo
Il middleware SSO tiene fuori le persone sbagliate. L'helper di ruoli tiene le persone giuste nella loro corsia. Il runner di bulk rende il team veloce e i cambi tracciabili. L'agente fa il lavoro che il team altrimenti darebbe a un intern, con un transcript che il team può leggere. Lo schema fa vivere tutta la cosa in un posto che non scola nel prodotto.
Il team di ops smette di essere una pipeline ETL umana. Il foglio Excel che faceva da specchio alla tabella clienti è sparito. La riconciliazione CSV del venerdì gira dall'admin in 12 secondi. I thread Slack che chiudevano con aggiornate questi ID per favore diventano form con dialog di conferma e riga di audit. L'engineer che scriveva uno script ogni settimana torna a lavorare sul prodotto. Il team che era cresciuto trasformandosi in un'azienda di manodopera torna a essere un'azienda di software.
Stack correlati
Domande frequenti
In cosa è diverso da Retool o Forest Admin?
Retool e Forest sono ottimi quando il team riesce a far stare i suoi workflow in una UI CRUD generica; molti ci riescono. Costruiamo un admin custom quando il team ha workflow che attraversano più entità, quando gli agenti AI darebbero una mano, quando audit e compliance devono essere stretti, o quando l'admin sopravviverà più di una licenza Retool. Abbiamo spedito entrambe le forme; la call di scoping decide quale fa al caso.
L'admin tool sostituisce i nostri tool interni esistenti?
In parte, per scelta. Il primo build sostituisce i fogli di calcolo a maggior dolore e i workflow manuali più ripetuti. Linear, Slack, Notion restano dove il team li vuole. L'admin tool è il posto dove il team riscrive nel prodotto, non un project management che fa concorrenza a ciò che il team già usa.
Come evitiamo che diventi il prossimo casino?
Tre cose. L'audit dei workflow è la spec; non aggiungiamo una schermata senza una riga di workflow nel documento. Il runbook spiega come rimuovere le schermate che il team smette di usare. Ogni trimestre rifacciamo l'audit e potiamo; le schermate che nessun ruolo usa vengono archiviate. L'admin è uno strumento vivo, non un cimitero di feature a metà.
E gli accessi per consulenti e partner esterni?
Stesso SSO, col consulente invitato a un ruolo con scope ristretto e scadenza a 90 giorni. Il ruolo vive nel config dei permessi; la scadenza la fa rispettare un cron giornaliero. I partner esterni passano dallo stesso flusso; l'ingaggio lascia al team il pattern per gestirli entrambi.
Quanto ci vuole?
Sei-dieci settimane per il primo cutover. L'audit dei workflow prende una settimana. Schema e SSO prendono una settimana. La shell con i primi tre workflow esce in settimana tre. I nove workflow rimanenti escono tra le settimane quattro e sette. Audit log, bulk action e CSV escono in parallelo. L'integrazione agenti prende due settimane se è in scope; molti team la aggiungono in un ingaggio di follow-up dopo aver vissuto col tool per un mese.
Costruite anche le dashboard, o solo i workflow?
Entrambi. Lo stesso schema che fa girare i workflow fa girare la dashboard. Costruiamo i quattro-otto numeri che il founder chiede ogni lunedì nel primo build; ne aggiungiamo altri su richiesta. Le dashboard che nessuno apre vengono archiviate nella revisione trimestrale.
E gli agenti che agiscono sui dati cliente?
Un agente che tocca dati cliente gira sotto la stessa RLS di un umano nel suo ruolo. Non può accedere a più di quanto il ruolo consenta. Ogni azione passa dall'audit log col nome dell'agente come attore. Il team può mettere in pausa ogni agente con un toggle; il toggle sta nel runbook.
Sostituite il nostro engineer di operations?
No. L'admin tool rende durevole il lavoro dell'engineer di operations. Invece di scrivere uno script ogni settimana, l'engineer aggiunge una schermata o una bulk action; il workflow diventa infrastruttura condivisa. Gli engineer di operations di solito si trovano liberati: il lavoro ad hoc si comprime in una superficie più piccola e più interessante.
Definisci lo scope del tuo admin interno
Una call di scoping, un audit dei workflow nella prima settimana, uno scope fisso e un numero che teniamo. Sei-dieci settimane dal kickoff a un admin tool che il team usa ogni giorno.