Existem três formas de integrar um storefront com a Storefront API. Cada uma tem diferentes níveis de segurança e complexidade.
| Modo | Autenticação | Segurança | Complexidade |
|---|---|---|---|
| 1. Hospedagem na plataforma | Cookies httpOnly (automático) | Máxima | Nenhuma configuração extra |
| 2. Frontend + Backend próprio | Cookies httpOnly (via proxy reverso) | Máxima | Requer proxy no servidor |
| 3. Apenas frontend próprio | Bearer Token (JWT no JavaScript) | Moderada | Simples, sem servidor |
A loja roda na infraestrutura da plataforma com domínio customizado (ex: www.minhaloja.com). O storefront e a API ficam no mesmo servidor, tudo same-origin.
Tudo é same-origin. Cookies httpOnly, Secure, SameSite=Lax funcionam nativamente. Nenhuma configuração especial é necessária. O JavaScript do frontend nunca vê nenhum token de autenticação.
O lojista hospeda seu próprio frontend e backend, e usa um proxy reverso para encaminhar requests à Storefront API. O resultado é a mesma segurança do Modo 1.
O servidor do lojista encaminha as requests GraphQL para a API da plataforma. Do ponto de vista do navegador, tudo é same-origin:
www.minhaloja.com/api/graphql (mesmo domínio)api.acessocomercial.com/graphqlwww.minhaloja.commodule.exports = {
async rewrites() {
return [
{
source: '/api/graphql',
destination:
'https://storefront.acessocomercial.com/graphql'
}
];
}
}; export default {
async fetch(request) {
const url = new URL(request.url);
if (url.pathname.startsWith('/api/graphql')) {
const target =
'https://storefront.acessocomercial.com/graphql';
return fetch(target, {
method: request.method,
headers: request.headers,
body: request.body
});
}
}
}; location /api/graphql {
proxy_pass https://storefront.acessocomercial.com/graphql;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_pass_header Set-Cookie;
} import { createProxyMiddleware } from 'http-proxy-middleware';
app.use('/api/graphql', createProxyMiddleware({
target: 'https://storefront.acessocomercial.com',
changeOrigin: true,
pathRewrite: {
'^/api/graphql': '/graphql'
},
cookieDomainRewrite: {
'api.acessocomercial.com': ''
}
})); O código do frontend é idêntico ao Modo 1. Basta chamar o endpoint local com credentials: 'include':
const response = await fetch('/api/graphql', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `mutation {
clientLogin(input: {
email: "user@example.com"
password: "secret"
hostname: "minha-loja"
}) {
auth
}
}`
})
}); X-Auth-Mode, Authorization ou gerenciamento manual de tokens. O frontend se comporta exatamente como se estivesse hospedado na plataforma.
O lojista hospeda apenas o frontend, sem servidor próprio. O frontend chama a API diretamente de um domínio diferente (ex: lovable.app, vercel.app). Cookies não funcionam neste cenário, então a API retorna tokens no corpo da resposta.
O modo Bearer é ativado enviando o header X-Auth-Mode: bearer nas requests GraphQL. Quando ativo:
jwtToken e refreshToken no corpo da respostasessionStorage (ou em memória)Authorization: Bearer <token>Crie um client reutilizável que gerencia tokens, refresh automático e carrinho:
const API_URL = 'https://storefront.acessocomercial.com/graphql';
const HOSTNAME = 'minha-loja';
// --- Token Storage ---
function getTokens() {
return {
jwt: sessionStorage.getItem('jwt'),
refresh: sessionStorage.getItem('refreshToken')
};
}
function saveTokens(jwtToken, refreshToken) {
sessionStorage.setItem('jwt', jwtToken);
sessionStorage.setItem('refreshToken', refreshToken);
}
function clearTokens() {
sessionStorage.removeItem('jwt');
sessionStorage.removeItem('refreshToken');
}
function isLoggedIn() {
return !!sessionStorage.getItem('jwt');
}
// --- Cart Storage ---
function getCartData() {
return localStorage.getItem('CartData') || '';
}
function saveCartData(xCookiesHeader) {
if (!xCookiesHeader) return;
try {
const cookies = JSON.parse(xCookiesHeader);
for (const cookie of cookies) {
if (cookie.name === 'CartData') {
localStorage.setItem('CartData', JSON.stringify(cookie.value));
}
}
} catch { /* ignore parse errors */ }
}
// --- GraphQL Client ---
async function graphql(query, variables = {}) {
const { jwt } = getTokens();
const cartData = getCartData();
const headers = {
'Content-Type': 'application/json',
'X-Auth-Mode': 'bearer'
};
if (jwt) {
headers['Authorization'] = `Bearer ${jwt}`;
}
if (cartData) {
headers['X-Cart'] = JSON.stringify({
CartData: JSON.parse(cartData)
});
}
const response = await fetch(API_URL, {
method: 'POST',
headers,
body: JSON.stringify({ query, variables })
});
// Save updated cart data from response
saveCartData(response.headers.get('X-Cart'));
const result = await response.json();
// If token expired, try refresh and retry
const isAuthError = result.errors?.some(
(e) => e.extensions?.code === 'UNAUTHENTICATED'
);
if (isAuthError && getTokens().refresh) {
const refreshed = await refreshSession();
if (refreshed) {
return graphql(query, variables); // retry with new token
}
}
return result;
}
// --- Auth ---
async function login(email, password) {
const result = await graphql(`
mutation Login($input: ClientLoginMutationInput!) {
clientLogin(input: $input) {
auth
jwtToken
refreshToken
}
}
`, {
input: { email, password, hostname: HOSTNAME }
});
const { auth, jwtToken, refreshToken } = result.data.clientLogin;
if (auth) {
saveTokens(jwtToken, refreshToken);
}
return auth;
}
async function refreshSession() {
const { refresh } = getTokens();
if (!refresh) return false;
const response = await fetch(API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Auth-Mode': 'bearer'
},
body: JSON.stringify({
query: `mutation {
refreshSession(input: {
refreshToken: "${refresh}"
}) {
success
jwtToken
refreshToken
}
}`
})
});
const result = await response.json();
const data = result.data?.refreshSession;
if (data?.success) {
saveTokens(data.jwtToken, data.refreshToken);
return true;
}
clearTokens();
return false;
}
function logout() {
clearTokens();
}
// --- Usage ---
// Login
await login('user@example.com', 'secret');
// Authenticated query (token injected automatically)
const cart = await graphql(`
query { shoppingCart(hostname: "${HOSTNAME}") {
id totalPrice products { id title quantity }
}}
`);
// Add to cart
const updated = await graphql(`
mutation { addShoppingCartItem(input: {
hostname: "${HOSTNAME}" productId: "abc123" quantity: 1
}) { shoppingCart { id totalPrice } }}
`); O carrinho funciona via header X-Cart, que transporta o identificador do carrinho. Este mecanismo funciona cross-domain pois usa um header HTTP customizado (não depende de cookies do navegador).
X-Cart com o valor do CartData em cada requestX-Cart da resposta para obter o CartData atualizadolocalStorage) entre requestsPara domínios externos, o login com Google usa um fluxo de proxy centralizado, pois o Google requer que cada domínio seja registrado como JavaScript Origin autorizado.
/auth/google/start?hostname=STORE&redirect=CALLBACK_URLCALLBACK_URL?google_auth_code=CODEPOST /auth/google/exchange com o código para obter os tokenssessionStorage (limpo ao fechar a aba) — nunca em localStorageA Storefront API aceita requests de qualquer origem HTTPS. Não é necessário configurar CORS adicionalmente. Em desenvolvimento local, origens http://localhost também são permitidas.
// Frontend chama o proxy local (same-origin)
const response = await fetch('/api/graphql', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: `mutation {
clientLogin(input: {
email: "user@example.com"
password: "secret"
hostname: "minha-loja"
}) {
auth
}
}`
})
});
// Cookies definidos automaticamente pelo servidor.
// Nenhum token retornado ao JavaScript. const response = await fetch(
'https://storefront.acessocomercial.com/graphql',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Auth-Mode': 'bearer'
},
body: JSON.stringify({
query: `mutation {
clientLogin(input: {
email: "user@example.com"
password: "secret"
hostname: "minha-loja"
}) {
auth
jwtToken
refreshToken
}
}`
})
}
);
const { data } = await response.json();
const { jwtToken, refreshToken } = data.clientLogin;
sessionStorage.setItem('jwt', jwtToken);
sessionStorage.setItem('refresh', refreshToken); const jwt = sessionStorage.getItem('jwt');
const response = await fetch(
'https://storefront.acessocomercial.com/graphql',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${jwt}`,
'X-Auth-Mode': 'bearer'
},
body: JSON.stringify({
query: `query {
shoppingCart(hostname: "minha-loja") {
id
totalPrice
products { id title price quantity }
}
}`
})
}
); const refreshToken = sessionStorage.getItem('refresh');
const response = await fetch(
'https://storefront.acessocomercial.com/graphql',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Auth-Mode': 'bearer'
},
body: JSON.stringify({
query: `mutation {
refreshSession(input: {
refreshToken: "${refreshToken}"
}) {
success
jwtToken
refreshToken
}
}`
})
}
);
const { data } = await response.json();
if (data.refreshSession.success) {
sessionStorage.setItem('jwt', data.refreshSession.jwtToken);
sessionStorage.setItem('refresh', data.refreshSession.refreshToken);
} const cartData = localStorage.getItem('CartData') || '';
const response = await fetch(
'https://storefront.acessocomercial.com/graphql',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${jwt}`,
'X-Cart': JSON.stringify({
CartData: cartData ? JSON.parse(cartData) : ''
})
},
body: JSON.stringify({
query: `mutation {
addShoppingCartItem(input: {
hostname: "minha-loja"
productId: "abc123"
quantity: 1
}) {
shoppingCart {
id
totalPrice
products { id title quantity }
}
}
}`
})
}
);
const xCookies = response.headers.get('X-Cart');
if (xCookies) {
const cookies = JSON.parse(xCookies);
for (const cookie of cookies) {
if (cookie.name === 'CartData') {
localStorage.setItem('CartData',
JSON.stringify(cookie.value));
}
}
} // 1. Redirect para login Google
const hostname = 'minha-loja';
const callback = encodeURIComponent(
window.location.origin + '/auth/callback'
);
window.location.href =
'https://storefront.acessocomercial.com' +
'/auth/google/start' +
`?hostname=${hostname}&redirect=${callback}`;
// 2. Na pagina de callback, trocar o code
const params = new URLSearchParams(
window.location.search
);
const code = params.get('google_auth_code');
if (code) {
const response = await fetch(
'https://storefront.acessocomercial.com' +
'/auth/google/exchange',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Auth-Mode': 'bearer'
},
body: JSON.stringify({ code })
}
);
const { auth, jwtToken } = await response.json();
if (auth) {
sessionStorage.setItem('jwt', jwtToken);
}
} curl -X POST \
https://storefront.acessocomercial.com/graphql \
-H 'Content-Type: application/json' \
-H 'X-Auth-Mode: bearer' \
-d '{
"query": "mutation { clientLogin(input: { email: \"user@example.com\" password: \"secret\" hostname: \"minha-loja\" }) { auth jwtToken refreshToken } }"
}'