Syncing Strapi V5 Content Across Environments
A guide to syncing schemas, content, nested components, and locales between Strapi V5 environments.
Strapi is a powerful headless CMS, but moving data between development, staging, and production environments can be tricky. Broadly speaking, there are two types of “syncing” you need to handle: Schema Syncing (how your content is structured) and Content Syncing (the actual data).
The Content-Type Builder’s collections and single types are stored as JSON files within your project’s src folder. Because these are flat files, you can sync them easily using Git.
The Workflow:
- Ensure your
srcfolder is not in your.gitignore. - When you create a new content type in the builder, Strapi generates a corresponding
.jsonfile. - Simply commit these files and push them to your target environment.
- Once the code is deployed and the server restarts, the new content types will be available.
Unlike schemas, content entries are stored directly in the database. To sync these, you need to use the Strapi REST API.
When building a sync script, you’ll typically hit an endpoint like: http://localhost:1337/api/restaurants.
Since database auto-increment ids can vary between environments, you should never rely on them for syncing. Instead, use a custom Unique Identifier (like a restaurant_code or slug).
The Step-by-Step Logic:
- Fetch from Source: Get the content from your source environment.
- Check Target: Hit the target environment with a filter query to see if the entry exists:
GET /api/restaurants?filters[restaurant_code][$eq]=MVH232 - Decide Action:
- If found: Capture the
documentIdfrom the response. Perform a PUT request to/api/restaurants/{documentId}to update it. - If not found: Perform a POST request to
/api/restaurantsto create it.
- If found: Capture the
Note: In Strapi 5, the
idis the local database primary key, while thedocumentIdis the unique string used to address entities across the API. Always target thedocumentIdfor updates.
When sending data to the target API, you must unset (remove) specific fields to avoid validation errors:
iddocumentIdpublishedAtcreatedAtupdatedAtlocalizations(Crucial to avoid schema conflicts)
If your content types use complex components or nested relations, a standard API call might not return all the data you need.
To solve this, I recommend the Strapi v5 Deep Populate Plugin. This allows you to fetch content to any depth level required for your sync.
Word of Caution: Deep population can significantly increase response times. For production environments, it is a good idea to cache the JSON responses at the API Gateway level to maintain performance.
When Internationalization (i18n) is enabled, Strapi handles content across different locales using the same documentId. Syncing localized content requires a recursive approach to ensure all versions of an entry are captured and linked correctly.
In Strapi V5, a collection type response includes a localizations array. This contains the metadata for other available language versions of that specific entry.
The Workflow:
- Fetch with Locale: To fetch a specific language, append the locale parameter to your request:
GET /api/restaurants?locale=hi(wherehiis the Hindi locale). - Recursive Sync: Fetch the entry in the default locale first. Then, iterate through the
localizationsarray and recursively call the fetch/sync logic for each associated locale. - Linking via documentId: To add a new locale version to an existing entry in the target environment, perform a PUT request to the endpoint using the entry’s
documentId. - Include the locale parameter in the URL:
PUT /api/restaurants/{documentId}?locale=hi
By hitting the PUT endpoint with a new locale, Strapi automatically creates that locale version and attaches it to the same document group.
Pro Tip: Just like standard syncing, you must strip the
id,createdAt, andupdatedAtfields. Crucially, you must also unset thelocalizationsfield from the request body. If you include thelocalizationsarray in your payload, Strapi V5 will throw a validation error because it manages those internal links automatically via thedocumentIdand the URL parameter.