Back to blog

CMS Migration: From Nuxeo to Strapi

We led a CMS migration from Nuxeo to Strapi that cut down 90% processing time, simplified the architecture, and gave our client full data ownership. This blog by our software engineer who drove the initiative breaks down the challenges, technical decisions, and positive results from making the switch.

Bao Tran avatar
Bao Tran
8 min read

Companies are relying more on Content Management Systems (CMS) to manage growing content needs. But when a CMS becomes more of a burden than a benefit, it’s time for a change.

Our global video game publisher client had built their CMS on Nuxeo, but the platform proved costly, complex, and underutilized, all while raising concerns about vendor lock-in and data ownership. They needed a lighter, more flexible, and scalable solution.

We turned to Strapi, a headless, flexible CMS that met those goals.

Motivation to Move Away from Nuxeo

While Nuxeo provides extensive capabilities, our client faced challenges, including:

  • Vendor Lock-In: Proprietary data structures limited portability, while rigid workflows and constrained schema customization restricted flexibility. This created long-term dependency on Nuxeo and slowed the team’s ability to adapt to evolving business needs.
  • High Costs vs. Low Usage: The team already had a dedicated ReactJS frontend, so Nuxeo’s Admin UI and workflow automation were largely irrelevant. Schema design and deployment also depended on Nuxeo Studio, which proved unintuitive and inefficient. In practice, Nuxeo was used primarily as a content repository rather than as a full platform, yet carried the costs of one.
  • Permission Complexities: Nuxeo’s permission system was so granular that it became confusing for normal administrators and user groups. We constantly ran into misunderstandings and misconfigurations when managing access.
  • Scalability Issues: We could only run a single Nuxeo instance reliably. Attempts to set up clustering for scale led nowhere, even with Nuxeo support. Not to mention the support process itself was time-consuming and often unhelpful.

These issues became more evident during development, when we had to find replacements for core Nuxeo functionalities. The options we explored were Directus, Contentful, and Strapi.

The first two options were quickly dismissed because:

  • Directus: Required extensive custom work, and the client aimed to complete the transition (including application go-live and data migration) within a few months.
  • Contentful: Presented similar challenges to Nuxeo, including a steep learning curve for server-side extensions/customization, high costs, and limited control over data schema and access control customization.

Ultimately, Strapi was the best fit, as an open-source, headless CMS, offering a higher level of abstraction with many suitable prebuilt features. It puts our client back in the driver's seat with full data ownership and an API-first approach for maximum flexibility.

Overview of the New Architecture

Overview of the New Architecture

Strapi CMS Server

The Strapi server served as our content repository, configured with AWS S3 for scalable storage:

  • Admin Portal: We leveraged Strapi's built-in user interface for Administrators to manage content models, roles, and permissions without coding.
  • Content Model Definition: We defined clear, efficient, and adaptable content models within Strapi for flexible usage.
  • AWS S3 Integration: We implemented AWS S3 for asset storage, ensuring scalability and performance.
  • API Layer: We used Strapi's REST APIs that serve as content backbone for the Middleware Layer (details below).

Middleware Layer

For middleware, we implemented ECS-containerized Node.js services and AWS Lambda functions:

  • Node.js Services: This served as an API gateway between React frontend and Strapi backend to manage complex business logic and handle image/video processing.
  • Event-Driven AWS Lambda Functions: This managed asynchronous digital asset processing tasks.

Front-End Layer

ReactJS apps provided intuitive interfaces and seamless communication with middleware APIs.

Detailed Image/Video Processing Implementation

In the middleware layer, we implemented a robust system for processing images, documents, and videos. As a matter of fact, one of the first challenges we faced when moving from Nuxeo to Strapi was supporting the upload of multiple assets and organizing them in a way the new system could handle. Nuxeo offered this natively, but with Strapi we had to re-design how assets were structured.

To achieve flexibility, we applied the Strategy pattern, which generates ImageMagick commands based on runtime parameters. This pattern isolates media processing logic from the code that uses it, making it easy to add new commands or renditions in the future without disrupting existing workflows.

Middleware Strategy Pattern Implementation

We leveraged the Strategy pattern within our middleware to streamline and manage different processing types efficiently.

Example:

const { exec } = require('child_process')
const { promisify } = require('util')

const execAsync = promisify(exec)

const makeThumbnail = (inputPath) => `convert ${inputPath} -strip -thumbnail 150x150^ -gravity center -extent 150x150 -quality 85 thumbnail.jpg`

const makeResize = size => (inputPath, outputPath) =>
  `convert ${inputPath} -resize ${size} -strip ${outputPath}.jpg`

// Define the commands as pure functions
const commandStrategies = {
  thumbnail: makeThumbnail(),
  small:     makeResize('640x480'),
  large:     makeResize('1920x1080'),
}

// processImage now returns a Promise; no classes, no mutation
const processImage = (commandName, inputPath, outputPath) => {
  const command = commandStrategies[commandName]
  if (!command) {
    return Promise.reject(new Error(`Unknown command: ${commandName}`))
  }
  const cmd = command(inputPath, outputPath)
  return execAsync(cmd)
}

// Usage example:
processImage('thumbnail', 'in.jpg', 'out-rendition.jpg')
  .then(({ stdout, stderr }) => {
    console.log('Done!', stdout)
  })
  .catch(err => {
    console.error('Processing failed:', err)
  })

Rendition Configuration

Renditions were mapped in a structured configuration, easing the management and invocation.

[
  {
    "id": "thumbnail",
    "label": "Thumbnail (150x150)",
    "command": "thumbnail"
  },
  {
    "id": "small",
    "label": "Small (640x480)",
    "command": "small"
  },
  {
    "id": "large",
    "label": "Large (1920x1080)",
    "command": "large"
  }
]

This can be further enhanced by including conditions for each rendition. For example, only creating a “large” rendition when the input image’s dimensions are bigger than the “large” rendition output dimensions.

Event-Driven Processing Workflow

Our middleware triggered processing events automatically after asset uploads to AWS S3, effectively utilizing AWS Lambda:

exports.handler = async (event) => {
  const s3 = new AWS.S3();
  const record = event.Records[0];
  const bucket = record.s3.bucket.name;
  const key = decodeURIComponent(record.s3.object.key.replace(/\\+/g, ' '));

  const inputPath = `/tmp/${key}`;
  await downloadFromS3(bucket, key, inputPath);

  const renditionTypes = ['thumbnail', 'small', 'large'];
  for (const type of renditionTypes) {
    const outputKey = `renditions/${type}/${key}`;
    const outputPath = `/tmp/${type}-${key}`;
    processImage(type, inputPath, outputPath);
    await uploadToS3(bucket, outputKey, outputPath);
  }
};

Performance Optimization

Moving thumbnail generation from Nuxeo into our custom middleware gives us significant improvements in both speed and efficiency:

  • 90% Reduction in Processing Time: Thumbnails that previously took several seconds to generate were now produced almost instantly, freeing up resources and improving responsiveness.
  • 95% Reduction in Thumbnail Size: Optimized image processing created much smaller files without compromising quality, cutting storage costs, and speeding up content delivery.

Elasticsearch Data Migration Implementation

Like any transition, data migration was one of the most important parts. We used Elasticsearch as our extraction point, pulled the records in bulk, simplified the relationships, and then imported them into Strapi. This approach kept the process smooth, reduced complexity, and made sure nothing was lost along the way.

Step 1: Extracting Data from Nuxeo using Elasticsearch

GET nuxeo-assets/_search
{
  "query": { "match_all": {} },
  "size": 1000
}

Step 2: Data Transformation & Relationship Simplification

function transformRecord(nuxeoRecord) {
  return {
    title: nuxeoRecord.title,
    description: nuxeoRecord.description,
    metadata: nuxeoRecord.metadata,
    fileUrl: nuxeoRecord.file.url
  };
}

Step 3: Automated Import to Strapi

async function migrateData(records) {
  for (const record of records) {
    await axios.post(`${STRAPI_ENDPOINT}/assets`, transformRecord(record));
  }
}

Key Technical Challenges & Solutions

  • Asynchronous Asset Processing: We needed to process images, documents, and videos at scale. This was efficiently handled with AWS Lambda, ImageMagick, Sharp, and FFmpeg.
  • Data Migration: Moving large volumes of content from Nuxeo to Strapi required a reliable process. We leveraged Elasticsearch for seamless data extraction and import with minimal disruption.
  • Partial Update Handling: Some content required partial rather than full updates. This was managed through custom Node.js services, ensuring data consistency without unnecessary cost.
  • Security and Permissions: Last but not least, protecting assets and content access was a huge concern. We implemented a robust security model using JWT-based authentication to secure APIs and enforce permission boundaries.

Future Roadmaps

  • Enhanced Analytics and Reporting: Boosting capabilities to provide deeper insights into content usage and performance.
  • Automation of Content Lifecycle Management: Streamlining processes for asset creation, updates, and archival.
  • Broader AWS Integrations: Extending the architecture with additional AWS services to improve scalability and resilience.

Conclusion

Our Nuxeo to Strapi migration made a big difference for the client. It simplified their tech stack, reduced 90% processing time and 95% thumbnail size, and gave them a CMS that’s both flexible and scalable without the headaches of vendor lock-in. Most importantly, they now have full control of their data and the freedom to grow their digital asset management on their own terms.

All in all, this project really showed that the right solution, coupled with team effort, can unlock both efficiency today and flexibility for tomorrow.

FULL-STACK ENGINEER

Bao Tran

FULL-STACK ENGINEER

Bao is a Full-stack Developer with seven years of experience delivering end-to-end technology solutions across Backend and Frontend. Proficient in .NET, JavaScript, and multiple tech stacks, he has built scalable, efficient applications while ensuring code quality and performance for both external projects and embedded teams.

Related articles

Explore our blog
avatar-blog

Lovable vs Builder.io vs Figma Make: What’s the Vibe Code Tool for You?

KA Nguyen avatar

by KA Nguyen

avatar-blog

The Cost of Choosing the Wrong Tech Stack – A Lesson from a Tech Lead

Harry Nguyen avatar

by Harry Nguyen

avatar-blog

Good Names, Clean Code: A Developer’s Guide to Clarity

Linh Truong avatar

by Linh Truong

Let's discuss your project needs.

We can help you get the details right.

Book a discovery call
background

CodeLink Newsletter

Subscribe to receive the latest news on technology and product development from CodeLink.

CodeLink

CodeLink powers growing startups and pioneering corporations to scale faster, leverage artificial intelligence, and release high-impact technology products.

Contact Us

(+84) 2839 333 143Write us at hello@codelink.io
Contact Us
2025 © CodeLink Limited.
All right reserved.
Privacy Policy