Assinaturas como fonte de verdade: área de conta headless com Hydrogen e Recharge — Blog AM Lab Ir para o conteúdo
Hydrogen · Recharge 22 set 2026 · 14 min de leitura

Assinaturas como fonte de verdade

Como a área de conta de uma loja headless foi construída direto sobre o modelo de assinaturas do Recharge: listar, editar, cancelar, reativar e recomprar, sem banco de dados no meio. Com o roteiro de montagem no fim.

Shopify Hydrogen React Router 7 Oxygen / Workers Recharge REST Customer Account API Storefront API

01Por que o centro é a assinatura e não o pedido

Na maioria das lojas headless, a área de conta gira em torno do pedido: uma lista do que já foi comprado, com rastreio e recibo. Para um negócio de assinatura recorrente com produto personalizado, isso resolve pouca coisa. O cliente entra na conta para mexer no que ainda vai acontecer: adiar a entrega do mês que vem, trocar um produto, mudar a fragrância.

Então a assinatura do Recharge virou o objeto central do projeto. A Shopify continua dona do catálogo, da autenticação e do checkout, mas quem responde o que esse cliente tem contratado hoje é o Recharge. A interface inteira é uma leitura desse modelo, e toda ação do cliente termina numa escrita nele. Não tem banco intermediário nem cópia local do estado.

O recorte: só a área de conta saiu do tema. Vitrine, página de produto e checkout continuaram na loja, no domínio principal; a conta ganhou um subdomínio próprio servido pelo Oxygen. Isso mantém o checkout da Shopify intacto e reduz muito o escopo, mas cria uma fronteira que reaparece várias vezes daqui para frente, sempre que a conta precisa mandar o cliente comprar alguma coisa.

02Três fontes de dados, um loader

A aplicação roda em Hydrogen com React Router 7, hospedada no Oxygen, que na prática é um Cloudflare Worker. Quem fala com o mundo externo é o loader da rota de conta: ele pega o e-mail do cliente autenticado, resolve o ID correspondente no Recharge e busca assinaturas, cobranças agendadas, métodos de pagamento e endereços. Em paralelo saem as queries da Shopify.

O resultado desce inteiro pelo contexto do Outlet. As telas de assinaturas, edição, reativação e recompra não buscam estado por conta própria: todas leem a mesma hidratação. Isso deixa o loader mais pesado, mas evita quatro telas exibindo versões diferentes da mesma informação.

Customer Account APIIDENTIDADE · PEDIDOS
Storefront APICATÁLOGO · METAFIELDS
Recharge RESTASSINATURAS · CHARGES
loader da conta — Oxygen WorkerTOKENS NÃO SAEM DAQUI
Minhas assinaturas
Editar assinatura
Reativar
Comprar de novo

O token administrativo do Recharge fica no worker e não sai de lá. O browser só conversa com rotas da própria aplicação.

03Uma assinatura por item de linha

O Recharge cria uma assinatura para cada item. Um cliente com shampoo, condicionador e leave-in na mesma entrega tem três assinaturas, cada uma com seu ciclo e sua data de cobrança. Só que ninguém pensa no próprio pedido assim: o cliente pensa na caixa que chega dia 14.

Juntar as três de volta num card é o trabalho de modelagem mais importante da tela, e a chave tem duas partes:

A cobrança agendadaA charge à qual a assinatura pertence é o que o Recharge já trata como um pedido futuro.
O perfil de fórmulaLido das propriedades de item de linha que o fluxo de personalização grava: __profile_id e __profile_name.

Quando não existe cobrança agendada, no caso de uma assinatura pausada por exemplo, caímos num fallback por endereço mais data agendada. É como o próprio Recharge agrupa uma entrega.

Um detalhe que só aparece em produção: propriedades escritas por versões diferentes do fluxo de compra convivem no mesmo cliente, em formatos que não batem entre si. O agrupamento reconhece as duas formas e normaliza antes de montar o card.

04Salvar uma edição escreve três listas

Na tela de edição o cliente faz coisas que parecem simples: troca um produto, adiciona outro, muda a frequência, tira um item. No modelo, quase nenhuma delas é uma operação só. Trocar produto é criar uma assinatura e cancelar outra. Mudar data e frequência é atualizar todas as assinaturas daquela entrega ao mesmo tempo, senão a caixa se parte em duas.

A tela não dispara nada enquanto o cliente mexe. Ela acumula as intenções e manda um envelope só quando ele salva:

POST /api/subscriptionService
{
  "subsToCreate": [ /* produtos adicionados ou substitutos */ ],
  "subsToUpdate": [ /* data, frequência, quantidade        */ ],
  "subsToCancel": [ /* removidos e trocados                */ ]
}
subsToCreateProduto novo na entrega, ou o lado novo de uma substituição. Herda endereço, ciclo e data da assinatura de referência.
subsToUpdateData de cobrança, frequência e quantidade. Vai sempre para o conjunto inteiro do card.
subsToCancelItem removido, ou o lado antigo de uma substituição, com o motivo de cancelamento junto.

Esse envelope vai para um serviço de escrita dedicado, autenticado com um JWT de vida curta. Como o runtime é um Worker, as bibliotecas de JWT feitas para Node não servem: o token é assinado com crypto.subtle (HMAC-SHA256) da Web Crypto API e guardado em memória até perto de vencer.

A vantagem de concentrar a escrita num envelope só aparece com o tempo. A ordem das operações e o agrupamento delas ficam num lugar; o componente de tela descreve o que o cliente quis e não precisa orquestrar a sequência.

05Regras que mudam sem deploy

Algumas regras mudam com frequência e não deveriam depender de deploy: até quando o cliente pode empurrar a próxima entrega dada a frequência dele, quais motivos de cancelamento a loja quer oferecer. Elas ficam em metafields da loja, editáveis no admin da Shopify.

custom.edit_subscriptions_frequency   → janela máxima por frequência
custom.account_cancellation_reasons   → motivos oferecidos ao cliente

O calendário na tela já limita as datas selecionáveis, para o cliente não descobrir o limite depois de salvar. Mas a mesma regra é conferida de novo no servidor, antes de a requisição seguir adiante. A checagem do cliente é conveniência; a do servidor é o que de fato vale. Como a regra é dinâmica, ela acaba tendo que existir nos dois lugares, lida da mesma origem.

A checagem do servidor falha fechada: se a configuração não puder ser lida ou interpretada, a operação é recusada. Uma guarda que existe para impedir adiamento indefinido não pode sumir justamente quando a configuração está indisponível.

06Reativar é conferir o catálogo de hoje

Uma assinatura cancelada há oito meses é uma foto do catálogo de oito meses atrás. O produto pode ter saído de linha, a cor pode ter sido aposentada, a fragrância pode ter sido reformulada. Reativar não é só ligar de volta, é checar se aquela foto ainda descreve alguma coisa que existe.

O produtoConsultado na Storefront API: precisa estar publicado, disponível para venda e sem marcação de rascunho ou não listado.
As preferênciasCor, fragrância e objetivo da fórmula, conferidos contra uma lista central de valores descontinuados, que inclui até fragrâncias retiradas apenas de alguns produtos.

O retorno é parcial de propósito. O que dá para reativar, reativa; o resto volta para a tela como uma lista de itens que precisam de uma escolha nova do cliente. Reativar cinco de sete e explicar os outros dois funciona melhor do que recusar o conjunto inteiro.

07Recomprar um produto que é único

Em catálogo comum, comprar de novo é jogar uma variante no carrinho. Com produto personalizado, a variante é só a casca: o que identifica o item são as propriedades de linha, cor, base, fragrância, objetivos, nome do perfil. Recomprar, aqui, é remontar um conjunto de propriedades que ainda seja válido.

Por isso o fluxo pergunta antes de mandar qualquer coisa para o carrinho. O cliente confirma os itens, escolhe a cor de cada produto e, se alguma preferência da fórmula original saiu de linha, substitui só aquele pedaço.

A entrega no carrinho é onde a fronteira de domínio aparece de forma mais concreta. O carrinho de conversão fica na loja, não na aplicação, e um fetch entre origens é barrado. Então a recompra sai como um POST de formulário para o endpoint de carrinho da loja, com cada propriedade num campo:

<input name="items[][id]"                        value="4838…">
<input name="items[][quantity]"                  value="1">
<input name="items[][properties][__color]"       value="Rosé">
<input name="items[][properties][__profile_name]" value="of Beauty">

É o caminho que o navegador permite e que a loja já entende sem nenhuma adaptação. O cliente cai no carrinho com a fórmula dele montada.

08A área de conta não cacheia nada

Hydrogen é feito para cachear bastante: página de produto, coleção, busca. A área de conta faz o contrário. Depois de mudar a data da entrega, o cliente tem que ver a data nova, não uma resposta de dez segundos atrás dizendo outra coisa.

As rotas de dados da conta respondem com no-store, a rota revalida sempre, e toda mutação bem-sucedida chama a revalidação do loader antes de a interface dizer que deu certo. O que aparece na tela vem sempre de uma leitura nova do Recharge: não usamos atualização otimista nesse fluxo.

É consequência direta da primeira decisão. Se o Recharge é a fonte de verdade, a tela não chuta o que acha que aconteceu; ela pergunta de novo.

09O roteiro, na ordem que funciona

Essa é a sequência que usamos, na ordem em que faz sentido fazer. Os três primeiros passos são chatos e não produzem nada visível, mas errar a ordem deles custa retrabalho: domínio, login e sessão dependem um do outro, e é ruim descobrir isso depois de já ter tela pronta.

01Decidir o que sai do temaNão precisa ser tudo. Aqui só a área de conta virou headless; vitrine, produto e checkout ficaram onde estavam. O checkout da Shopify continua intacto e o escopo fica muito menor, em troca de uma fronteira entre dois domínios.
02Criar o storefront Hydrogen no adminIsso gera o ID do storefront, os tokens da Storefront API e o canal de deploy no Oxygen. Com o projeto criado, npx shopify hydrogen env pull traz as variáveis para o .env local.
03Resolver o domínio antes de escrever telaA conta vive num subdomínio apontado para o storefront do Oxygen; a loja continua no domínio raiz. Vale fazer cedo porque o callback do login e a política de segurança de conteúdo dependem dele, e os dois ficam em variável, não em código.
04Ligar o login pela Customer Account APITrês rotas finas delegando para o cliente que o Hydrogen injeta no contexto: login, authorize e logout. No admin você cadastra o callback URI e o logout URI apontando para elas.A pegadinha que custa uma tarde: o OAuth da Shopify não redireciona para localhost. Você precisa de um túnel público e de registrar aquela URL nos mesmos campos, refazendo isso toda vez que o túnel muda de endereço.
05Fechar a sessão num cookie assinadoCookie httpOnly, sameSite: lax, assinado com um segredo de ambiente. O handler confere ao fim de cada requisição se a sessão foi mexida e só então devolve o Set-Cookie; sem isso o login funciona no primeiro acesso e some no segundo.
06Conectar o RechargeToken de Admin API com leitura e escrita em subscriptions, charges, addresses e payment_methods, com a versão fixada no header em vez do padrão da conta. A ligação entre o cliente logado e o cliente do Recharge é feita por e-mail: o loader resolve o ID uma vez e reaproveita. Esse token nunca é exposto ao browser, nem por rota de proxy transparente.
07Tirar do código o que o negócio mudaCriar os metafields de loja para as regras que mudam sem deploy e lê-los pela Storefront API. O ganho não é técnico: é o time de operação ajustar uma regra numa sexta à tarde sem abrir chamado.
08Pôr a escrita atrás de uma rota da própria aplicaçãoNenhuma chamada de escrita sai do browser direto para o Recharge. A tela posta para uma rota interna, que valida as regras de novo, assina um JWT de vida curta e repassa. No Worker isso é Web Crypto, não uma biblioteca de JWT de Node: metade dos pacotes populares depende de módulos que não existem nesse runtime.
09Montar o CSP com os dois domíniosO Hydrogen gera a política, mas você declara o domínio da loja e o do checkout e libera em connect-src o host do Recharge. Com GTM, 'strict-dynamic' evita listar cada tag de terceiro. Deixamos a política atrás de uma flag de ambiente para subir sem ela e ligar depois de ver o relatório.
10Deploy por ambiente e um login de teste reaproveitadoGitHub Actions rodando o deploy do Hydrogen, com token separado para staging e produção. Para o e2e, um projeto de setup do Playwright faz o login uma vez e salva o estado autenticado: como a entrada é por código enviado por e-mail, repetir o fluxo a cada teste não é viável.

No fim, o conjunto de variáveis de ambiente é um bom resumo do que precisa existir para tudo isso funcionar:

# Shopify
PUBLIC_STORE_DOMAIN
PUBLIC_CHECKOUT_DOMAIN
PUBLIC_STOREFRONT_ID
PUBLIC_STOREFRONT_API_TOKEN
PRIVATE_STOREFRONT_API_TOKEN
PUBLIC_CUSTOMER_ACCOUNT_API_CLIENT_ID
PUBLIC_CUSTOMER_ACCOUNT_API_URL
SHOP_ID
SESSION_SECRET              assina o cookie de sessão

# Recharge
RECHARGE_URL                base da API
RECHARGE_API_TOKEN          admin; só no worker

# Camada de escrita
SUBSCRIPTION_SERVICE_URL    endpoint que aplica o envelope
SECRET_KEY_GENERATE_JWT     segredo que assina o JWT

O que levamos desse projeto

O primeiro aprendizado é sobre disciplina: escolher uma fonte de verdade e não trair ela. A vontade de guardar uma cópia local do estado das assinaturas só para a tela abrir mais rápido é a origem de uma família inteira de bug em que a interface e a cobrança discordam, e esses são os piores de depurar porque cada lado está certo dentro da própria versão dos fatos.

O segundo é que vale manter a tela falando de intenção. Quando uma ação do usuário vira várias operações no modelo, juntar tudo num envelope único tira a ordem e a atomicidade de dentro do componente, que é o último lugar onde essas duas coisas deveriam morar.

E o terceiro: reativar, recomprar, repetir, qualquer funcionalidade que traga o passado de volta é na prática uma reconciliação com o catálogo atual. Tratar isso como parte do fluxo normal, e não como caminho de erro, foi o que mais diferença fez na percepção de cuidado do produto.

[ HEADLESS · ASSINATURAS ]
Sua área de conta trava a operação de recorrência?

Projetamos e construímos portais de assinatura em Hydrogen, Recharge e Shopify Subscriptions, com regra de negócio editável pelo time e escrita auditável.

Falar com a engenharia ↗
Leia também Ver todos os artigos →