Saltar a contenido

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

  1. El navegador entra a una URL con countryCode (/py/...). Si falta, el middleware la resuelve contra el mapa de regiones de Medusa y redirige.
  2. /[countryCode]/(checkout)/checkout es un Server Component: llama a retrieveCart() y retrieveCustomer(). Sin carrito devuelve notFound().
  3. El CheckoutForm avanza por pasos —dirección, envío, pago, revisión—, cada uno resuelto por una Server Action de src/lib/data/. La dirección de envío usa el selector de departamento y ciudad alimentado por GET /store/geozones (ver integraciones).
  4. Elegido el método de envío, initiatePaymentSession() crea la sesión de pago en Medusa para el proveedor seleccionado. Si es Stripe, PaymentWrapper monta los Elements con el client_secret.
  5. Al confirmar, placeOrder() (src/lib/data/cart.ts) llama a sdk.store.cart.complete(id). Si Medusa responde type === "order", se revalidan los tags carts y orders, se borra la cookie del carrito y se redirige a /{countryCode}/order/{id}/confirmed. Si responde type === "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 .env del entorno.