Architecture and modules
SeeAMess is built from a few modules with one-way dependencies and no shared in-process state. One binary runs any subset of them.
Modules
Section titled “Modules”| Module | Owns | Scales with | Notes |
|---|---|---|---|
| Schema | Content types, fields, schema revisions, form metadata | Rarely changes | Loaded into memory as an immutable snapshot. A new revision swaps the snapshot. |
| Authoring | Content identity, tree, versions, publishing, references, access lists | Number of editors | The only writer of content. Every command is one transaction and appends to the outbox. |
| Projector | The published read model, routes, search feed |
Publish volume | Consumes the outbox. Can be rebuilt from versions at any time. |
| Delivery | Nothing. Reads published. |
Visitor traffic | Can run against a read replica, in another region, with no access to authoring tables. |
| Media | Blob metadata, uploads, image renditions | Upload and transform volume | Content-addressed blobs (SHA-256). Renditions are made on demand, signed, and cached. |
| Jobs | Scheduled publish, cleanup, extension jobs | Fixed | A job is claimed with a database lease, so it runs once however many nodes exist. |
| Editor UI | Nothing. Static files. | CDN | One React application. Forms come from the schema. |
No module reads another module’s tables, except Delivery reading published, which is the Projector’s public output.
One binary, many shapes
Section titled “One binary, many shapes”The seeamess binary has six roles: authoring, projector, delivery, media, jobs and editor. Choose them with SEEAMESS_ROLES or --roles:
seeamess --roles authoring,projector,delivery,media,jobsWithout either, every role runs: the shape for development and small sites. Production starts the same binary several times with different roles. Modules call each other through traits in process, and through HTTP when they are split.
| Roles in one process | Behaviour |
|---|---|
authoring and projector |
Commands project inside their own transaction, so a publish is visible at once. |
authoring without projector |
Commands only write the outbox; a process with projector projects the events. |
projector alone |
Consumes the outbox and projects every event written elsewhere. Two projectors do not double-apply. |
delivery alone |
Needs neither migrations nor the schema. Keeps answering while authoring is stopped. |
jobs |
Runs the job runner and one event consumer per subscribed extension. |
media |
Needs a migrated database; reads the schema itself. A process without it serves no files. |
editor |
Serves the editor’s static files at /editor/. |
Measured with authoring, projector and delivery as three processes on one database and a 250 ms outbox poll: a publish is visible on the delivery process after 245 ms (median of 20). See Performance results.
Languages
Section titled “Languages”- Rust for everything on a request path that must be fast or small: authoring commands, delivery, media, jobs.
- Rust or .NET 10, the integrator’s choice, for what integrators write: content types, extension hosts, typed clients.
- .NET for the Optimizely adapter, because it sits next to Optimizely code.
The boundary is always a wire contract (JSON over HTTP, events), never a shared library.
What is deliberately not built
Section titled “What is deliberately not built”| Goal | What it rules out |
|---|---|
| Fast, memory efficient | Runtime type scanning, runtime proxies, an unbounded object cache, a first-request initializer |
| Modular | A static service locator, modules that reach into each other’s tables |
| Scalable | Sticky sessions, per-node scheduled jobs, node-local caches that need a custom invalidation bus |
| Secure | A built-in user table, a first-admin registration page, module-level policies that can be forgotten |
| Easy to extend | In-process plugins, a second UI framework |
| A reversible path from Optimizely | Any Optimizely concept in the core, writing into Optimizely’s tables |