Radixia

AI · serverless

Using Strapi as our Headless CMS layer

In the third part of this multi-part series, we present Radixia Maca, our open-source solution for AI-composable commerce.

Using Strapi as our Headless CMS layer

As digital commerce evolves, content becomes more than just a supporting layer—it becomes a strategic asset. In Part 3 of our series on Radixia Maca, our open-source AI-composable commerce stack, we shift our focus to building a robust headless content layer using Strapi CMS. This chapter is about structure, governance, and seamless integration. Strapi empowers us to manage rich product data, editorial content, and media assets in a centralized, API-first manner, making it easy to decouple content from frontend delivery. Whether we’re creating SKUs for ServerlessDay T-shirts or syncing catalog updates to external systems, Strapi serves as the authoritative source of content truth. Combined with AWS Fargate for serverless deployment and a custom plugin for real-time integration with Commerce Layer, this setup forms the backbone of content operations in Radixia Maca.

Strapi Service Structure

Our Strapi service is configured to run on AWS Fargate, a serverless container service that allows us to run containers without managing servers. We've already defined the necessary infrastructure in StrapiServiceConstruct our CDK project. Now let's look at how to configure Strapi for our ServerlessDay merchandising use case.

Dockerfile and Docker Compose

Let's start with the Dockerfile that defines the Docker image for Strapi:dockerfile

# Dockerfile for Strapi CMS
FROM node:18-alpine

# Install necessary dependencies
RUN apk add --no-cache build-base gcc autoconf automake zlib-dev libpng-dev vips-dev > /dev/null 2>&1

# Create application directory
WORKDIR /opt/app

# Copy package.json and package-lock.json
COPY ./package.json ./
COPY ./package-lock.json ./

# Install dependencies
RUN npm install

# Copy the rest of the application
COPY . .

# Build the application
RUN npm run build

# Expose port 1337
EXPOSE 1337

# Start command
CMD ["npm", "run", "start"]

For local development, we use Docker Compose to configure Strapi along with a PostgreSQL database:yaml

# docker-compose.yml for Strapi CMS
version: '3'

services:
  strapi:
    build: .
    container_name: strapi-cms
    restart: unless-stopped
    env_file: .env
    environment:
      DATABASE_CLIENT: postgres
      DATABASE_HOST: postgres
      DATABASE_PORT: 5432
      DATABASE_NAME: strapi
      DATABASE_USERNAME: strapi
      DATABASE_PASSWORD: ${DATABASE_PASSWORD}
      NODE_ENV: production
      SYNC_LAMBDA_URL: ${SYNC_LAMBDA_URL}
    volumes:
      - ./app:/opt/app
      - ./config:/opt/app/config
      - ./src:/opt/app/src
      - ./package.json:/opt/package.json
      - ./package-lock.json:/opt/package-lock.json
    ports:
      - "1337:1337"
    networks:
      - strapi-network
    depends_on:
      - postgres

  postgres:
    image: postgres:14-alpine
    container_name: postgres
    restart: unless-stopped
    env_file: .env
    environment:
      POSTGRES_USER: strapi
      POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
      POSTGRES_DB: strapi
    volumes:
      - postgres-data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    networks:
      - strapi-network

networks:
  strapi-network:
    driver: bridge

volumes:
  postgres-data:

Environment Configuration

Let's create an .env.example file that will serve as a template for the actual .env file:

# Configuration file .env for Strapi CMS
# This file contains environment variables for the Strapi service

# Database
DATABASE_PASSWORD=strapi_password_example

# Lambda Integration
SYNC_LAMBDA_URL=https://example.execute-api.us-east-1.amazonaws.com/prod/sync

# Strapi Configuration
ADMIN_JWT_SECRET=your-admin-jwt-secret
JWT_SECRET=your-jwt-secret
API_TOKEN_SALT=your-api-token-salt

# AWS Configuration
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key

# Commerce Layer Configuration
COMMERCE_LAYER_CLIENT_ID=your-client-id
COMMERCE_LAYER_CLIENT_SECRET=your-client-secret
COMMERCE_LAYER_ENDPOINT=https://yourdomain.commercelayer.io

Content Models

Now, let's define the content models for our e-commerce platform. Let's start with the model for products:

// src/api/product/content-types/product/schema.json
module.exports = {
  kind: 'collectionType',
  collectionName: 'products',
  info: {
    singularName: 'product',
    pluralName: 'products',
    displayName: 'Product',
    description: 'ServerlessDay merchandising products',
  },
  options: {
    draftAndPublish: true,
  },
  attributes: {
    name: {
      type: 'string',
      required: true,
      unique: true,
    },
    description: {
      type: 'text',
      required: true,
    },
    sku: {
      type: 'string',
      required: true,
      unique: true,
    },
    price: {
      type: 'decimal',
      required: true,
      default: 0,
    },
    stock: {
      type: 'integer',
      default: 0,
    },
    colors: {
      type: 'string',
    },
    sizes: {
      type: 'string',
    },
    image: {
      type: 'media',
      allowedTypes: ['images'],
      multiple: false,
    },
    category: {
      type: 'relation',
      relation: 'manyToOne',
      target: 'api::category.category',
      inversedBy: 'products',
    },
    commerceLayerId: {
      type: 'string',
    },
    commerceLayerSyncedAt: {
      type: 'datetime',
    },
  },
};

Let's also define the model for categories:

// src/api/category/content-types/category/schema.json
module.exports = {
  kind: 'collectionType',
  collectionName: 'categories',
  info: {
    singularName: 'category',
    pluralName: 'categories',
    displayName: 'Category',
    description: 'Product categories for merchandising',
  },
  options: {
    draftAndPublish: true,
  },
  attributes: {
    name: {
      type: 'string',
      required: true,
      unique: true,
    },
    description: {
      type: 'text',
    },
    products: {
      type: 'relation',
      relation: 'oneToMany',
      target: 'api::product.product',
      mappedBy: 'category',
    },
    image: {
      type: 'media',
      allowedTypes: ['images'],
      multiple: false,
    },
    commerceLayerId: {
      type: 'string',
    },
  },
};

And finally, the model for orders:

// src/api/order/content-types/order/schema.json
module.exports = {
  kind: 'collectionType',
  collectionName: 'orders',
  info: {
    singularName: 'order',
    pluralName: 'orders',
    displayName: 'Order',
    description: 'Merchandising orders',
  },
  options: {
    draftAndPublish: false,
  },
  attributes: {
    commerceLayerId: {
      type: 'string',
      required: true,
      unique: true,
    },
    number: {
      type: 'string',
      required: true,
    },
    status: {
      type: 'enumeration',
      enum: [
        'pending',
        'approved',
        'fulfilled',
        'shipped',
        'delivered',
        'canceled'
      ],
      default: 'pending',
    },
    customerEmail: {
      type: 'email',
      required: true,
    },
    totalAmount: {
      type: 'decimal',
      required: true,
    },
    currency: {
      type: 'string',
      required: true,
    },
    items: {
      type: 'json',
    },
    shippingAddress: {
      type: 'json',
    },
    billingAddress: {
      type: 'json',
    },
    metadata: {
      type: 'json',
    },
  },
};

Plugin for Commerce Layer Integration

Let's create a custom plugin to integrate Strapi with Commerce Layer. This plugin will handle data synchronization between the two systems. First, let's configure the plugin in the config/plugins.js file:

// config/plugins.js
module.exports = {
  // Commerce Layer Plugin for Strapi
  'commerce-layer-integration': {
    enabled: true,
    resolve: './src/plugins/commerce-layer-integration',
    config: {
      syncEndpoint: process.env.SYNC_LAMBDA_URL,
      commerceLayerEndpoint: process.env.COMMERCE_LAYER_ENDPOINT,
      clientId: process.env.COMMERCE_LAYER_CLIENT_ID,
      clientSecret: process.env.COMMERCE_LAYER_CLIENT_SECRET,
      // Webhook configuration
      webhooks: {
        enabled: true,
        events: [
          'product.created',
          'product.updated',
          'product.deleted'
        ]
      },
      // Field mapping between Strapi and Commerce Layer
      fieldMapping: {
        products: {
          name: 'name',
          description: 'description',
          sku: 'sku_code',
          price: 'amount_cents',
          stock: 'quantity',
          category: 'metadata.category',
          image: 'image_url'
        }
      }
    }
  }
};

You're halfway in — the full article lives on our blog.

Continue reading on blog.radixia.ai →

Free to read. Subscribe there to get new posts by email.