← All docs
Guide

Architecture

The single design idea: content compiles into immutable artifacts, and deploys are pointer flips at the artifact layer. Everything else — A/B testing, drip publishing, replay, audit, multi-channel output — falls out of that.

Architecture

The single design idea: content compiles into immutable artifacts, and deploys are pointer flips at the artifact layer. Everything else — A/B testing, drip publishing, replay, audit, multi-channel output — falls out of that.

This doc covers:


The data model

Six primitives in the graph. Everything else is a variation on them.

┌─────────┐                  ┌────────────┐
│  Site   │ ─[BELONGS_TO]──▶ │  Platform  │
└────┬────┘                  └────────────┘
     │ owns
     ▼
┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐
│  ContentType    │  │   Template      │  │      Route      │
│  (Versionable)  │  │ (Versionable)   │  │ (Versionable)   │
└────────┬────────┘  └─────────────────┘  └─────────────────┘
         │ HAS_TYPE
         ▼
┌─────────────────┐  ┌─────────────────┐
│  Content node   │  │     Asset       │
│  (Versionable)  │  │  (Versionable)  │
└─────────────────┘  └─────────────────┘

           [Versionables get bundled into Releases]
                            │
                            ▼
┌──────────────────────────────────────────────────┐
│                    Release                        │
│  - immutable bundle of Versionable versions       │
│  - state: draft | pending | approved | deployed   │
│                       …                           │
└──────────┬─────────────────────┬─────────────────┘
           │ INCLUDES (each item) │ HAS_REVIEW
           ▼                      ▼
   ┌──────────────────┐    ┌──────────────────┐
   │ ReleaseInclude   │    │     Review       │
   │ (snapshot data)  │    │ kind: ai/auto/   │
   │                  │    │       manual/    │
   │                  │    │       compliance │
   └──────────────────┘    └──────────────────┘

           [Releases get pointed at Environments via Deployments]
                            │
                            ▼
┌──────────────────────────────────────────────────┐
│                  Deployment                       │
│  - immutable fact: releaseId + envId + validFrom  │
│  - status: pending → building → deployed |        │
│           failed | superseded                     │
└──────────────────────────────────────────────────┘

Versionable kinds

A Release bundles Versionables. Currently supported kinds:

Kind What Source of truth
content content nodes (articles, pages, products, …) graph node, versioned
template render templates (njk, handlebars, etc.) graph node, versioned
type content type definitions graph node, versioned
route URL → template/content mappings graph node, versioned
asset media (images, files) content-addressed bytes
settings site-level settings graph node, versioned
widget reusable layout fragments graph node, versioned

Output kinds

Templates declare an outputKind — what channel the rendered output is for. The build pipeline dispatches per kind. See features.md → Multi-output for the full registry.

Kind Default DPI Print-bound Status
web 72 no shipping
social_post 72 no foundation
flyer 300 yes foundation
sign 150 yes foundation
qr_code 300 no shipping (generator + tests)
email 72 no foundation
ad_creative 72 no foundation
transcript 72 no foundation

"Foundation" means the registry + types ship; per-kind compilers land in subsequent phases.


Request lifecycle

Anatomy of a typical admin request (PATCH /api/content/:type/:id):

┌─────────────────────────────────────────────────────────────────┐
│ 1. CloudFront / API Gateway                                      │
│    - TLS, WAF, edge caching of static UI bundles                 │
└────────────────────────────┬─────────────────────────────────────┘
                             │
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│ 2. Express server (packages/server)                              │
│    Middleware chain on /api/*:                                   │
│      a. cognitoAuthMiddleware  → req.auth.{userId, email, …}     │
│      b. platformContext        → req.platformUser, .platformRole │
│      c. requireSiteContext     → req.services.{graph, content,   │
│                                   templates, scripts, schema,    │
│                                   workflow, locale, transforms,  │
│                                   site, role}                    │
└────────────────────────────┬─────────────────────────────────────┘
                             │
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│ 3. Route handler (e.g., contentRoutes)                           │
│    - validates input                                             │
│    - calls req.services.content.update(...)                      │
└────────────────────────────┬─────────────────────────────────────┘
                             │
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│ 4. Service layer (packages/core/crud/*)                          │
│    - applies lifecycle hooks (onSave) via TransformService       │
│    - writes to the graph via GraphClient                         │
└────────────────────────────┬─────────────────────────────────────┘
                             │
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│ 5. Graphiquity engine (HTTP, /e/* on api.graphiquity.com)        │
│    - bitemporal append-only writes                               │
│    - audit chain hash update                                     │
│    - cypher response with the new state                          │
└─────────────────────────────────────────────────────────────────┘

For a public visitor request (the rendered site), the path is much shorter:

visitor → CloudFront edge → S3 (static-paths target)
                          ↳ Lambda@Edge resolver → manifest → S3 artifact (manifest-pointer target)
                          ↳ GitHub Pages / Cloudflare Pages / Vercel (github target, customer-hosted)

No app server, no database, no edge function in the visitor path when the deploy target is static-paths or github. That's the "no runtime to fail" pillar.


Build pipeline

packages/server/src/build/compile.ts is the entry point. The compileSite() function:

  1. Reads the deployment modelegacy reads envPublishedAt_<envId> scalars; new reads from the active Release; dual reads legacy and logs what new would do.
  2. Resolves content — for the new model, walks the active Release's INCLUDES edges to find the bundled content nodes at their bundled versions (using bitemporal AT '<iso>' queries).
  3. Renders each item — runs beforeRender lifecycle hooks, looks up the matching template, calls renderPage(tpl, fields, opts).
  4. Emits artifacts — HTML at /blog/post-1/index.html, plus optional JSON sidecars at /_meta/pages/blog/post-1.json for the GitHub target.
  5. Dispatches to publishers:
    • static-paths: writes files to disk; deployToS3() syncs to S3.
    • manifest-pointer: hashes each artifact, uploads to s3://artifacts/by-hash/<hash>.<ext>, builds a 256-shard manifest, atomically flips the current/<envId>.json pointer.
    • github: uses GitHubPublisher to clone the target branch, copy the build output, commit, and push.
    • both: runs static-paths and manifest-pointer in parallel.

All publishers are gated by predicates in packages/core/src/observability/index.ts:


The manifest-pointer deploy architecture

This is the "managed atomic-release" mode. It uses content-addressing + a sharded manifest + Lambda@Edge to deliver:

Layout in S3

s3://staticowl-artifacts/
  by-hash/<sha256>.<ext>         ← every artifact, content-addressed

s3://staticowl-manifests/
  <siteId>/<deploymentId>/
    manifest-root.json           ← totals, errors, hashAlgo
    shards/00.json … ff.json     ← 256 shards, xxhash32(path) → entry
  <siteId>/current/
    <envId>.json                 ← THE POINTER. Single S3 write = deploy.

Resolver flow (Lambda@Edge)

For a visitor request to https://example.com/blog/post-1/:

  1. L@E reads s3://staticowl-manifests/<siteId>/current/<envId>.json → gets { deploymentId, validFrom, switchedAt }.
  2. Hashes the request path with xxhash32 → picks a shard (00ff).
  3. Reads s3://staticowl-manifests/<siteId>/<deploymentId>/shards/<shardId>.json → maps /blog/post-1/ to { hash, ext, size }.
  4. Streams s3://staticowl-artifacts/by-hash/<hash>.<ext> to the visitor.

The pointer file is the single source of truth for "what's live". Changing it is one S3 write. Reverting it is another. No mixed-state window — either the new manifest is in front of all visitors, or it isn't.

A/B variants on the manifest

A manifest entry can carry multiple variants instead of a single hash:

{
  "/landing/": {
    "variants": [
      { "key": "A", "hash": "abc...", "ext": "html", "size": 1200, "weight": 0.5 },
      { "key": "B", "hash": "def...", "ext": "html", "size": 1300, "weight": 0.5 }
    ]
  }
}

The resolver runs selectVariant() per request: sticky-by-cookie, deterministic across visits, independent across paths. See features.md → A/B testing.


Multi-tenant story

Tenancy lives at three layers:

  1. Engine — Graphiquity hosts N graphs. Each tenant has at least one graph (e.g., kindatechnical, acme-corp). Engine API keys are graph-scoped.
  2. CMS server — One CMS server can serve many tenants by routing per X-Site-Id header. The requireSiteContext middleware looks up the site's graphName and constructs a per-request GraphClient against that graph.
  3. Per-tenant EC2 isolation (planned) — High-value tenants can be routed to dedicated EC2 instances via graph_router.js. Path to ECS/EKS for full isolation.

Site = tenant boundary. Every content node, template, type, route, release, deployment carries a siteId and the engine enforces graph-scoped access. Cross-tenant reads/writes are not possible.


Bitemporal layer

The Graphiquity engine is bitemporal append-only. Every record has:

Writes never overwrite. A "delete" is a soft-delete (sets _valid_to). An "update" creates a new version (the prior version stays readable via AT '<iso>').

This is why Replay works: Site(env, time) reads each Versionable bundled in the Release that was active at time, with each Versionable resolved at its bundled version's timestamp. The result is byte-identical to what was live then.

This is also why Reviews + Deployments are append-only: a rollback is a new Deployment fact pointing at an older Release, not a mutation of an existing fact. "Who shipped what when and what was reviewed" is one Cypher query, not archaeology.

See ADR-0019 for the full deployment fact model rationale and features.md → Replay & Audit for the customer-facing capabilities this enables.


See also