 Prompt: Refactor Multi-Tenant para Schema-per-Tenant em Supabase
                                                                                                                          Contexto e objetivo

  Estou migrando meu sistema multi-tenant de RLS-only com tenant_id em cada tabela para schema-per-tenant (tenant_<slug>
   com clones físicos da estrutura). Quero isolamento físico das tabelas que pertencem a um tenant, mantendo em public
  apenas tabelas globais (auth.users, profiles, tenants, planos SaaS, notificações de sistema, etc.).

  Já fiz esse refactor num projeto irmão (Vue 3 + Supabase + Postgres 17). Quero que você execute o mesmo aqui,
  considerando as lições que aprendi.

  Antes de começar — varredura obrigatória

  Não confie na lista que o usuário (ou um amigo programador) te entregar. Verifique tudo:

  1. Liste TODAS as tabelas em public e classifique cada uma como "tenant-scoped" ou "global". Use a heurística: tem
  coluna tenant_id? É candidata a tenant-scoped. Mas reveja caso a caso — algumas globais (tenant_features,
  tenant_audit_log, support_messages) também têm tenant_id como FK e devem ficar em public.
  SELECT table_name,
         EXISTS(SELECT 1 FROM information_schema.columns c
                 WHERE c.table_schema='public' AND c.table_name=t.table_name
                   AND c.column_name='tenant_id') AS has_tenant_id
  FROM information_schema.tables t
  WHERE table_schema='public' AND table_type='BASE TABLE'
  ORDER BY table_name;
  2. Liste TODAS as funções em public que referenciam essas tabelas-tenant. Não confie em listas pré-feitas — eu recebi
  "29 funções" e eram na verdade 52. Use:
  WITH tenant_tabs AS (SELECT unnest(ARRAY[/* sua lista */]) AS tab)
  SELECT DISTINCT p.proname, p.prokind, l.lanname
  FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
  JOIN pg_language l ON l.oid = p.prolang
  CROSS JOIN tenant_tabs t
  WHERE n.nspname='public'
    AND pg_get_functiondef(p.oid) ~ ('\m' || t.tab || '\M')
  ORDER BY 1;
  3. Liste FKs cross-schema (de tabelas que vão ficar em public, apontando pras que vão sair). Se houver, planeje
  cuidado especial.
  4. Liste todas as edge functions e grep cada uma por .from('<tabela_tenant>').
  5. Liste as policies RLS que usam funções a refatorar — vão precisar ser dropadas/recriadas.

  Plano de execução em fases

  F0 — Categorização (não codar nada ainda)

  Faça as listagens acima. Salve em documento markdown na raiz: docs/F0_categorizacao.md. Conte tabelas, funções, edge
  functions, FKs cross-schema, policies dependentes. Pause e mostre pro usuário antes de seguir.

  F1 — Template + helpers

  - Crie schema _tenant_template com TODAS as tabelas tenant-scoped clonadas SEM a coluna tenant_id (compostos unique
  também perdem tenant_id). Inclua índices, FKs locais, sequences, constraints.
  - Crie helpers em public:
    - tenant_schema_name(slug text) → text (IMMUTABLE) — converte slug→nome de schema sanitizado.
    - tenant_schema_for(tenant_id uuid) → text (STABLE) — busca slug e devolve schema.
    - tenant_id_for_schema(schema text) → uuid (STABLE) — inverso. CRÍTICO pra triggers que precisam descobrir o
  tenant_id (porque a coluna não existe mais nas tabelas tenant).
    - current_tenant_schema() → text (STABLE SECURITY DEFINER) — lê profiles.tenant_id do auth.uid() e devolve o schema
  dele.
    - clone_tenant_template(slug) → void (SECURITY DEFINER) — clona o template pra um schema novo.
    - drop_tenant_schema(tenant_id) → void — proteção: assert que target LIKE 'tenant_%' antes de DROP CASCADE.

  F2 — Provisionamento

  - Adapte sua função/edge provision_from_intent (ou equivalente) pra chamar clone_tenant_template(slug) quando criar
  tenant novo.
  - Confirme que policies padrão são criadas no schema clonado (uma policy tenant_member_full TO authenticated filtrando
   por profiles.tenant_id = '<id-do-tenant>').

  F3 — Frontend: composable de acesso tenant

  - Crie useTenantDb.js:
  export function useTenantDb() {
    const { perfil } = useAuth();
    const schemaName = computed(() => tenantSchemaName(perfil.value?.tenant_slug));
    const isReady = computed(() => Boolean(schemaName.value));
    function db() {
      if (!schemaName.value) throw new Error('tenant não disponível');
      return supabase.schema(schemaName.value);
    }
    return { db, schemaName, isReady };
  }
  - Faça find/replace amplo: supabase.from('<tenant_table>') → db().from('<tenant_table>') em todas as
  views/components/composables que tocam tabelas tenant.

  F4 — Edge functions

  Padrão pra qualquer edge function que precisa acessar tabela tenant:
  const userClient = createClient(SUPABASE_URL, ANON_KEY, {
    global: { headers: { Authorization: authHeader } }
  });
  const { data: tenantSchema } = await userClient.rpc('current_tenant_schema');
  const tenantDb = userClient.schema(tenantSchema as string);
  await tenantDb.from('oficios').update(...).eq(...);
  Tabelas globais (profiles, tenants, addon_*, support_*, etc.) seguem usando userClient.from(...) direto.

  F5 — Expor schemas no PostgREST

  Edite supabase/config.toml:
  [api]
  schemas = ["public", "graphql_public", "tenant_<slug1>", "tenant_<slug2>", ...]
  extra_search_path = ["public", "extensions"]
  Restart Supabase. Toda criação de tenant novo precisa atualizar este array e restartar PostgREST — automatize via
  migration que regenera config.toml, ou aceite gerenciamento manual.

  F6 — Rewrite funções + drop tabelas em public (a fase mais perigosa)

  Divida em lotes pequenos e teste cada um:

  Lote 1 — split de notifications

  Caso especial crítico. Antes do split, identifique:
  - Tipos de notif que cruzam tenants (dev recebe de todos os tenants, support_reply enviado pelo dev pro tenant,
  system_alert global).
  - Tipos que são puramente tenant-local (voucher_gerado, os_atribuida, oficio_assinado, prazos).

  Decisão estrutural: notifications precisa virar duas tabelas:
  - tenant_<slug>.notifications — locais do tenant.
  - public.notifications_sistema — cross-tenant (SaaS pro tenant, ou pro dev).

  Migration faz:
  1. Cria public.notifications_sistema (mesma estrutura + RLS própria + adiciona à publication realtime).
  2. Migra dados: INSERT INTO notifications_sistema SELECT ... WHERE type IN (cross_tenant_types), depois loop por
  tenant INSERT INTO tenant_X.notifications SELECT ... WHERE tenant_id = X AND type IN (local_types).
  3. Refatora todas as funções de notif (notify_user, notify_user_sistema, notify_tenant_admins, notify_all_devs,
  mark/archive_*) — duas variantes (_sistema_ em public, outras EXECUTE format pro schema tenant).
  4. DROP TABLE public.notifications.
  5. Frontend useNotifications.js: lê das duas fontes em paralelo, mescla por created_at DESC, cada item ganha campo
  _origem: 'tenant' | 'sistema'. Realtime em 2 canais. markRead/archive roteiam pra RPC correta via _origem.

  Lote 2-4 — refator das demais funções

  Padrão pra TRIGGER em tabela tenant:
  CREATE OR REPLACE FUNCTION public.trg_xxx() RETURNS trigger
  LANGUAGE plpgsql SECURITY DEFINER
  SET search_path TO 'public', 'pg_temp'
  AS $$
  DECLARE v_tenant_id uuid;
  BEGIN
      PERFORM set_config('search_path', TG_TABLE_SCHEMA || ',public,pg_temp', true);
      v_tenant_id := public.tenant_id_for_schema(TG_TABLE_SCHEMA);  -- só se precisar
      -- ... lógica com tabelas tenant SEM prefixo `public.` ...
  END $$;

  Padrão pra RPC chamada por user logado em um tenant:
  CREATE OR REPLACE FUNCTION public.minha_rpc(...) RETURNS ...
  LANGUAGE plpgsql SECURITY DEFINER
  SET search_path TO 'public', 'pg_temp'
  AS $$
  DECLARE v_schema text := public.current_tenant_schema();
  BEGIN
      IF v_schema IS NULL THEN RAISE EXCEPTION 'sem tenant'; END IF;
      PERFORM set_config('search_path', v_schema || ',public,pg_temp', true);
      -- ... lógica ...
  END $$;

  Padrão pra RPC global (cron, dev, varre múltiplos tenants):
  FOR t_row IN SELECT id, slug FROM public.tenants WHERE ativo = true LOOP
      v_schema := public.tenant_schema_name(t_row.slug);
      IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = v_schema) THEN CONTINUE; END IF;
      EXECUTE format('UPDATE %I.tabela ...', v_schema);
  END LOOP;

  Padrão pra função que escreve no schema de OUTRO tenant (notify_user com p_tenant_id, etc.):
  v_schema := public.tenant_schema_for(p_tenant_id);
  IF v_schema NOT LIKE 'tenant_%' THEN RETURN; END IF;
  EXECUTE format('INSERT INTO %I.notifications (...) VALUES ($1, $2, ...)', v_schema)
      USING ...;

  Lote 4.5 — migração de DADOS (esqueci de avisar primeiro, vai se ferrar)

  ESSE É O ERRO MAIS COMUM: o template clona estrutura, mas você esquece dos DADOS. Depois descobre que
  tenant_sindspam.os está vazio porque você nunca migrou. Faça uma migration que:

  SET session_replication_role = replica;  -- desabilita FK checks
  DO $$
  DECLARE
      tenant_id_target uuid := '...';
      tenant_schema text := 'tenant_...';
      tabs text[] := ARRAY[/* lista */];
      t text;
      v_cols text;
  BEGIN
      FOREACH t IN ARRAY tabs LOOP
          -- Lista colunas do schema tenant (sem tenant_id já)
          SELECT string_agg(quote_ident(column_name), ', ' ORDER BY ordinal_position)
            INTO v_cols
            FROM information_schema.columns
           WHERE table_schema = tenant_schema AND table_name = t;
          IF EXISTS (SELECT 1 FROM information_schema.columns
                      WHERE table_schema='public' AND table_name=t AND column_name='tenant_id') THEN
              EXECUTE format(
                  'INSERT INTO %I.%I (%s) SELECT %s FROM public.%I WHERE tenant_id = %L ON CONFLICT DO NOTHING',
                  tenant_schema, t, v_cols, v_cols, t, tenant_id_target);
          ELSE
              EXECUTE format(
                  'INSERT INTO %I.%I (%s) SELECT %s FROM public.%I ON CONFLICT DO NOTHING',
                  tenant_schema, t, v_cols, v_cols, t);
          END IF;
      END LOOP;
  END $$;
  -- Reset sequences:
  FOR r IN SELECT t.table_name, c.column_name FROM information_schema.tables t
           JOIN information_schema.columns c ON c.table_schema=t.table_schema AND c.table_name=t.table_name
           WHERE t.table_schema=tenant_schema AND c.data_type='bigint' AND c.column_default LIKE 'nextval(%' LOOP
      v_seq := pg_get_serial_sequence(format('%I.%I', tenant_schema, r.table_name), r.column_name);
      EXECUTE format('SELECT setval(%L, COALESCE((SELECT MAX(%I) FROM %I.%I), 0))',
                     v_seq, r.column_name, tenant_schema, r.table_name);
  END LOOP;
  SET session_replication_role = origin;

  Lote 5 — DROP CASCADE das tabelas em public

  Só depois de TODAS as funções refatoradas e dados migrados:
  SET session_replication_role = replica;
  DO $$ BEGIN
      FOREACH t IN ARRAY tabs LOOP
          IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema='public' AND table_name=t) THEN
              EXECUTE format('DROP TABLE public.%I CASCADE', t);
          END IF;
      END LOOP;
  END $$;
  SET session_replication_role = origin;

  Limitações conhecidas e workarounds

  1. PostgREST não suporta embed FK cross-schema

  Você vai pagar esse pato. O PostgREST 14.x não consegue resolver embeds tipo db().from('os').select('*,
  profiles!os_solicitante_profile_id_fkey(nome)') quando os está em tenant_X e profiles em public, mesmo com FK física
  existindo. Mensagem: PGRST200: Could not find a relationship between 'os' and 'profiles' in the schema cache.

  Solução: helper de "fake embed" no frontend. Crie useProfileEmbed.js:
  export async function attachProfiles(rows, mappings, columns = 'id, nome, email, role') {
      if (!rows?.length) return rows;
      const allIds = new Set();
      for (const m of mappings) rows.forEach(r => { if (r?.[m.idField]) allIds.add(r[m.idField]); });
      const { data } = await supabase.from('profiles').select(columns).in('id', [...allIds]);
      const map = new Map((data || []).map(p => [p.id, p]));
      return rows.map(r => {
          const out = { ...r };
          for (const m of mappings) out[m.alias] = r?.[m.idField] ? map.get(r[m.idField]) || null : null;
          return out;
      });
  }
  // Variantes: attachProfilesNested(rows, nestedKey, mappings), attachProfilesById(rows, idField, alias)
  Faz 2 queries + merge em JS. Toda tela que tinha profiles!fkey(...) precisa virar duas queries + attach.

  2. %ROWTYPE de tabelas tenant

  Funções que declaravam v_plano public.convenio_planos%ROWTYPE quebram quando a tabela some do public. Troque por
  RECORD em todas. Quando precisar retornar tabela (RETURNS os_problemas), troque por RETURNS jsonb e construa via
  jsonb_build_object(...).

  3. SQL functions com SET search_path TO 'public' declarado

  Algumas funções são LANGUAGE sql com declaração estática SET search_path TO 'public'. Não dá pra usar set_config
  dinâmico em SQL puro. Converta pra LANGUAGE plpgsql. Atenção: isso exige DROP + CREATE (CREATE OR REPLACE não muda
  linguagem) → se tiver policy dependendo da função, drope a policy primeiro.

  4. Triggers de notif que filtram cada destinatário

  notify_tenant_admins insere em múltiplos owners via SELECT ... FROM profiles WHERE role IN (...). Pra respeitar
  preferências individuais, adicione AND public.should_notify(p.id, p_type) no WHERE.

  5. Realtime

  - A tabela notifications_sistema precisa ser adicionada explicitamente à publication: ALTER PUBLICATION
  supabase_realtime ADD TABLE public.notifications_sistema.
  - Canais realtime no frontend precisam do schema correto: { event: '*', schema: 'tenant_<slug>', table:
  'notifications', filter: 'owner_id=eq.X' } — não mais schema: 'public'.

  6. Filtros .eq('tenant_id', X) no frontend

  Após o split, qualquer db().from('tabela_tenant').eq('tenant_id', X) quebra com column tenant_id does not exist — a
  coluna sumiu. Faça grep e remova esses filtros (o isolamento agora é pelo schema). Mantenha em tabelas que ficam em
  public (tenant_features, tenant_audit_log, profiles).

  7. session_replication_role na migração de dados

  INSERTs em massa com FKs entre tabelas tenant podem falhar por ordem topológica. SET session_replication_role =
  replica desabilita checks de FK durante o INSERT. Lembre de voltar pra origin ao final.

  8. Reset de sequences

  Tabelas tenant com id bigint generated by sequence precisam de setval pós-migração — senão próximo INSERT vai colidir
  com PKs existentes.

  9. Policies que usam funções refatoradas

  unidade_in_current_tenant(uuid) aparecia como USING (...) em policies de public.prestador_unidade_acessos. Antes de
  DROP+CREATE da função, dropei as 2 policies. Tabelas que vão sumir não precisam recriar policy. Se a função é usada em
   policies de tabelas que ficam, recrie a policy depois.

  10. FKs de tabelas que ficam em public apontando pras que saem

  Antes de DROP, rode query pra detectar. Se houver, decida: migra a tabela referenciadora pro tenant também, ou
  converte FK pra coluna solta sem constraint.

  Frontend — refactor sistemático

  1. Find/replace em massa: supabase.from('<lista_tabelas_tenant>') → db().from(...). Importe useTenantDb.
  2. Caça por .eq('tenant_id': remova nos from('<tenant_table>'), mantenha nos from('<public_table>').
  3. Caça por embed profiles!fkey(...) em queries de tabelas tenant: refatore com attachProfiles.
  4. Caça por subscribeRealtime com schema: 'public' pra tabelas que viraram tenant — troque pra schema:
  tenantSchemaName(slug).
  5. Composables/serviços que usam supabase.from(...) em vez de db() direto: idem.

  Backups e segurança

  Sempre faça backup antes de cada lote:
  docker exec supabase_db_<projeto> pg_dump -U postgres -d postgres --schema=public --no-owner --no-acl >
  backups/pre-loteN/public.sql
  docker exec supabase_db_<projeto> pg_dump -U postgres -d postgres --schema=tenant_<slug> --no-owner --no-acl >
  backups/pre-loteN/tenant_<slug>.sql

  Pra recarregar cache do PostgREST após mudanças:
  docker exec supabase_db_<projeto> psql -U postgres -d postgres -c "NOTIFY pgrst, 'reload schema'"

  Se mudou config.toml (schemas expostos), restart obrigatório:
  docker restart supabase_rest_<projeto>

  Checklist final por lote

  Antes de marcar um lote como concluído:
  - Migration aplica sem erro (psql -v ON_ERROR_STOP=1)
  - Smoke test SQL chamando as funções refatoradas via SET LOCAL request.jwt.claim.sub
  - NOTIFY pgrst, 'reload schema' rodado
  - Usuário testou as telas do FE que tocam essas funções
  - Sem erros novos no console do navegador (network 4xx/5xx, PGRST200, etc.)

  Como interagir comigo durante o trabalho

  - Antes de codar qualquer fase, mostre o plano resumido e pergunte se prossegue.
  - Para decisões estruturais (ex: notifications split, função X retorna jsonb ou record composto, drop CASCADE de
  policy órfã), use perguntas múltipla escolha — não decida sozinho.
  - Ao terminar um lote, sumarize o que mudou + lista de coisas pra eu testar no FE.
  - Não confie em listas pré-feitas (suas ou do usuário). Sempre re-confirme via query no banco.
  - Backup antes de cada DROP destrutivo.
  - PostgREST cache é teimoso — NOTIFY pgrst resolve tabelas/funções; restart do container pra mudanças de config.toml.
