umbraco-env-sync · git:20261016.edce7f4 · 2026-10-16 · sha256 16bb32960137c10a
umbraco-env-sync git:20261016.edce7f4A
Immutable. This exact content never changes and is served at /api/v1/blob/16bb32960137c10a.
--- name: umbraco-env-sync description: >- Moving an Umbraco package's own entities between environments with Umbraco Deploy or uSync: what moves as files and what moves over the wire, export shape and dependencies, ordered restores, keeping secrets out, linking users by alias, translating stored ids, and keeping sync code current. Use when adding Deploy or uSync support, or changing an entity the package syncs. Not for deploying the site itself. --- # Moving package data between environments A site runs in more than one place: a developer's machine, a staging site, live. Umbraco moves its own schema and content between them. A package's own data (its settings, its records in its own tables) only moves if the package teaches a sync tool how. Two tools do this, and a package can support either, both, or neither: - **Umbraco Deploy**, Umbraco HQ's licensed tool, used on Umbraco Cloud and on its own. - **uSync**, a community tool (Jumoo). Files on disk are free; push and pull between servers needs the licensed uSync.Complete. This file holds the rules that apply to both. The tool-specific API is in [references/deploy.md](references/deploy.md) and [references/usync.md](references/usync.md). Verify any API against the docs and the package versions for the major the project targets (`umbraco-docs`). ## Does this project sync? Supporting a sync tool is the package's choice. Look before assuming: - **Deploy:** a project references `Umbraco.Deploy.Infrastructure`. - **uSync:** a project references `uSync.BackOffice` or `uSync.Core`. If neither is there, this skill applies only when the work is to add support. If one is there, every new entity needs a decision on how it moves, and every change to a synced entity carries a sync change (see "Keep sync in step" below). ## Ship it as its own package Put the sync code in a separate project and NuGet package (`MyPackage.Deploy`, `MyPackage.uSync`) that depends on the main package. Sites that don't use the tool don't take its dependency. - Reference only the tool's packages the code needs, with a version range pinned to the CMS major (`[18.0.0, 18.999.999)`). Both tools release a major per CMS major. - Register after the main package (`[ComposeAfter(typeof(MyPackageComposer))]`), so the services the sync code calls exist. - Keep the main package free of any reference to the tool. If the main package must raise something the sync code listens to (a saved or deleted notification), that's a plain notification the main package publishes anyway. ## Choose how each entity moves Decide per entity, and write the choice down in the project's `CLAUDE.md` so later changes follow it. | The entity is | Move it | Why | |---|---|---| | Configuration a developer sets once and every environment shares (providers, connections, store settings, templates, a settings singleton) | **As files on disk**, committed to source control and applied on deploy | It's part of the site's setup, like a Document Type. It should travel with the code. | | Data editors create and change, and that differs between environments (catalogue records, editor-made presets) | **Over the wire**, an editor picks it in the backoffice and sends or restores it | Editors decide when it moves. Committing it to source control would fight their edits. | | Data users or the site create at run time (orders, submissions, logs, generated codes) | **Not at all** | It belongs to the environment it was made in. Moving it overwrites real records. | Something an entity in the first group links to must also be in the first group, or be something the target always has. A file can't depend on a record an editor may never send. ## The export shape Each entity becomes one exported item (Deploy: an artifact; uSync: an XML file). - **Keep the entity's id the same in every environment.** Create entities with a GUID key, and when the sync tool creates one on the target, give it the source's key. Integer ids never travel. - **Link to other entities by key, typed.** Store a reference as the tool's typed id (Deploy: a `GuidUdi`; uSync: the key), not a raw GUID with no type and never an integer. - **Declare every link as a dependency.** A link the export doesn't declare is a link that can arrive dangling. This includes lists of ids and links buried in settings JSON. - **Look up by alias when the key doesn't match.** An entity created by hand on both sites has two different keys. Find the existing one by key first, then by alias or code, and update it rather than creating a duplicate. - **Make the output stable.** Sort lists, fix decimal places, serialize polymorphic values against their runtime type. The same entity must export to the same bytes, or every save shows a change and every diff is noise. - **Read old exports.** A file written by an earlier version of the package can arrive at a newer one. A missing field keeps the target's value or a default. A new field is optional on read. ## Restore in passes A sync tool restores many entities in one run, and some link to others. The order matters: an entity can't link to something that doesn't exist yet. Both tools solve this with passes: every entity's code runs in the passes it asks for, and all of one pass finishes before the next starts. 1. **Draw the graph.** List the entities and which ones each links to. 2. **Turn it into a pass table.** Entities that link to nothing come first. An entity comes after everything it needs. 3. **Break cycles and late links with a fill-in pass.** When two entities link to each other, or an early entity links to a later one (a store's default country, where the country belongs to the store), create the entity early with those links empty, then fill them in a late pass once everything exists. | Pass | Does | |---|---| | 1 | Create entities that link to nothing, and the bare shells of entities others hang off | | 2 | Create entities that need pass 1 to exist | | 3 | Create entities that need pass 2 | | last | Fill in every link that was left empty | Write the table into the sync code's own docs, or as a comment next to the pass numbers. It's the thing that breaks when someone adds a link. - **Be lenient where a lost value is better than a failed run.** A link to something the target doesn't have: drop it and log a warning, unless the entity can't work without it, then fail with a message that names it. A value outside the target's limits: clamp it. One bad item shouldn't stop the whole restore. - **Save each pass's work before the next pass reads it.** Keep the entity the pass created so the fill-in pass updates it rather than finding or creating it again. ## Keep secrets out Exports end up in source control, in transfer packages, and on other servers. A secret in one has leaked. - **Leave secrets out by default.** API keys, passwords, tokens, connection strings, and encrypted values don't go into the export unless the site owner turns them on. - **Let config references through.** A setting that points at configuration instead of holding the secret (`umbraco-security`'s "Secrets and sensitive configuration") is safe to move. Each environment resolves it to its own value. - **Give site owners an ignore list.** A setting naming fields to always leave out, for provider settings the package doesn't know about. - **Merge on restore, don't replace.** A field left out of the export must keep the target's own value, not be wiped. - **Mark fields sensitive where they're defined.** The sync code reads the marker; it shouldn't keep its own list of field names to skip. - **Don't rely on spotting encrypted values.** A value encrypted at rest can reach the sync code already decrypted, because the package's service decrypts on read. Filter on the field's sensitive marker, and test with a real transfer that the secret isn't in the export. ## Users, groups, and members Deploy doesn't move users, user groups, or members, and free uSync doesn't either. Their keys differ between environments. - **Link to a user group by alias, a user by username or email, a member group by name.** Look them up on the target by that value, and convert back to the target's key. - **Skip a link the target can't resolve, and log a warning.** Don't fail the restore. - **Watch what a lost link means.** If dropping it widens access (a lost "deny this group" rule lets the group in), resolve the rule's meaning before skipping: fail, or keep the target's existing rule. A permission that quietly loosens on deploy is a security bug. - The same applies to anything else the tool doesn't move: media referenced from settings (move it as a typed media key so the tool can bring it along), languages (by ISO code), and entities from another package without sync support. ## Property values that store ids A property editor whose stored value holds an entity id (a picker for the package's own entities, a price keyed by currency) needs a translator, so the id is swapped for a typed key on export and back on import, and the entity is declared as a dependency. Deploy calls it a value connector, uSync a mapper. Without one, the content arrives pointing at nothing. ## Keep sync in step Once a project syncs an entity, the sync code is part of that entity. A change that skips it ships an entity that quietly stops round-tripping. | Change | Sync change that goes with it | |---|---| | New field on a synced entity | Add it to the export and the restore. Optional on read. Leave it out if it's a secret. | | New link to another entity | Store it as a typed key, declare the dependency, and check the pass table still holds. | | New entity type | Decide how it moves (table above), then the full set: export shape, restore passes, registration, tests. | | Renamed or removed field | Keep reading the old name from existing exports. | | New property editor that stores ids | A value translator. | `feature-design` and `feature-plan` carry these into the plan as tasks; `builder` builds them alongside the entity change; `reviewer` checks they're there. ## Test it - Unit test each connector or serializer: export an entity, check the output, restore it onto an empty target, check the result. Cover a missing dependency and a secret field. - One scenario test across entities, restoring a full set in pass order, catches a broken pass table. - Run a real transfer between two local sites when sync support is added, and when the pass table or the secret handling changes. Both tools can do it on one machine without a license: [references/real-transfer.md](references/real-transfer.md). Check the export for a fake secret, and the target's database for the result. ## Reviewing a diff for environment sync Apply when the project syncs (see "Does this project sync?"). - A new entity with no decision on how it moves (files, over the wire, or not at all), or one that should move with no sync code. - A synced entity's model gained, lost, or renamed a field, and the export and restore didn't change with it. - A new link stored as a raw GUID or integer, or not declared as a dependency. - A new link that breaks the pass table: restored before the thing it points at, with no fill-in pass. - A secret field exported with no default exclusion; a restore that wipes a field the export left out. - A user, user group, or member stored by key instead of alias; an unresolved one that fails the restore, or widens access when skipped. - An entity users create at run time added to the sync. - Unsorted lists or culture-dependent formatting in the export. - An entity created on the target with a new key instead of the source's. - A new property editor storing ids with no value translator.