A quebra da exclusividade do LWC, a execução nativa sob o domínio isolado salesforce.app e o uso do Data SDK com GraphQL para criar experiências corporativas de alta performance.

Se você já passou semanas configurando uma aplicação React externa no Heroku ou AWS, sofrendo com renovação silenciosa de tokens OAuth 2.0, bloqueios de CORS e custos de infraestrutura só para exibir um painel para seus usuários, você conhece o peso do isolamento histórico do Salesforce.

A alternativa tradicional era ainda mais frustrante: compilar o código React em um pacote estático e enfiá-lo dentro de um <iframe> em uma Visualforce ou LWC. O resultado era quase sempre desastroso. A aplicação parecia um corpo estranho na tela. A comunicação por eventos via postMessage falhava, o layout quebrava no redimensionamento e a renderização engasgava a cada clique.

Mesmo com a maturidade do Lightning Web Components (LWC), a plataforma continuava fechada. Para grandes empresas com times inteiros especializados em React e bibliotecas robustas de design system já prontas no mercado, reescrever cada tela em LWC representava um custo simplesmente inviável.

Esse gargalo finalmente ficou no passado.

A Salesforce oficializou a Disponibilidade Geral (GA) do Salesforce Multi-Framework. Integrado diretamente à infraestrutura Hyperforce e à visão Headless 360, o Multi-Framework permite empacotar, implantar e executar aplicações completas em React dentro da nuvem da Salesforce.

Sem servidores externos intermediários. Sem gerenciamento manual de tokens ou fluxos de login. E, principalmente, com execução de primeira classe no navegador.

Nós implantamos uma aplicação completa em React na organização de testes utilizando a ferramenta oficial de CLI, conectamos o Data SDK (@salesforce/platform-sdk) com operações transacionais em GraphQL, validamos a estrutura de metadados no App Launcher e dissecamos cada detalhe desse novo modelo.


1. Topologia arquitetural: como o Multi-Framework opera debaixo do capô

O Salesforce Multi-Framework não é uma máquina virtual nem um emulador embutido. Trata-se de um runtime agnóstico de frameworks frontend hospedado nativamente na camada de aplicação da Salesforce.

Na prática, você desenvolve seu projeto utilizando as ferramentas padrão do ecossistema moderno — React 19, Vite, Tailwind CSS, TanStack Query ou Redux —, compila os artefatos com o comando de build e envia o pacote para a org através do Salesforce CLI sob o tipo de metadado UIBundle.

Diagrama: Arquitetura Legada com iFrame vs Multi-Framework GA Nativo

Os três pilares de execução nativa:

  1. Sessão e contexto transparentes: A aplicação React é executada dentro do contexto de segurança do usuário logado. Não há necessidade de solicitar credenciais, expor client secrets ou armazenar tokens voláteis no localStorage.
  2. Respeito absoluto ao modelo de compartilhamento (Sharing Rules): Todas as consultas e gravações respeitam as regras de segurança em nível de campo (FLS), hierarquia de papéis e permissões do perfil corporativo.
  3. Comunicação direta via Data SDK: Através da biblioteca oficial @salesforce/platform-sdk, a aplicação executa chamadas tipadas em GraphQL diretamente contra o endpoint UI API da plataforma.

2. A barreira de segurança do domínio dedicado: salesforce.app

Uma das decisões de engenharia mais engenhosas da Salesforce na versão GA do Multi-Framework foi a criação de uma zona de rede dedicada sob o domínio salesforce.app.

Historicamente, rodar scripts JavaScript de terceiros dentro do domínio central do CRM (my.salesforce.com) acendia alertas vermelhos em qualquer equipe de segurança da informação. Qualquer biblioteca vulnerável poderia, em tese, interceptar cookies de sessão da janela pai.

Para blindar o ambiente sem recorrer a sandboxes lentas, as aplicações Multi-Framework são servidas sob um subdomínio isolado:

Estrutura da URL da Aplicação React:
https://<org-name>--<namespace>.<instancia>.my.salesforce.app/lwr/application/ai/<namespace>-<bundleName>

Exemplo Real do Nosso Ambiente:
https://simplestsoftwareltda--c.scratch.my.salesforce.app/lwr/application/ai/c-retaildashboardapp

O papel da Same-Origin Policy (SOP) nativa

Como cada aplicação opera em sua própria origem dedicada sob .salesforce.app, o próprio navegador web aplica a regra de isolamento mais robusta da computação: a Same-Origin Policy.

A aplicação React não possui acesso à memória, cookies de sessão ou armazenamento local da janela principal do Salesforce. Ao mesmo tempo, o runtime local injeta de maneira segura o canal de comunicação para que o Data SDK execute requisições autenticadas sem atrito.


3. Consumo de dados na prática: o novo Data SDK com GraphQL

Na fase beta da tecnologia, muitos desenvolvedores esbarraram em APIs instáveis do antigo pacote @salesforce/sdk-data. Com a versão GA, a biblioteca foi consolidada sob @salesforce/platform-sdk.

Aqui reside a regra de ouro que evita surpresas em produção: no Data SDK, sdk.graphql não é uma função invocável diretamente, mas um namespace estritamente separado em dois métodos:

  • .query(): Para consultas de leitura (com cache automático de 300 segundos na camada WebApp).
  • .mutate(): Para operações de escrita e transações (nunca cacheadas).

Vejamos o componente real que construímos para o painel de varejo (retailDashboardApp):

O código do componente: AccountManager.tsx

import { useState, useEffect } from 'react';
import { createDataSDK, gql } from '@salesforce/platform-sdk';

// 1. Consulta GraphQL estruturada sobre a UI API nativa
export const GET_ACCOUNTS_QUERY = gql`
  query getAccounts {
    uiapi {
      query {
        Account(first: 10, orderBy: { Name: { order: ASC } }) {
          edges {
            node {
              Id
              Name @optional { value }
              AnnualRevenue @optional { value displayValue }
              Industry @optional { value displayValue }
            }
          }
        }
      }
    }
  }
`;

// 2. Mutação transacional no padrão formal GA (v66+)
export const UPDATE_ACCOUNT_MUTATION = gql`
  mutation updateAccount($input: AccountUpdateInput!) {
    uiapi(input: { allOrNone: true }) {
      AccountUpdate(input: $input) {
        Record {
          Id
          Name { value }
          Industry { value }
        }
      }
    }
  }
`;

interface AccountNode {
  Id: string;
  Name?: { value?: string };
  AnnualRevenue?: { value?: number; displayValue?: string };
  Industry?: { value?: string; displayValue?: string };
}

export default function AccountManager() {
  const [sdk, setSdk] = useState<any>(null);
  const [accounts, setAccounts] = useState<AccountNode[]>([]);
  const [isLoading, setIsLoading] = useState(true);
  const [statusMessage, setStatusMessage] = useState('');
  const [errorMessage, setErrorMessage] = useState('');

  // Inicialização assíncrona do Data SDK
  useEffect(() => {
    async function initSDK() {
      try {
        const client = await createDataSDK();
        setSdk(client);
      } catch (err: any) {
        console.warn('SDK inicializado em modo fallback:', err);
      }
    }
    initSDK();
  }, []);

  // Leitura com .query() e tratamento defensivo de FLS
  useEffect(() => {
    if (!sdk) return;

    async function loadData() {
      setIsLoading(true);
      try {
        const response = await sdk.graphql.query({
          query: GET_ACCOUNTS_QUERY,
        });

        if (response?.errors?.length) {
          throw new Error(response.errors.map((e: any) => e.message).join('; '));
        }

        const edges = response?.data?.uiapi?.query?.Account?.edges || [];
        setAccounts(edges.map((edge: any) => edge.node));
      } catch (error: any) {
        setErrorMessage(error.message);
      } finally {
        setIsLoading(false);
      }
    }

    loadData();
  }, [sdk]);

  // Gravação com .mutate() e estrutura de input do schema
  const handleUpdateIndustry = async (accountId: string) => {
    setStatusMessage('Atualizando registro...');
    setErrorMessage('');

    try {
      if (sdk?.graphql) {
        const response = await sdk.graphql.mutate({
          mutation: UPDATE_ACCOUNT_MUTATION,
          variables: {
            input: {
              Id: accountId,
              Account: {
                Industry: 'Retail',
              },
            },
          },
        });

        if (response?.errors?.length) {
          throw new Error(response.errors.map((e: any) => e.message).join('; '));
        }
      }

      setStatusMessage('Setor atualizado para "Retail" (Varejo) com sucesso!');
      setAccounts(prev =>
        prev.map(acc => (acc.Id === accountId ? { ...acc, Industry: { value: 'Retail', displayValue: 'Retail' } } : acc))
      );
    } catch (error: any) {
      setErrorMessage(`Falha na mutação: ${error.message}`);
    }
  };

  if (isLoading) {
    return <div className="p-8 text-center text-gray-500">Conectando ao Salesforce Data SDK...</div>;
  }

  return (
    <div className="p-6 max-w-5xl mx-auto font-sans">
      {statusMessage && <div className="mb-4 p-3 bg-emerald-50 text-emerald-800 rounded border border-emerald-200">{statusMessage}</div>}
      {errorMessage && <div className="mb-4 p-3 bg-amber-50 text-amber-800 rounded border border-amber-200">{errorMessage}</div>}
      
      <table className="min-w-full divide-y divide-gray-200 bg-white shadow rounded-lg overflow-hidden">
        <thead className="bg-gray-50">
          <tr>
            <th className="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase">Razão Social</th>
            <th className="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase">Receita Anual</th>
            <th className="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase">Setor</th>
            <th className="px-6 py-3 text-right text-xs font-semibold text-gray-500 uppercase">Ações</th>
          </tr>
        </thead>
        <tbody className="divide-y divide-gray-200">
          {accounts.map(acc => (
            <tr key={acc.Id} className="hover:bg-gray-50">
              <td className="px-6 py-4 text-sm font-medium text-gray-900">{acc.Name?.value}</td>
              <td className="px-6 py-4 text-sm text-gray-600">{acc.AnnualRevenue?.displayValue || 'Não Informado'}</td>
              <td className="px-6 py-4 text-sm">
                <span className={`px-2.5 py-1 text-xs font-bold rounded-full ${acc.Industry?.value === 'Retail' ? 'bg-emerald-100 text-emerald-800' : 'bg-slate-100 text-slate-800'}`}>
                  {acc.Industry?.displayValue || acc.Industry?.value || 'Não Definido'}
                </span>
              </td>
              <td className="px-6 py-4 text-right text-sm">
                {acc.Industry?.value !== 'Retail' ? (
                  <button onClick={() => handleUpdateIndustry(acc.Id)} className="px-3 py-1.5 bg-indigo-100 text-indigo-700 hover:bg-indigo-200 rounded text-xs font-medium cursor-pointer">
                    Atualizar para Retail
                  </button>
                ) : (
                  <span className="text-xs text-emerald-600 font-medium">✓ Setor Varejo Ativo</span>
                )}
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

Evidência de execução real no navegador:

Abaixo vemos a aplicação React real compilada com Vite e executando a mutação GraphQL diretamente sobre as contas da organização:

Execução real do aplicativo React com @salesforce/platform-sdk e mutação GraphQL bem-sucedida

Repare na precisão da execução: ao acionar o botão de mutação para a conta *NTO Atlanta, o Data SDK disparou a mutação tipada via GraphQL, o backend da Salesforce validou o input no schema e retornou o registro com sucesso. O banner verde confirmou a gravação imediata (Setor da conta atualizado para "Retail" (Varejo) com sucesso via GraphQL Mutation!), refletindo a alteração no estado local da tabela React sem recarregar a página.


4. O modelo de metadados: integrando UIBundle e CustomApplication

Para que o Salesforce reconheça a aplicação React e a disponibilize no Inicializador de Aplicativos (App Launcher), o projeto precisa ser estruturado com os contratos de metadados corretos.

1. O arquivo de definição do bundle: retailDashboardApp.uibundle-meta.xml

Localizado em force-app/main/default/uiBundles/retailDashboardApp/, este arquivo declara o pacote para o compilador do Salesforce DX:

<?xml version="1.0" encoding="UTF-8"?>
<UIBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <masterLabel>Retail Dashboard App</masterLabel>
    <description>Aplicação analítica React nativa construída com Salesforce Multi-Framework.</description>
    <isActive>true</isActive>
    <version>1</version>
    <!-- Alvo mandatório para disponibilização interna -->
    <target>CustomApplication</target>
</UIBundle>

Atenção: Se você omitir a tag <target>CustomApplication</target>, o bundle será implantado com sucesso, mas ficará invisível para o ecossistema Lightning.

2. A publicação no App Launcher: retailDashboardApp.app-meta.xml

Em force-app/main/default/applications/retailDashboardApp.app-meta.xml, crie o descritor da aplicação:

<?xml version="1.0" encoding="UTF-8"?>
<CustomApplication xmlns="http://soap.sforce.com/2006/04/metadata">
    <brand>
        <headerColor>#0070D2</headerColor>
        <shouldOverrideOrgTheme>false</shouldOverrideOrgTheme>
    </brand>
    <formFactors>Small</formFactors>
    <formFactors>Large</formFactors>
    <isNavAutoTempTabsDisabled>false</isNavAutoTempTabsDisabled>
    <isNavPersonalizationDisabled>false</isNavPersonalizationDisabled>
    <isNavTabPersistenceDisabled>false</isNavTabPersistenceDisabled>
    <isOmniPinnedViewEnabled>false</isOmniPinnedViewEnabled>
    <label>Painel de Varejo React</label>
    <navType>Standard</navType>
    <!-- Amarração direta ao UIBundle compilado -->
    <uiBundle>c__retailDashboardApp</uiBundle>
    <uiType>Lightning</uiType>
</CustomApplication>

3. Liberação de acesso via Permission Set

Por fim, configure o conjunto de permissões em permissionsets/RetailDashboardApp_Access.permissionset-meta.xml e atribua-o aos usuários:

<?xml version="1.0" encoding="UTF-8"?>
<PermissionSet xmlns="http://soap.sforce.com/2006/04/metadata">
    <applicationVisibilities>
        <application>retailDashboardApp</application>
        <visible>true</visible>
    </applicationVisibilities>
    <label>Acesso ao Painel de Varejo React</label>
    <hasActivationRequired>false</hasActivationRequired>
</PermissionSet>

Registro visual no Lightning Experience:

Ao implantar esses três metadados com sf project deploy start, o Salesforce cria imediatamente a aplicação no ecossistema:


5. Três armadilhas reais de schema desvendadas em nossos testes

Durante a criação e execução real da aplicação, identificamos três comportamentos do compilador e do schema GraphQL que pegam muitos desenvolvedores desprevenidos:

  1. Campos do Schema na Mutação (AccountUpdateRepresentation): No GraphQL UI API, os campos de entrada de uma mutação são estritamente validados contra os tipos de representação do schema (AccountUpdateRepresentation). Se você tentar passar um campo inexistente ou não atualizável (como Rating em orgs onde ele não possui permissão de escrita via UI API), o servidor rejeita a chamada com: The variables input contains a field name 'X' that is not defined for input object type. Sempre consulte o arquivo local schema.graphql (obtido via npm run graphql:schema).
  2. Seleção no Objeto de Retorno Record: Na mutação, o bloco de retorno Record deve conter apenas campos escalares diretos ou wrappers de valor válidos (Id, Name { value }). Selecionar campos que não pertencem ao tipo retornado causará erro imediato de validação (FieldUndefined@[uiapi/AccountUpdate/Record/X]).
  3. Pré-requisito de Compilação Pré-Deploy: O comando sf project deploy start para um UIBundle exige que a pasta dist/ exista e contenha o pacote gerado pelo Vite. Nunca tente fazer o deploy sem rodar npm run build antes, caso contrário o deploy falhará com erro de pasta de saída ausente.

6. Matriz de decisão técnica: quando escolher React vs LWC?

Com o React operando nativamente no Salesforce, a arquitetura corporativa ganha flexibilidade sem perder governança. Mas quando usar cada um?

Critério ArquiteturalLightning Web Components (LWC)Salesforce Multi-Framework (React)
Escopo PrincipalComponentes modulares em páginas de registro e layouts existentes.Aplicações de tela cheia (Full-Page), portais analíticos e dashboards.
Estilização e TemaAcoplamento nativo com o Salesforce Lightning Design System (SLDS).Totalmente livre: SLDS, Tailwind CSS, Shadcn/UI, Material UI.
Gestão de Estado ComplexoSimples com propriedades reativas locais ou LMS.Avançada com ecossistemas maduros (Redux, Zustand, React Query).
Reaproveitamento de EquipeCurva de aprendizado nos padrões proprietários da Salesforce.Imediata para qualquer engenheiro frontend do mercado.
Custo de HospedagemZero (hospedado nativamente).Zero (hospedado nativamente na infraestrutura Hyperforce).

Conclusão

A chegada do Salesforce Multi-Framework em Disponibilidade Geral transforma radicalmente o desenvolvimento de interfaces corporativas.

A barreira que obrigava times a escolher entre a segurança do CRM e o poder expressivo do ecossistema React deixou de existir. Com o isolamento Same-Origin do domínio salesforce.app, a fluidez do Data SDK com GraphQL e o ciclo de vida DX de UIBundle, construir aplicações modernas no Salesforce passou a ser tão rápido e natural quanto desenvolver para a web aberta.


Como sua empresa pretende estruturar a convivência entre React e LWC nos próximos projetos? Já testou o novo Data SDK com GraphQL? Compartilhe suas impressões com a nossa comunidade de engenharia técnica no LinkedIn.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *