Integración con Medusa

Cannahub utiliza Medusa v2 como backend de e-commerce. Esta guía documenta la arquitectura de integración, endpoints disponibles, y las convenciones utilizadas para extender Medusa con funcionalidad específica del dominio.


Qué es Medusa

Medusa es una plataforma de e-commerce headless y open-source que proporciona:

  • Name
    API RESTful
    Type
    core
    Description

    Endpoints completos para productos, órdenes, clientes y más.

  • Name
    Multi-tenancy
    Type
    feature
    Description

    Soporte para múltiples tiendas/organizaciones.

  • Name
    Extensibilidad
    Type
    feature
    Description

    Campos metadata flexibles para datos de dominio.

  • Name
    TypeScript
    Type
    tech
    Description

    SDK tipado y arquitectura moderna.

Stack de Medusa v2

// Backend
- Node.js + TypeScript
- PostgreSQL (database)
- Redis (cache/sessions)

// Arquitectura
- Admin API (/admin/*)
- Store API (/store/*)
- Auth API (/auth/*)

// Extensiones
- Custom metadata en todas las entidades
- Workflows para lógica compleja
- Event system para webhooks

Arquitectura de Integración

Loading diagram...

URLs Base

  • Name
    Producción
    Type
    https://api.cannahub.tech
    Description

    API de producción para aplicaciones en vivo.

  • Name
    Staging
    Type
    https://staging-api.cannahub.tech
    Description

    Entorno de pruebas pre-producción.

  • Name
    Desarrollo
    Type
    http://localhost:9000
    Description

    Servidor local de Medusa.

Configuración de cliente

// lib/medusa/client.ts
import Medusa from '@medusajs/medusa-js'

const MEDUSA_BACKEND_URL =
  process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL
  ?? 'http://localhost:9000'

export const medusaClient = new Medusa({
  baseUrl: MEDUSA_BACKEND_URL,
  maxRetries: 3,
})

Estructura de la API

Admin API (/admin/*)

Endpoints para administradores y staff. Requieren autenticación con cannahubAdminToken.

EndpointDescripción
/admin/productsCRUD de productos
/admin/customersGestión de clientes/miembros
/admin/ordersGestión de pedidos
/admin/customer-groupsMembresías (customer groups)
/admin/inventory-itemsControl de inventario
/admin/stock-locationsUbicaciones de stock
/admin/price-listsListas de precios por membresía
/admin/fulfillmentsGestión de fulfillment
/admin/paymentsCaptura de pagos

Ejemplo: Listar productos

curl https://api.cannahub.tech/admin/products \
  -H "Authorization: Bearer {admin_token}" \
  -H "Content-Type: application/json"

Respuesta

{
  "products": [
    {
      "id": "prod_01HQ8BD5G",
      "title": "Blue Dream Indoor",
      "status": "published",
      "variants": [...],
      "metadata": {
        "strainId": "strain_blue_dream",
        "isFlower": true
      }
    }
  ],
  "count": 45,
  "offset": 0,
  "limit": 20
}

Store API (/store/*)

Endpoints públicos para clientes. Algunos requieren autenticación con cannahubToken.

EndpointAuthDescripción
/store/productsNoCatálogo público
/store/products/:idNoDetalle de producto
/store/cartsNoCrear carrito
/store/carts/:idOperaciones en carrito
/store/carts/:id/completeCompletar orden
/store/customers/mePerfil del cliente
/store/customers/me/ordersHistorial de órdenes

Ejemplo: Crear carrito

curl -X POST https://api.cannahub.tech/store/carts \
  -H "Content-Type: application/json" \
  -H "x-publishable-api-key: {api_key}" \
  -d '{
    "region_id": "reg_argentina",
    "sales_channel_id": "sc_high_up"
  }'

Respuesta

{
  "cart": {
    "id": "cart_01HQ8ABC123",
    "region_id": "reg_argentina",
    "items": [],
    "total": 0
  }
}

Auth API (/auth/*)

Endpoints de autenticación para diferentes tipos de usuarios.

EndpointDescripción
/auth/customer/emailpassLogin de miembro
/auth/user/emailpassLogin de admin/staff
/auth/customer/emailpass/registerRegistro de miembro
/auth/sessionVerificar sesión activa

Login de miembro

curl -X POST https://api.cannahub.tech/auth/customer/emailpass \
  -H "Content-Type: application/json" \
  -d '{
    "email": "miembro@ejemplo.com",
    "password": "contraseña_segura"
  }'

Respuesta

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI..."
}

Autenticación

Tokens JWT

Cannahub utiliza JWT tokens almacenados en cookies HTTP-only para seguridad.

  • Name
    cannahubToken
    Type
    cookie
    Description

    Token de miembro. Expira en 7 días.

  • Name
    cannahubAdminToken
    Type
    cookie
    Description

    Token de admin/staff. Expira en 7 días.

Headers Requeridos

  • Name
    Authorization
    Type
    header
    Description

    Bearer {token} para endpoints autenticados.

  • Name
    x-publishable-api-key
    Type
    header
    Description

    API key pública para endpoints de store.

Configuración de headers

// api/medusa/config.ts
export function getMedusaHeaders(
  isAdmin: boolean = false
) {
  const token = isAdmin
    ? cookies().get('cannahubAdminToken')?.value
    : cookies().get('cannahubToken')?.value

  return {
    'Authorization': token
      ? `Bearer ${token}`
      : undefined,
    'Content-Type': 'application/json',
    'x-publishable-api-key':
      process.env.MEDUSA_PUBLISHABLE_KEY,
  }
}

Multi-Tenancy

Cannahub soporta múltiples organizaciones (clubs) con aislamiento completo de datos. La implementación combina identificación por subdominio en el frontend con un sistema de módulos custom de tenant en Medusa.

Identificación de Organización

  • Name
    Subdomain
    Type
    identificador
    Description

    high-up.cannahub.tech identifica al club en el frontend.

  • Name
    tenant-id
    Type
    header
    Description

    Header HTTP requerido en requests POST a Medusa. Formato: alfanumérico + guiones, 2-64 caracteres.

  • Name
    Sales Channel
    Type
    medusa
    Description

    Cada club tiene su propio sales channel.

  • Name
    Stock Location
    Type
    medusa
    Description

    Inventario separado por ubicación.

Organizaciones soportadas

const ORGANIZATIONS = {
  'high-up': {
    salesChannelId: 'sc_high_up',
    stockLocationId: 'sloc_high_up',
    publishableKey: 'pk_high_up_xxx',
  },
  'don-marcelino': {
    salesChannelId: 'sc_don_marcelino',
    stockLocationId: 'sloc_don_marcelino',
    publishableKey: 'pk_don_marcelino_xxx',
  },
  'circulo-rojo': {
    salesChannelId: 'sc_circulo_rojo',
    stockLocationId: 'sloc_circulo_rojo',
    publishableKey: 'pk_circulo_rojo_xxx',
  },
  // ... más organizaciones
}

// Extracción desde request
function getOrganization(request: Request) {
  const host = request.headers.get('host')
  const subdomain = host?.split('.')[0]
  return ORGANIZATIONS[subdomain]
}

Arquitectura Multi-Tenant Custom

La implementación real de multi-tenancy usa 8 módulos custom que crean tablas de enlace (link tables) entre entidades de Medusa y un tenant_id. Esto permite aislamiento de datos por organización sin modificar el core de Medusa.

Loading diagram...

Módulos Tenant

Cada módulo crea una tabla de enlace con dos campos: id (PK) y tenant_id (text).

MóduloTablaEntidad Medusa Enlazada
product-tenantproduct_tenantproduct
order-tenantorder_tenantorder
customer-tenantcustomer_tenantcustomer
customer-group-tenantcustomer_group_tenantcustomer_group
price-list-tenantprice_list_tenantprice_list
inventory-tenantinventory_tenantinventory_item
stock-location-tenantstock_location_tenantstock_location
user-tenantuser_tenantuser

Header tenant-id

El header tenant-id controla el aislamiento de datos:

  • GET requests: Opcional. Si se envía, filtra resultados al tenant. Si no se envía (ej: Medusa Admin), devuelve todos los recursos.
  • POST requests: Recomendado. El middleware inyecta tenant_id en additional_data automáticamente para que los workflow hooks creen los registros de enlace.
  • Validación: Regex /^[a-z0-9][a-z0-9-_]{1,63}$/, se normaliza a lowercase.

Rutas Protegidas

El tenantGuard middleware se aplica a:

  • /admin/products/**
  • /admin/orders/**
  • /admin/customers/**
  • /admin/customer-groups/**
  • /admin/price-lists/**
  • /admin/inventory-items/**
  • /admin/stock-locations/**
  • /admin/users/**
  • /store/carts/:id/complete

Request con tenant-id

# Listar productos de un tenant específico
curl https://api.cannahub.tech/admin/products \
  -H "Authorization: Bearer {admin_token}" \
  -H "tenant-id: high-up"

# Crear producto asociado a un tenant
curl -X POST https://api.cannahub.tech/admin/products \
  -H "Authorization: Bearer {admin_token}" \
  -H "tenant-id: high-up" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Blue Dream Indoor",
    "status": "published"
  }'

Validación del tenant-id

// medusa/src/utils/tenant.ts
const TENANT_ID_REGEX = /^[a-z0-9][a-z0-9-_]{1,63}$/
export const TENANT_ID_HEADER = 'tenant-id'

export const normalizeTenantId = (value: string) =>
  value.trim().toLowerCase()

export const isValidTenantId = (value: string) =>
  TENANT_ID_REGEX.test(value)

Workflow Hooks

Los hooks se activan cuando se crean entidades en Medusa y automáticamente crean el registro de enlace con el tenant:

HookWorkflowComportamiento
product-created-tenantcreateProductsWorkflowExtrae tenant_id de additional_data, crea ProductTenant y enlaza
order-created-tenantcreateOrderWorkflowUsa tenant_id de additional_data, o lo deriva del customer si no existe
customer-created-tenantcreateCustomersWorkflowCrea CustomerTenant por cada customer en batch
stock-location-created-tenantcreateStockLocationsWorkflowSafety net; tenant linking se maneja en route override

Convenciones de Metadata

Medusa permite almacenar datos arbitrarios en el campo metadata de todas las entidades. Cannahub usa convenciones consistentes.

Customer Metadata

Estructura

interface CustomerMetadata {
  // Estado del miembro
  status: MemberStatus
  role: Role

  // Onboarding
  onboardingStep: OnboardingStep
  hasReprocann: boolean

  // Información médica
  medicalRecord: {
    reprocannCode?: string
    reprocannExpiration?: string
    allergies: string[]
    conditions: string[]
    medications: string[]
    emergencyContact: {
      name: string
      phone: string
      relationship: string
    }
  }

  // Membresía activa
  membership: {
    id: string
    name: string
    status: 'PAID' | 'PENDING' | 'EXPIRED'
    expirationDate: string
  }

  // Datos personales extendidos
  bornDate?: string
  document?: string
  avatar?: string
}

Product Metadata

Estructura

interface ProductMetadata {
  // Identificación de tipo
  isFlower: boolean

  // Referencia a cepa (productos de flor)
  strainId?: string
  strainName?: string
  strainType?: 'sativa' | 'indica' | 'hybrid'

  // O cepa completa (legacy)
  strain?: {
    id: string
    name: string
    type: string
    cannabinoids: {
      thc: number
      cbd: number
    }
    terpenes: string[]
    effects: {
      helpsWith: string[]
      feelings: string[]
    }
  }

  // Información adicional
  cultivator?: string
  harvestDate?: string
  batchNumber?: string
}

Variant Metadata

Estructura

interface VariantMetadata {
  // Peso/cantidad por unidad
  weight: number      // Gramos (para flores)
  quantity: number    // Unidades (para otros)
  unit: 'grams' | 'ml' | 'units'

  // Información de lote
  batchId?: string
  expirationDate?: string
}

Customer Group (Membership) Metadata

Estructura

interface CustomerGroupMetadata {
  // Información de membresía
  description: string
  price: number
  currency: string

  // Beneficios y límites
  benefits: string[]
  limits?: {
    monthlyPurchase?: number
    productAccess?: string[]
  }

  // Configuración
  priceListId: string
  isActive: boolean
  sortOrder: number
  color?: string
}

Capa BFF (Backend for Frontend)

El BFF simplifica y optimiza las llamadas al backend.

Propósito

  • Name
    Agregación
    Type
    optimization
    Description

    Combina múltiples llamadas a Medusa en una sola respuesta.

  • Name
    Transformación
    Type
    core
    Description

    Convierte tipos de Medusa a tipos de Cannahub.

  • Name
    Caché
    Type
    optimization
    Description

    Cachea respuestas frecuentes.

  • Name
    Lógica de Negocio
    Type
    core
    Description

    Implementa reglas específicas del dominio.

Ejemplo de ruta BFF

// app/api/products/route.ts
import { getMedusaClient } from '@/lib/medusa'
import { parseMedusaProduct } from '@/lib/medusa/products'
import { NextResponse } from 'next/server'

export async function GET(request: Request) {
  const medusa = getMedusaClient()

  // Llamada a Medusa
  const { products, count } =
    await medusa.admin.products.list({
      status: 'published',
      expand: 'variants,variants.prices',
    })

  // Transformación
  const transformed = products.map(parseMedusaProduct)

  // Respuesta simplificada
  return NextResponse.json({
    products: transformed,
    count,
  })
}

Rutas BFF Disponibles

RutaMétodoDescripciónLlamadas Reducidas
/api/productsGETProductos con cepas2 → 1
/api/products/:id/with-inventoryGETProducto + inventario multi-ubicaciónN+1 → 1
/api/strainsGETCatálogo de cepas filtrable-
/api/membershipsGETMembresías disponibles1 → 1
/api/dashboard/statsGETEstadísticas agregadas4 → 1
/api/orders/create-and-processPOSTFlujo completo de orden15+ → 1

Flujo de Creación de Orden

El flujo completo de orden requiere múltiples llamadas a Medusa que el BFF orquesta.

Loading diagram...

Manejo de Errores

Códigos de Error de Medusa

CódigoSignificado
400Solicitud inválida
401No autenticado
403Sin permisos
404Recurso no encontrado
409Conflicto (ej: email duplicado)
422Entidad no procesable
500Error interno

Manejo de errores

// lib/medusa/error-handler.ts
export function handleMedusaError(
  error: any
): ApiError {
  if (error.response) {
    const { status, data } = error.response

    return {
      code: status,
      message: data.message
        ?? 'Error de servidor',
      details: data.errors,
    }
  }

  return {
    code: 500,
    message: 'Error de conexión con Medusa',
    details: error.message,
  }
}

Rate Limiting

Medusa implementa límites de tasa que Cannahub respeta y propaga.

  • Name
    Store endpoints
    Type
    1000 req/min
    Description

    Endpoints públicos de tienda.

  • Name
    Admin endpoints
    Type
    500 req/min
    Description

    Endpoints administrativos.

  • Name
    Auth endpoints
    Type
    10 req/min
    Description

    Login para prevenir fuerza bruta.


Próximos Pasos

Was this page helpful?