The Silent Success: When Your Optimizely SaaS CMS Config Push Succeeds with "0" Changes
Picture this frustratingly common scenario in headless, code-first development with Optimizely SaaS CMS:
You’ve defined a brilliant new element, block, or page type in your React codebase. You trigger your local push command. The console works its magic, spinning up green indicators and a clean finish:
📤 Pushing content types...
✓ Configuration file uploaded
✅ Push succeeded.
But when you open the Optimizely CMS UI, navigate to Settings > Content Types, and look for your new type... there is absolutely, crushing nothingness.
You immediately jump into emergency triage mode. You double-check your .env keys. You check permissions. Everything is correct. So why did the CLI say "Success" while doing absolutely nothing?
This is the CLI Silent Empty Push — and to debug it, you have to work through a specific hierarchy of checks. Let's trace the journey from the most obvious suspects down to the real, silent culprit.
The "Everything Looks Correct" Checklist
Before tear-downs and code reviews, developers usually start with the obvious infrastructure questions. Here is the checklist of things you likely already verified:
🔍 Check 1: Client ID & Client Secret
-
The Question: Are your credentials expired or invalid?
-
How to verify: Run npx @optimizely/cms-cli@latest login. If the connection is broken, the CLI will throw an authentication error immediately. If it says "Authentication complete," your credentials are functional.
🔍 Check 2: API Permissions & Impersonation Scopes
-
The Question: Does your API Key have the rights to actually write schemas?
-
How to verify: Check Settings > API Keys in the CMS UI. Ensure the key is active and possesses write scopes. If your key lacks permissions, a push would trigger a stark 403 Forbidden or 401 Unauthorized error in your terminal.
🔍 Check 3: The Target Environment URL
-
The Question: Are you accidentally pushing to the wrong sandbox or staging instance?
-
How to verify: Inspect your OPTIMIZELY_CMS_API_URL environment variable. If you pushed to a different instance, the schemas would exist, just on the wrong server.
If all of those are green, why is the CMS still empty?
If credentials, permissions, and host URLs are completely correct, and the terminal prints a successful 200 OK, you have officially bypassed standard configuration errors.
The issue isn't security or network-related. It is a silent, 12-character path resolution discrepancy.
What the Official Documentation Says
The official Optimizely Configure JavaScript SDK guide is explicit about where the configuration file should live. Step 4 reads:
*"Create an optimizely.config.mjs file in the root of your project."
The official project structure diagram reinforces this:
.
├── src/
│ └── components/
├── .env
├── optimizely.config.mjs ← root level, explicitly
└── ...
The documentation does not cover what happens when you move this file into a subdirectory — because it assumes you won't. Developers following natural project organisation instincts — a clean root, a monorepo structure, a dedicated config/ folder for multiple environment files — deviate from this guidance without realising there is a hidden consequence.
The CLI works perfectly when the file is at the root. The moment it moves into a subdirectory, globs silently resolve against the wrong base path, and the push uploads nothing.
The Real Culprit: The Glob Resolution Trap
In clean, modern JavaScript monorepos or well-structured project hierarchies, developers rarely leave configuration files floating in the root directory. To keep things tidy, we often organize configurations into subdirectories like .optimizely/ or config/.
For instance, you might move your CMS configuration to:
config/optimizely.config.mjs
And inside that nested configuration file, you write your relative component search paths:
import { buildConfig } from '@optimizely/cms-sdk';
export default buildConfig({
components: ['./src/content-types/**/*.ts'],
});
This is where the silent trap springs. Here is the golden rule of the Optimizely CMS CLI:
⚠️ The CLI resolves glob patterns inside a configuration file relative to the location of the configuration file itself, not relative to the root directory where you ran the CLI command.
Because the configuration file lives inside the config/ subdirectory, the CLI reads ./src/content-types/... as relative to config/. It starts searching for your schemas here:
config/src/content-types/**/*.ts
That subdirectory path does not exist. The CLI scans the folder, matches 0 files, and finds exactly 0 schemas to process.
Why did the CLI report success?
The Optimizely CLI is highly permissive. If a glob pattern matches zero files, it does not throw an error or fail the build. It assumes you just have a clean configuration slate, packs up an empty schema payload, and uploads it.
The CMS API receives a valid, perfectly formed (but empty) configuration payload, updates nothing, and returns an HTTP 200 OK. The CLI reports Configuration file uploaded because the upload itself did succeed.
The result: a completely silent, completely successful push of nothing.
The Fix
The simplest fix is to follow the official documentation — keep your configuration file at the project root where the CLI expects it:
# ❌ Nested (globs resolve relative to config/ — finds nothing)
npx @optimizely/cms-cli config push config/optimizely.config.mjs
# ✅ Root level (globs resolve relative to root — finds your files)
npx @optimizely/cms-cli config push optimizely.config.mjs
If you have a strong reason to keep the config file inside a subdirectory — such as a monorepo setup or managing multiple environment configurations — you can adjust the glob paths inside the config to step up one level:
import { buildConfig } from '@optimizely/cms-sdk';
export default buildConfig({
// ../ steps up from config/ back to the project root
components: ['../src/content-types/**/*.ts'],
});
Both approaches work. The root-level approach aligns with official documentation and is the least error-prone for single-app projects. The ../ prefix approach is useful for monorepos or multi-environment setups where nesting is intentional.
Reusable Debugging Checklist for Teams
Bookmark this order-of-operations checklist for the next time a developer on your team runs into missing content types after a successful-looking push:
-
CLI Authentication Test — Run npx @optimizely/cms-cli@latest login to rule out expired credentials.
-
API Key Scope Verification — Confirm your key has write access under Settings > API Keys in the CMS.
-
Environment URL Check — Verify OPTIMIZELY_CMS_API_URL points to the instance you are currently viewing.
-
Config File Location Audit — Is your optimizely.config.mjs (or any dynamically generated config) in the project root? If it is nested in a subdirectory, check whether the glob paths inside use ../ to compensate.
-
Glob Match Validation — Test whether your glob patterns actually match any files before pushing:
import { globSync } from 'fast-glob'; const matched = globSync('./src/content-types/**/*.ts'); if (matched.length === 0) { console.error('❌ Zero files matched. Config push will be empty.'); } -
Visual Builder Composition — If your type appears in Settings > Content Types but not in the Visual Builder editor, go to that type's Settings tab and enable "Available for composition in Visual Builder".
Key Takeaway
The Optimizely developer documentation is clear: optimizely.config.mjs belongs at the root of your project. When it lives there, everything works exactly as expected. When it moves — even for completely legitimate reasons — the CLI's glob resolution silently breaks, the push uploads an empty payload, and the CMS returns a perfectly polite 200 OK.
The fix is simple. The behaviour, however, is not documented — which is precisely what makes it one of the most confusing silent failures in a SaaS CMS headless setup.
One line change. Types appear instantly.
Have you run into other silent failure modes when working with Optimizely SaaS CMS content type workflows? Share your experience in the comments — this kind of institutional knowledge keeps teams shipping faster.
Comments