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
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.
| Endpoint | Descripción |
|---|---|
/admin/products | CRUD de productos |
/admin/customers | Gestión de clientes/miembros |
/admin/orders | Gestión de pedidos |
/admin/customer-groups | Membresías (customer groups) |
/admin/inventory-items | Control de inventario |
/admin/stock-locations | Ubicaciones de stock |
/admin/price-lists | Listas de precios por membresía |
/admin/fulfillments | Gestión de fulfillment |
/admin/payments | Captura 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.
| Endpoint | Auth | Descripción |
|---|---|---|
/store/products | No | Catálogo público |
/store/products/:id | No | Detalle de producto |
/store/carts | No | Crear carrito |
/store/carts/:id | Sí | Operaciones en carrito |
/store/carts/:id/complete | Sí | Completar orden |
/store/customers/me | Sí | Perfil del cliente |
/store/customers/me/orders | Sí | Historial 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.
| Endpoint | Descripción |
|---|---|
/auth/customer/emailpass | Login de miembro |
/auth/user/emailpass | Login de admin/staff |
/auth/customer/emailpass/register | Registro de miembro |
/auth/session | Verificar 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.techidentifica 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.
Módulos Tenant
Cada módulo crea una tabla de enlace con dos campos: id (PK) y tenant_id (text).
| Módulo | Tabla | Entidad Medusa Enlazada |
|---|---|---|
product-tenant | product_tenant | product |
order-tenant | order_tenant | order |
customer-tenant | customer_tenant | customer |
customer-group-tenant | customer_group_tenant | customer_group |
price-list-tenant | price_list_tenant | price_list |
inventory-tenant | inventory_tenant | inventory_item |
stock-location-tenant | stock_location_tenant | stock_location |
user-tenant | user_tenant | user |
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_idenadditional_dataautomá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:
| Hook | Workflow | Comportamiento |
|---|---|---|
product-created-tenant | createProductsWorkflow | Extrae tenant_id de additional_data, crea ProductTenant y enlaza |
order-created-tenant | createOrderWorkflow | Usa tenant_id de additional_data, o lo deriva del customer si no existe |
customer-created-tenant | createCustomersWorkflow | Crea CustomerTenant por cada customer en batch |
stock-location-created-tenant | createStockLocationsWorkflow | Safety net; tenant linking se maneja en route override |
El hook de órdenes tiene lógica de fallback: si no llega tenant_id en additional_data, busca el tenant vinculado al customer. Si el customer tiene múltiples tenants, usa el primero lexicográficamente (determinístico).
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
| Ruta | Método | Descripción | Llamadas Reducidas |
|---|---|---|---|
/api/products | GET | Productos con cepas | 2 → 1 |
/api/products/:id/with-inventory | GET | Producto + inventario multi-ubicación | N+1 → 1 |
/api/strains | GET | Catálogo de cepas filtrable | - |
/api/memberships | GET | Membresías disponibles | 1 → 1 |
/api/dashboard/stats | GET | Estadísticas agregadas | 4 → 1 |
/api/orders/create-and-process | POST | Flujo completo de orden | 15+ → 1 |
Flujo de Creación de Orden
El flujo completo de orden requiere múltiples llamadas a Medusa que el BFF orquesta.
Manejo de Errores
Códigos de Error de Medusa
| Código | Significado |
|---|---|
400 | Solicitud inválida |
401 | No autenticado |
403 | Sin permisos |
404 | Recurso no encontrado |
409 | Conflicto (ej: email duplicado) |
422 | Entidad no procesable |
500 | Error 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.