How to Build a Versioned Notion Archive in C#

A backup is useful when you can restore it. A local catalogue is useful when you can inspect what it contains, identify a document and find an earlier revision without opening every exported file. ObjectSpaces provide a compact storage layer for building that second capability around an existing Notion backup workflow.

This example imports a small, deliberately normalised manifest into a transactional C# store. It records immutable revisions, skips a revision already present and performs a visible title search. It does not require live Notion credentials, and it does not pretend that Notion's ordinary Markdown/CSV export is a JSON API response.

A versioned archive retaining successive snapshots of Notion content.
A versioned archive retaining successive snapshots of Notion content.

Contents

Define a Safe Import Boundary

Project the documents you intend to archive into PageId, Title and Body fields. A real exporter can produce that manifest from selected Markdown files, CSV metadata or a documented Local Notion export. The downloadable example uses two public fixture records so the storage behaviour can be reproduced without an account.


[{"PageId":"guide-001","Title":"Transaction Basics","Body":"Commit an accepted batch."},
 {"PageId":"guide-002","Title":"Working With Indexes","Body":"Look up a stored key."}]

⚠️ Import selected content, not an application's credential store. Do not publish integration keys, Authentication properties or Local Notion registry files as part of a downloadable archive.

Install and Declare the Store


dotnet new console --framework net10.0
dotnet add package Sphere10.Framework --version 3.1.3

The model retains a numeric identifier, the source PageId, a RevisionKey and the document text. The unique revision index supports duplicate detection. This example uses a straightforward title scan rather than an indexed full-text engine.


builder.AddDimension<ArchivePage>(ignoreAnnotations: true)
    .WithIdentifier(item => item.Id)
    .WithUniqueIndexOn(item => item.RevisionKey)
    .Done();

Treat Each Revision as Immutable

Derive the revision key from the native page identity and a hash of the normalised title/body. If that key already exists, skip the record. A changed document produces a different revision key and is appended as a new revision rather than mutating an indexed historical record.


var digest = Convert.ToHexString(Hashers.Hash(
    CHF.SHA2_256, Encoding.UTF8.GetBytes(document.Title + "\n" + document.Body)));
var revisionKey = document.PageId + ":" + digest;
if (store.TryGet<ArchivePage, string>(item => item.RevisionKey, revisionKey, out var existing))
    continue;

That key identifies the chosen projection, not every original file byte or a legally authenticated document. If attachments, formatting or export metadata matter, include their versioned identities in your archive design and retain the original export separately.

Commit and Search

The complete sample creates the pending-page directory, loads the FileObjectSpace, performs its work under an access scope and explicitly commits. Create, use and dispose that store on the same owner thread. There is no cross-thread UI work hidden inside the importer.


((FileObjectSpace)store).Commit();
var matches = store.GetAll<ArchivePage>()
    .Where(page => page.Title.Contains("transaction", StringComparison.OrdinalIgnoreCase))
    .ToArray();

GetAll returns the saved live objects. The sample's title predicate scans that snapshot without turning it into an indexed full-text query. The unique revision lookup is the indexed operation in this example.

Run the project with a fresh caller-selected folder. The first run inserts two revisions and finds one title. The second run against the same folder inserts zero revisions while retaining the same two records. This reopen check establishes persistence and repeatable import behaviour, not merely a cached result inside one process.


First run: Inserted revisions: 2; Title scan matches: 1; Stored revisions: 2
Second run: Inserted revisions: 0; Title scan matches: 1; Stored revisions: 2

Grow the Archive Deliberately

For a larger collection, decide whether the application needs a separate token index, an established search engine, attachment integrity checks or a browser UI. Keep those responsibilities explicit. The small example already demonstrates useful building blocks: typed storage, a unique lookup path, append-only history and transactional publication.

Begin with the existing Notion backup guide , then compare the ObjectSpace primer and transaction tutorial . The next article shows how to run this importer under systemd without turning the storage library into a deployment framework.

Continue the Series

ObjectSpace, ORM or Serializer: Which Problem Does Each Solve?

How to Give Your C# Objects Commit and Rollback

How to Run a Persistent .NET Importer Under systemd

sample.zip
Download the complete worked example and build instructions (ZIP).

For the broader Sphere 10 development toolkit, see the Sphere10 Framework overview .