Arquitectura¶
CLX-Storefront es una aplicación Next.js 15 con App Router. No tiene base de datos propia: es una capa de presentación y orquestación sobre tres servicios externos (Medusa, Payload y Algolia). Todo el estado de negocio —carrito, pedido, cliente— vive en Medusa; el storefront solo guarda cookies de sesión.
Diagrama¶
flowchart LR
U[Navegador] --> MW[middleware.ts<br/>resuelve region por countryCode]
MW --> APP[Next.js 15 · App Router<br/>Server Components + Server Actions]
subgraph Storefront
APP --> DATA["src/lib/data/*<br/>capa de datos 'use server'"]
APP --> RAPI["src/app/api/*<br/>route handlers"]
APP --> IS[react-instantsearch<br/>búsqueda client-side]
end
DATA -->|"@medusajs/js-sdk"| MED[(Medusa V2<br/>catálogo · carrito · pedidos)]
MW -->|"GET /store/regions"| MED
APP -->|"REST + JWT"| PAY[(Payload CMS<br/>home · navegación · formularios)]
RAPI --> PAY
IS --> ALG[(Algolia<br/>índice de productos)]
DATA --> ALG
APP --> STR[Stripe]
APP --> GO[Google OAuth]
APP --> RC[reCAPTCHA v3]
Componentes internos¶
| Componente | Ubicación | Rol |
|---|---|---|
| Middleware de región | src/middleware.ts |
Pide /store/regions a Medusa, arma un mapa país → región cacheado 1 h en memoria, y redirige a la URL con countryCode. Setea la cookie _medusa_cache_id. |
| Capa de datos | src/lib/data/ |
Server Actions ("use server" + server-only) que hablan con Medusa: cart, customer, products, collections, categories, orders, regions, payment, fulfillment, geozones, cookies, onboarding. |
| Cliente Medusa | src/lib/config.ts |
Única instancia del @medusajs/js-sdk, con publishableKey. |
| Cliente Payload | src/lib/payload.ts, payload-home.ts, lexical-render.tsx |
Login contra Payload con usuario/clave, cachea el JWT en memoria y renderiza el contenido Lexical de la home. |
| Clientes Algolia | src/lib/algolia.ts (server) y src/lib/search-client.ts (browser) |
Dos clientes distintos: el completo para operaciones server-side y algoliasearch/lite para InstantSearch. |
| Route handlers | src/app/api/credit-form/route.ts, src/api/header/route.ts |
Proxies hacia Payload (formulario de crédito y menú de navegación) para no exponer credenciales al navegador. |
| Módulos de UI | src/modules/ |
Un directorio por dominio (products, cart, checkout, account, search, shipping, credit-form, layout, common), cada uno con components/ y templates/. |
Rutas. Todo cuelga de src/app/[countryCode]/, separado en dos grupos:
(main) (con nav y footer) y (checkout) (layout reducido). La sección de
cuenta usa rutas paralelas @dashboard / @login.
Sesión y cache. El JWT del cliente va en la cookie _medusa_jwt, el carrito
en _medusa_cart_id y el identificador de cache en _medusa_cache_id, todas
HTTP-only. La invalidación usa cache tags de Next.js scopeados por usuario
(carts-<cacheId>, products-<cacheId>, ...), que se revalidan después de cada
mutación.
Dependencias externas¶
| Servicio | Para qué se usa | ¿Qué pasa si no está? |
|---|---|---|
Medusa V2 (MEDUSA_BACKEND_URL / NEXT_PUBLIC_MEDUSA_BASE_URL) |
Regiones, catálogo, carrito, checkout, clientes, pedidos | El sitio no funciona. El middleware lanza Error fetching regions y ninguna página resuelve: la resolución de región ocurre antes de renderizar. |
Payload CMS (PAYLOAD_PUBLIC_URL, PAYLOAD_USER_EMAIL, PAYLOAD_USER_PASSWORD) |
Contenido de la home, menú de navegación horizontal, formulario de crédito | La home muestra "No se encontró la homepage en Payload"; el menú de categorías queda vacío y /api/credit-form responde 500. El resto (catálogo, carrito, checkout) sigue funcionando. |
Algolia (NEXT_PUBLIC_ALGOLIA_APP_ID, ..._SEARCH_KEY, ..._PRODUCT_INDEX, ..._SUGGESTIONS_INDEX, ALGOLIA_ADMIN_API_KEY) |
Búsqueda, autocompletado y las grillas de producto de la home | La búsqueda deja de responder y las grillas de la home quedan vacías. La navegación por categorías y colecciones —que va contra Medusa— no se ve afectada. |
Stripe (NEXT_PUBLIC_STRIPE_KEY) |
Pago con tarjeta en el checkout | El checkout no puede cobrar con tarjeta. Los demás proveedores (transferencia bancaria, Bancard QR, pago manual) siguen disponibles si están habilitados en Medusa. |
Google OAuth (NEXT_PUBLIC_GOOGLE_CLIENT_ID) |
Login con Google (/auth/google/callback) |
Solo se puede entrar con email y contraseña. |
reCAPTCHA v3 (NEXT_PUBLIC_RECAPTCHA_SITE_KEY, RECAPTCHA_SECRET_KEY) |
Protección del formulario de crédito | El provider avisa por consola que falta la site key; el formulario queda sin protección antibot. |
Bancard QR y vPOS (NEXT_PUBLIC_BANCARD_VPOS_ORIGIN) |
Pago por QR y con tarjeta en el paso de revisión, resueltos por los proveedores pp_bancard_qr y pp_bancard_vpos de Medusa |
Sin esos proveedores habilitados en Medusa, las opciones no aparecen en el checkout; sin el origen, la CSP bloquea el iframe de tarjeta. |
Los proveedores de pago que el storefront sabe representar están declarados en
src/lib/constants.tsx:
Stripe (tarjeta, iDeal, Bancontact), PayPal, pago manual, transferencia bancaria
y Bancard QR. Quién está realmente habilitado lo decide el backend Medusa,
no este repo. El mismo archivo lista las monedas sin división por 100 — el
guaraní (pyg) entre ellas.
Flujo principal: del carrito al pedido¶
- El navegador entra a una URL con
countryCode(/py/...). Si falta, el middleware la resuelve contra el mapa de regiones de Medusa y redirige. /[countryCode]/(checkout)/checkoutes un Server Component: llama aretrieveCart()yretrieveCustomer(). Sin carrito devuelvenotFound().- El
CheckoutFormavanza por pasos —dirección, envío, pago, revisión—, cada uno resuelto por una Server Action desrc/lib/data/. La dirección de envío usa el selector de departamento y ciudad alimentado porGET /store/geozones(ver integraciones). - Elegido el método de envío,
initiatePaymentSession()crea la sesión de pago en Medusa para el proveedor seleccionado. Si es Stripe,PaymentWrappermonta los Elements con elclient_secret. - Al confirmar,
placeOrder()(src/lib/data/cart.ts) llama asdk.store.cart.complete(id). Si Medusa respondetype === "order", se revalidan los tagscartsyorders, se borra la cookie del carrito y se redirige a/{countryCode}/order/{id}/confirmed. Si respondetype === "cart", el carrito vuelve con el error y el usuario se queda en el checkout.
Pendiente de documentar¶
- Dónde se despliega efectivamente el contenedor (host, orquestador, dominio): no hay pipeline de deploy ni manifiesto de infraestructura en este repo.
- Qué instancia de Payload y qué índice de Algolia se usan en producción: los
valores viven solo en el
.envdel entorno.