Vipin Banka
Jul 20, 2026
visibility 121
star star star star star
(0 votes)

Parallel Development in Optimizely CMS SaaS: A Smarter Way to Push Content Models

When Optimizely shipped the JavaScript SDK and CLI for CMS SaaS, they gave developers something pretty cool — a code-first workflow for content modeling. Define your content types in .tsx files, run npx @optimizely/cms-cli@latest push, and your content models land in the CMS instance via the REST API.

Clean. Fast. Developer-friendly.

But here's the thing about developer tools — the moment you hand them to a team, the dynamics shift. And that's not a flaw in the tooling. It's just what happens when multiple people start pushing content models to the same environment at the same time.

Let's talk about that.


The Reality of Parallel Development

Picture this: you're on a feature branch, building out a HeroBanner component. Your teammate is on a separate branch, refining the ArticlePage schema. Both of you are pointing your CLI at the same non-production CMS instance.

You run push. The CLI reads your optimizely.config.mjs, scans the entire src/components/ directory, and attempts to sync everything it finds — your changes, your teammate's changes, and every other content type definition sitting in the codebase.

Now, if your local branch doesn't have your teammate's latest work (because they haven't merged yet), the CLI sees a mismatch. It flags a conflict. You get an error.

This is actually good behavior. The CLI is protecting you from accidentally overwriting someone else's work. Optimizely built in a --force flag for exactly this reason — when you know what you're doing and want to push regardless, you can.

But here's where the mental model needs to shift.


The Force Flag Solves the Wrong Problem (For This Scenario)

Let's be clear — the --force option is a legitimate and useful tool. It exists because there are real scenarios where you need to override what's on the remote instance. Schema migrations, breaking changes, cleanup operations — force is the right call.

But in a parallel development workflow, the question isn't "how do I push harder?" — it's "why am I pushing things I didn't change?"

Think about it. If you only modified HeroBanner.tsx, why should the CLI even look at ArticlePage.tsx? Why should it care about 47 other component files that you never touched?

The problem isn't conflict resolution. The problem is scope.


A Different Way to Think About It

Here's the mental shift: instead of treating the CLI push as a full sync operation, treat it as a deployment of your changes.

This is how we already think about code deployments. Your CI/CD pipeline doesn't redeploy every microservice when you change one. Your database migration tool doesn't re-run every migration from the beginning. You deploy what changed.

Content model pushes should work the same way.

The good news? The CLI already supports this. The optimizely.config.mjs file controls which components the CLI processes. If you narrow the components array to only the files you've changed, the CLI will only push those content types. No conflicts with your teammate's work. No force flags needed. No wasted API calls.

The trick is doing this dynamically — based on your actual Git branch changes.


Git as Your Scope Engine

Your version control system already knows exactly what you changed. A simple git diff against your base branch gives you the precise list of modified .tsx files. From there, it's straightforward to generate a temporary config file, push only those components, and clean up.

Here's a Node.js script that handles the entire flow:

// push-branch-changes.mjs
import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';

const baseBranch = process.argv[2] || 'origin/main';
const tempConfig = 'optimizely.config.temp.mjs';

try {
  console.log(`Checking changes against ${baseBranch}...`);

  const gitOutput = execSync(
    `git diff ${baseBranch} --name-only -- "src/components/**/*.tsx"`,
    { encoding: 'utf8' }
  );

  const changedFiles = gitOutput
    .split('\n')
    .map(f => f.trim())
    .filter(f => f.length > 0 && fs.existsSync(f));

  if (changedFiles.length === 0) {
    console.log('No component changes on this branch. Nothing to push.');
    process.exit(0);
  }

  console.log(`\nPushing ${changedFiles.length} changed component(s):`);
  changedFiles.forEach(f => console.log(`  - ${f}`));

  // Generate a temporary config scoped to changed files only
  const paths = changedFiles.map(f => `'./${f}'`).join(',\n    ');
  const config = `import { buildConfig } from '@optimizely/cms-sdk';

export default buildConfig({
  components: [
    ${paths}
  ],
});
`;

  fs.writeFileSync(path.resolve(tempConfig), config, 'utf8');

  // Push using the scoped config
  execSync(`npx @optimizely/cms-cli@latest push --config ${tempConfig}`, {
    stdio: 'inherit',
  });

  // Cleanup
  fs.unlinkSync(tempConfig);
  console.log('Done. Only your branch changes were pushed.');

} catch (err) {
  console.error('Push failed:', err.message);
  if (fs.existsSync(tempConfig)) fs.unlinkSync(tempConfig);
  process.exit(1);
}

Wire it up in your package.json:

"scripts": {
  "cms:push-branch": "node push-branch-changes.mjs"
}

And run it:

# Compare against origin/main (default)
npm run cms:push-branch

# Or compare against a different base
npm run cms:push-branch origin/develop

That's it. The script diffs your branch, generates a scoped config, pushes only your changes, and cleans up after itself.


What This Actually Gets You

Speed. Instead of the CLI parsing and validating every content type in your project, it processes only the handful you touched. On larger codebases with dozens of content types, the difference is noticeable.

Safety. You literally cannot overwrite a teammate's content type because the CLI never sees it. There's no conflict to resolve, no force flag to debate, no Slack message asking "did someone just push over my changes?"

Focus. Your push operation maps 1:1 to your pull request. What you changed in code is what gets pushed to the CMS. Nothing more, nothing less. That's easier to review, easier to debug, and easier to roll back.


Where This Fits in a Bigger Picture

This approach pairs well with a structured environment strategy. Consider this setup:

Environment

Who Pushes

How

Developer instances

Individual devs

Branch-scoped push (this script)

Shared QA/Staging

CI/CD pipeline

Full push on merge to main

Production

CI/CD pipeline

Full push on release

Developers get fast, isolated feedback loops on their own instances. The shared environments only receive content models that have been reviewed and merged. Production stays locked down behind your release process.

The Optimizely CLI's full push behavior is exactly right for CI/CD — you want a complete sync when deploying from a merged, canonical branch. The branch-scoped approach fills the gap for the development phase, where speed and isolation matter more than completeness.


Wrapping Up

The Optimizely CMS SaaS CLI is a solid tool. The force option exists for a reason and handles real conflicts well. But parallel development introduces a different kind of challenge — not "how do I resolve conflicts" but "how do I avoid creating them in the first place?"

By scoping your pushes to only the content types you've actually changed, you sidestep the problem entirely. Your Git branch becomes the source of truth for what gets pushed, and everyone on the team stays out of each other's way.

It's a small script with a big impact on your daily workflow. Give it a try on your next feature branch.


Have you tried a similar approach or found other ways to streamline parallel development with Optimizely CMS SaaS? We'd love to hear what's working for your team.

Jul 20, 2026

Comments

error Please login to comment.
Latest blogs
Finding Thomas Part 5 - The Closed Loop

Five weeks. Five layers. One Thomas. If you've followed this series from the start — thank you. If you're just landing here, the short version:...

Ritu Madan | Jul 20, 2026

Extending the Optimizely Product Recommendations Feed to Include Custom Product Types

A practical way to extend the Optimizely Product Recommendations catalog feed so the export scheduled job also includes custom catalog types, like...

Wojciech Seweryn | Jul 20, 2026 |

Fixing index_not_found_exception After Purging External Data in Optimizely Graph

The Scenario: Indexing External Data When working with Optimizely Content Graph, indexing external data is a straightforward process. Synchronize...

Akash Borkar | Jul 16, 2026