OverlayRuns on top of a live Optimizely site

The lean headless CMS
to replace Optimizely

One Rust binary, PostgreSQL and a React editor. Put SeeAMess on top of your live Optimizely site without writing to it, move editors over page by page, and cut loose when you are ready.

  • 0.22 ms p99 delivery by path
  • 28.5 MB idle, every role
  • Never writes to Optimizely
The SeeAMess editor with an article open: the content tree of a small library site on the left, the schema-generated form with localized and shared fields in the middle, and the versions panel with a draft over the published version on the right.The SeeAMess editor with an article open: the content tree of a small library site on the left, the schema-generated form with localized and shared fields in the middle, and the versions panel with a draft over the published version on the right.

The Optimizely path

Migration is a period, not a moment

SeeAMess sits on top of a running Optimizely site and never writes to it. Until the last step, going back means discarding the overlay: Optimizely was never changed, so there is nothing to restore.

  1. Editors stay in Optimizely

    Read only

    A sync adapter copies Optimizely's content into SeeAMess as upstream versions. SeeAMess Delivery serves it, and your existing front-end runs unchanged against it.

    seeamess-migrate sync --source "…" --watch 30
    Optimizely
    System of record
    SeeAMess
    Serves reads
  2. Editors move page by page

    Editorial overlay

    Editors start publishing in SeeAMess. Where an overlay version is published, it wins; everywhere else the upstream content shows through. Changes upstream are flagged for comparison.

    GET /authoring/v1/overlay?state=upstreamChanged
    Optimizely
    Still syncing
    SeeAMess
    Overlay wins where published
  3. Optimizely can be retired

    Independence

    One command adopts all upstream content as native content. Nothing visible changes and nothing is re-projected. The adapter is switched off.

    POST /authoring/v1/sources/optimizely/adopt
    Optimizely
    Retired
    SeeAMess
    System of record

Parity checked, not assumed. seeamess-migrate verify against a live Optimizely CMS 12: 12 responses compared, no unexpected differences.

Same front-end. The existing front-end renders the same visible text from either CMS for all six demo pages.

Quiet cutover. After adopt, every delivery response is byte-identical to before.

Run SeeAMess on top of Optimizely →

The editor

Built for the people who publish every day

One React application that talks only to the public APIs. Forms are generated from the schema, opening a content is one request, and the initial bundle is 173 kB gzipped.

  • Ctrl/Cmd+K quick search
  • Signed preview links at three widths
  • Autosave with conflict dialog
  • Ready for review and reject
  • Scheduled publish
  • Recycle bin with restore
  • References: "used by"
  • English and Polish UI
  • Dark theme

Using the editor →

Why SeeAMess

Every goal rules something out

Optimizely's useful ideas are kept: a stable identity, immutable versions, a published pointer per language. The weight around them is not.

  • Fast and memory efficient

    Delivery reads one pre-built document per request; the schema is compiled at build time.

    Rules out: runtime type scanning, runtime proxies, an unbounded object cache.

  • Modular

    Modules with one-way dependencies and no shared in-process state. One binary runs any subset of them.

    Rules out: a static service locator, modules that reach into each other's tables.

  • Scalable

    Each module is stateless and scales alone. PostgreSQL and the blob store hold all state.

    Rules out: sticky sessions, per-node scheduled jobs, node-local caches that need a custom invalidation bus.

  • Secure

    Every command and query passes one authorization interface. Sign-in is OpenID Connect only.

    Rules out: a built-in user table, a first-admin registration page.

  • Easy to extend

    Extension hosts for server logic, ES modules for the UI, both outside the core process.

    Rules out: in-process plugins, a second UI framework.

  • A reversible path from Optimizely

    SeeAMess runs over a live Optimizely site and never writes to it. Going back means discarding the overlay.

    Rules out: Optimizely concepts in the core, writing into Optimizely's tables.

Architecture

One binary. Any shape you need.

Authoring is the only writer. Every command is one transaction that appends to an outbox; the Projector turns it into documents Delivery serves with a single indexed lookup. Delivery can run on a read replica, in another region, with no access to authoring tables.

SeeAMess modulesEditors use the Editor UI, which sends commands and queries to Authoring. Authoring writes outbox events that the Projector turns into the published read model. Delivery reads it and serves visitors and front-ends. Authoring uses the Schema, Policy, Media, Jobs and external extension host modules. PostgreSQL and a blob store hold all state.EditorsVisitors, front-endsEditor UIstatic React SPADeliveryread-only, cacheablecommands, queriesreads publishedAuthoringthe only writerProjectorbuilds publishedoutbox eventsin process, or HTTP when splitSchemaimmutable snapshotPolicyauthorize()Mediauploads, renditionsJobsleased, run onceExtension hosts.NET or Rust, externalStatePostgreSQLauthoring schema · published schema · outboxBlob storeS3 API or disk, addressed by SHA-256SeeAMess modulesEditors use the Editor UI, which sends commands to Authoring. Authoring writes outbox events that the Projector turns into the published read model, which Delivery serves to visitors. Schema, Policy, Media, Jobs and extension hosts support Authoring. PostgreSQL and a blob store hold all state.EditorsEditor UIstatic React SPAcommands, queriesAuthoringthe only writeroutbox eventsProjectorbuilds publishedpublishedDeliveryread-only, cacheableVisitors, front-endsModulesSchemasnapshotPolicyauthorize()MediarenditionsJobsleasedExtension hosts.NET or Rust, outside the coreStatePostgreSQLauthoring · published · outboxBlob storeS3 API or disk
Roles of the one binaryauthoringprojectordeliverymediajobseditorAll in one process for development and small sites; several processes with different roles in production.

What each module owns and scales with →

Performance

Measured against a budget set before the code

Delivery reads one pre-built document per request. No ORM, prepared statements, an ETag computed at projection time. Every target below was written down first; each bar shows how much of it the measured result uses.

  • Delivery, content by path

    0.22 ms p99

    Target p99 under 5 ms4%

    p50 0.09 ms, 2,000 sequential requests

  • Conditional GET (304)

    0.22 ms p99

    Target p99 under 1 ms22%

    p50 0.09 ms

  • Cold start to first request

    13–25 ms

    Target under 500 ms5%

    authoring, projector and delivery roles

  • Resident memory, idle

    28.5 MB

    Target under 60 MB48%

    every role; 9 MB with authoring, projector and delivery

  • 4000 px JPEG to 1200 px WebP

    105 ms median

    Target under 150 ms70%

    to JPEG 51 ms; pure Rust plus libwebp

  • Editor, initial JavaScript

    173 kB gzipped

    Target under 250 kB69%

    rich text and admin load on demand

  • 245 mspublish to visible on a separate delivery process (outbox poll 250 ms)
  • 20,000contents with 42,000 versions synced in 40 s; repeat sync with no changes 8 s
  • 0.24 msroute lookup p99 at that volume
  • 30 MBthe whole engine binary, every role included

Measured 2026-11-13 on a development machine (Ryzen 7, 16 threads, local PostgreSQL 17), release build, one process running every role unless stated. Authoring save and publish latency (targets 30 ms and 50 ms p99) are not measured separately yet. Full results and known limits

Developer experience

Your content model is code you can review

Port an Optimizely model by changing base types and attributes. The front-end reads natural JSON keyed by field name.

Define content types
[ContentType(Id = "2066d4d5-0dff-40c0-b42b-cc35e694fa73",
    DisplayName = "Article",
    Description = "A news or blog article.")]
[AllowedChildren]
public class ArticlePage : Page
{
    [Localized, Field(Order = 10, DisplayName = "Summary",
        Editor = Editors.Textarea)]
    public string? Summary { get; set; }

    [Field(Order = 20, DisplayName = "Image", Editor = Editors.Image)]
    public ContentRef? Image { get; set; }

    [Localized, Field(Order = 30, DisplayName = "Main body")]
    public RichText? MainBody { get; set; }

    /// <summary>Shared across languages (not localized).</summary>
    [Field(Order = 40, DisplayName = "Author")]
    public string? Author { get; set; }
}
Content types in code
C# classes or Rust structs. Both SDKs derive the same field IDs, so a Rust model and a C# model of one type agree.
No proxies, no startup scan
The classes are plain records at runtime. The server never loads integrator assemblies; it reads a JSON manifest.
Renames are free
Field IDs are UUIDv5 of the type ID and the original name. Version data is keyed by ID, so renaming needs no data rewrite.
A plan before anything changes
Breaking changes, such as changing a field kind, are refused unless you push again with --force.

Start with a read‑only overlay.There is nothing to roll back.

Run SeeAMess locally in a few commands, point it at a copy of your Optimizely database, and compare both delivery APIs before anyone changes how they work.