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.

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.
