Working stores and archives
Many workflows need to pause and resume manifest authoring or reuse previously validated ingredients. Working stores and C2PA archives (or simply archives) provide a standard way to save and restore this state of a Builder.
Overview
Working store and archive refer to the same underlying concept:
- Working store emphasizes the editable state, the content of the editable C2PA manifest state (claims, ingredients, assertions) that has not yet been bound to a final asset. Typically used when describing "work in progress" manifest data.
- C2PA archive emphasizes the saved, portable representation, the artifact of the saved bytes (in a
.c2pafile or stream) resulting from a working store that you can read back to restore aBuilder.
An archive is simply a working store serialized as a normal manifest store.
Both use the standard JUMBF format (application/c2pa). The specification does not define a separate archive format; the SDK reuses the standard manifest store format:
- The same format is used for signed manifests (bound to an asset), working stores (saved for later editing), and saved ingredients (e.g. validated once, reused in other manifests).
- An archive can be embedded in files, stored as sidecars (for example,
.c2pa), or kept in the cloud or a database. - Unsigned working stores use placeholder signatures (
BoxHash). - Validate once, then reuse without re-validation.
Practical distinction:
- Saving a
Builderwithto_archive()produces a working store serialized as JUMBFapplication/c2pa(an archive). - Restoring it with
from_archive()orwith_archive()reads the archive back into aBuilderto continue editing.
You can't merge working stores by calling with_archive() repeatedly.
API summary
| Operation | API | Description |
|---|---|---|
| Save | builder.to_archive(&mut stream) | Writes the working store to stream. By default, generates the current archive format. Use the setting builder.generate_c2pa_archive = false to specify legacy ZIP format. |
Restore to a new Builder | Builder::from_archive(stream) | Creates a default-context Builder and loads the archive into it. |
| Restore (existing context) | builder.with_archive(stream) | Loads the archive into an existing Builder (preserving its context). |
Legacy ZIP archive format
The SDK also supports an older format: a ZIP file containing manifest.json, resources/, and manifests/ (see Settings). This ZIP format is generated when builder.generate_c2pa_archive = false. When builder.generate_c2pa_archive = true (default), to_archive() writes the C2PA working-store format. Restore accepts both (with_archive / from_archive): it tries ZIP first, then falls back to the C2PA format.
Best practices
- Use intents: Set an intent to get automatic validation and action generation.
- Archive validated ingredients: Save expensive validation results.
- Use shared context: Create once, share across operations.
- Label ingredients: Use labels to link ingredients to actions.
- Store archives flexibly: Files, databases, and cloud storage all work.
Examples
Run the builder example:
cd sdk
cargo run --example builder_sample
Saving a working store
When using the archive format, saving a Builder does the following:
- Prepares manifest data (assertions, ingredients, etc.) for signing.
- Adds a BoxHash assertion over an empty asset (placeholder), so the manifest is not bound to real content.
- Adds an ephemeral, self-signed signature for tamper detection only (not intended for public trust).
- Serializes to JUMBF
application/c2paand writes to the output stream (for example, a file orVec<u8>).
The resulting stream is the archive (the serialized working store).
The following sequence diagram shows the flow when Builder::to_archive(stream) is called.
Restoring a working store
Restoring from an archive does the following:
- Reads and parses the archive as JUMBF
application/c2pa. - Creates a
Readerand populates it from that stream. Note: Trust checks are relaxed so the archive's placeholder signature can be accepted. - Converts the
Readerback into aBuilderwithinto_builder(), so you can continue editing and later sign to a real asset.
The following sequence diagram shows the flow when Builder::from_archive(stream) or with_archive(stream) is called and the archive is in C2PA (JUMBF) format.
Common tasks
Save and restore a Builder
Use to_archive() to save a Builder:
pub fn to_archive(&mut self, mut stream: impl Write + Seek) -> Result<()>
For example:
// Save
let mut archive = Cursor::new(Vec::new());
builder.to_archive(&mut archive)?;
std::fs::write("work.c2pa", archive.get_ref())?;
Use from_archive to restore an archive using the default Context. Use with_archive to restore an archive using a custom shared Context:
pub fn from_archive(stream: impl Read + Seek + Send) -> Result<Self>
pub fn with_archive(self, stream: impl Read + Seek + Send) -> Result<Self>
// Restore (default context)
let builder = Builder::from_archive(Cursor::new(std::fs::read("work.c2pa")?))?;
// Or restore with a custom, shared context (see: docs/context.md)
let builder = Builder::from_shared_context(&context)
.with_archive(Cursor::new(std::fs::read("work.c2pa")?))?;
Capture an ingredient as an archive and reuse it
// Capture and sign a C2PA-only archive (no embedded asset)
let signer = context.signer()?;
let ingredient_c2pa = builder.sign(
signer,
"application/c2pa",
&mut io::empty(),
&mut io::empty(),
)?;
This returns the raw C2PA manifest store as Vec<u8>.
Later, you can add that archived ingredient to a new manifest as follows:
let mut builder = Builder::from_shared_context(&context)
.with_definition(
json!({
"title": "New Title",
"relationship": "componentOf"
})
)?;
builder.add_ingredient_from_stream(
json!({
"title": "Archived Ingredient",
"relationship": "componentOf",
"label": "ingredient_1"
})
.to_string(),
"application/c2pa",
&mut Cursor::new(ingredient_c2pa),
)?;
builder.add_action(json!({
"action": "c2pa.placed",
"parameters": { "ingredientIds": ["ingredient_1"] }
}))?;
Calling add_ingredient_from_stream() with format "application/c2pa":
- Reads the archive.
- Extracts the first ingredient from the active manifest.
- Merges with provided JSON properties, but your overrides take precedence.
This ensures:
- No long chains of signed manifests.
- Better user experience.
- Support for iterative workflows.
Override archived ingredient properties
JSON properties passed to add_ingredient_from_stream() override archived values:
builder.add_ingredient_from_stream(
json!({
"title": "New Title", // Overrides archived title
"relationship": "componentOf" // Overrides archived relationship
})
.to_string(),
"application/c2pa",
&mut archived_stream,
)?;
For creating and sharing a Context (including using Arc), see: Configuring the SDK using Context.
Link ingredients to actions
Use labels to reference ingredients in actions:
builder.add_action(json!({
"action": "c2pa.placed",
"parameters": {
"ingredientIds": ["ingredient_1"], // References the label
}
}))?;
FAQs
Can I use both old and new archive formats?
Yes. Loading an archive automatically detects supported formats.
Can I modify an archived ingredient's properties?
Yes. JSON properties passed to add_ingredient_from_stream() override archived values.
Where should I store archives?
Anywhere—Local files, S3, databases, and in-memory all work.
Can I have multiple parent ingredients?
No. Only one parent is allowed. Other ingredients use different relationships (for example, componentOf, inputTo).