Portability
Portability
Exports content-type definitions and their content as one JSON bundle, and imports a bundle into another instance. Useful for backups, moving between environments, seeding a new tenant, and sharing content-type templates.
Obsolete since 4.6.0, removal planned for barakoCMS 6.0. The config engine in the core covers
the bundle: POST /api/config/plan and POST /api/config/apply accept a bundle unchanged, and
GET /api/config/export?sections=contentTypes,contents exports the same types and entries. With
this module installed it registers the contents section that makes that work. The two routes
below keep working until the removal.
Enable it
dotnet add package BarakoCMS.Portability
builder.Services.AddBarakoCMS(builder.Configuration);
var app = builder.Build();
app.UseBarakoCMS();
The package reference plus a restart is the install. AddBarakoCMS finds every module in the
application's dependency context, and BarakoCMS:Modules:Enabled decides which of them run
(BarakoCMS__Modules__Enabled=Portability). Unset, every referenced module runs and the API logs
one warning saying so. To name it by hand instead, put
modules.Add(new BarakoCMS.Portability.PortabilityModule())
in the AddBarakoCMS callback; discovery skips a type the host already added. See MODULES.md in
the repository.
Endpoints
| Method & path | Purpose | Access |
|---|---|---|
GET /api/portability/export |
Download a bundle | Admin / SuperAdmin |
POST /api/portability/import |
Apply a bundle | Admin / SuperAdmin |
How an import behaves
- Content types are upserted by name, so re-importing an evolved bundle updates the type rather than duplicating it.
- Content is recreated through events, so imported content has real history and behaves identically to content authored in place.
- Each entry keeps the document-level sensitivity the bundle records for it, so a restored Hidden entry stays Hidden. A bundle from before 4.3.1 records none, and its entries import as Public.
- Each content type goes through the checks
POST /api/content-typesruns, its lifecycle included, and a stored type keeps the add field rule: a required field with no default is refused on a type that already has entries. An import does not change who may read a stored field; a bundle that raises, lowers or leaves out a non-Public field is refused, andPUT /api/content-types/{name}/fields/{field}/sensitivityis the way to change it. - An import does not change the currency or scale a stored money field declares either. A bundle
that carries a different one, or none where the stored field has one, is refused, and
PUT /api/content-types/{name}/fields/{field}/currencyis the way to change it. A field the stored type does not have yet takes what the bundle declares. - A field's
editor,sectionandroleand a type'srouteTemplatetravel in the bundle and are checked the wayPOST /api/content-typeschecks them. Over a stored type, a value the bundle carries replaces the stored one and a member it does not carry is kept, so a bundle exported before these members existed leaves them as stored. A bundle cannot clear one;PUT /api/content-types/{name}/fields/{field}/presentationandPUT /api/content-types/{name}/route-templatedo. - A type's
uniquenessrules travel in the bundle. A type the import creates takes them, checked the wayPOST /api/content-typeschecks them. A stored type keeps its own: a bundle carrying none leaves them, and one carrying different rules, or leaving out a field a stored rule compares, is refused;PUT /api/content-types/{name}/uniquenessis the way to change them. An entry holding values a stored entry or an earlier entry of the bundle holds under a rule is refused by its index, unlike the singleton cap. - Each entry goes through the same write path as
POST /api/contents: a field the caller may not see is dropped, the entry is validated against its type as the bundle leaves it, the type's lifecycle hooks run, and the entry starts in the type's initial lifecycle state. The singleton cap is the one exception, since an import is a restore and lands what the bundle holds. - A
tokenfield does not travel. An export leaves its value out of every entry, for every caller, and an import gives each entry it creates a new token, discarding one a bundle carries. So moving a site through a bundle changes every token: move the database to keep tokens that are already printed or sent. A bundle that turns a stored field into a token, or a stored token into another type, is refused. Seedocs/token-fields.md. - The import is one database transaction, and each entry is written before the next is checked, so lifecycle hooks see the entries before it: journal entries are numbered in sequence, an account's parent and a page's parent from the same bundle resolve.
- Each record carries the
idit had where it was exported. Import gives it a new id, and a reference field holding the old id of another record in the bundle is pointed at the new one, whatever order the records are in. A reference to anything outside the bundle must exist where the bundle is imported, so an entry referencing content left behind in another tenant is refused. - A bundle carries entries, not files. A
filefield holds a stored file's id, and the import checks it as a new attachment for the importing user, so an entry naming a file this tenant does not have is refused, and with it the whole import. Restore the files first, with the same ids, then import the bundle. Seedocs/file-fields.md. - A stored type keeps its lifecycle when the bundle has none. A bundle that changes it, or adds one to a type that already has entries, is refused.
- The entries' events are written together just before the commit, so a long import leaves no gap in the event sequence and workflows run for every imported entry. The rows the import writes are held for the whole transaction: run a large import outside busy hours, since a post that needs the same row (the journal entry number sequence, a content type the bundle changes) waits for it and may need a retry.
- All or nothing. A refused type or entry answers 400 naming each one as
contentTypes[i]orcontents[i], and nothing is written, so the fixed bundle can be imported again without duplicating what would have landed. A dry run refuses exactly what the real run would. - A bundle holds at most 5,000 entries (
Portability:MaxImportRecords) and 500 content types. Past either, the import answers 400 before reading anything. Move a larger site in several bundles using export'stypesparameter. - The import runs inside the calling tenant. A bundle carries no tenant identity of its own, which is what makes it safe to move between environments.
Treat a bundle as sensitive
An export shows each entry the way the content read endpoints would show it to the caller who asked
for it. An entry the caller's read rule for its type does not allow is left out, the same entry
GET /api/contents leaves out, and so is an entry they may read nothing of; both are counted in
contentsWithheld. A field the caller may not read comes out under its mask and is listed in the
record's maskedFields.
Import skips every field named in maskedFields, so a mask is never stored as a value. A full
backup therefore needs a caller who may read every entry and every field, such as SuperAdmin. A
role holding export_content and no read rule for a type exports none of that type's entries.
What the caller may read is still in the bundle, and it leaves the system's access control behind the moment it is downloaded.
Part of barakoCMS
This is an optional module for barakoCMS, an open-source
headless CMS for .NET 10. Every module is published under the barakocms-module tag, so a single
search on nuget.org returns the whole set.
Contributions are welcome — including a module icon or other design work. See CONTRIBUTING.md.
Licensed under MIT.
If barakoCMS is useful to you, a star on the repository helps other people find it.