Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ const sidebar: DefaultTheme.SidebarItem[] = [
{ text: "Releasing", link: "/releasing" },
],
},
{
text: "Backend blueprints",
items: [
{ text: "Overview", link: "/backends" },
{ text: "SQLite · supported", link: "/sqlite" },
{ text: "FoundationDB · experimental", link: "/foundationdb" },
{ text: "Datomic · proposed", link: "/datomic" },
{ text: "XTDB · proposed", link: "/xtdb" },
],
},
{
text: "Project",
items: [
Expand Down Expand Up @@ -107,6 +117,7 @@ export default defineConfig({
{ text: "Configuration", link: "/configuration" },
{ text: "Operational primitives", link: "/operational-primitives" },
{ text: "HTTP API", link: "/http-api" },
{ text: "Backend blueprints", link: "/backends" },
],
},
{ text: "Operate", link: "/tools" },
Expand Down
92 changes: 92 additions & 0 deletions docs/.vitepress/theme/blueprint.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
.backend-blueprint .blueprint-eyebrow {
margin: 0 0 16px;
color: var(--vp-c-brand-1);
font-family: var(--vp-font-family-mono);
font-size: 12px;
letter-spacing: 0.04em;
text-transform: uppercase;
}

.backend-blueprint .blueprint-lead {
margin: 24px 0;
color: var(--vp-c-text-2);
font-size: 22px;
line-height: 1.5;
}

.backend-blueprint .blueprint-status {
display: inline-block;
margin: 0 0 16px;
padding: 5px 10px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg-alt);
color: var(--vp-c-text-1);
font-family: var(--vp-font-family-mono);
font-size: 12px;
line-height: 1.6;
}

.backend-blueprint .blueprint-actions {
display: flex;
flex-wrap: wrap;
gap: 12px 24px;
margin: 24px 0 32px;
}

.backend-blueprint .blueprint-cards {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
margin: 32px 0;
}

.backend-blueprint .blueprint-cards a {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
padding: 24px;
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-elv);
color: var(--vp-c-text-2);
font-weight: 400;
text-decoration: none;
transition: border-color 0.15s;
}

.backend-blueprint .blueprint-cards a:hover {
border-color: var(--vp-c-brand-1);
}

.backend-blueprint .blueprint-cards a:focus-visible {
outline: 2px solid var(--vp-c-brand-1);
outline-offset: 4px;
}

.backend-blueprint .blueprint-cards strong {
color: var(--vp-c-text-1);
font-size: 24px;
}

.backend-blueprint .blueprint-cards .blueprint-status {
margin: 0;
}

.backend-blueprint .blueprint-card-link {
margin-top: auto;
padding-top: 8px;
color: var(--vp-c-brand-1);
font-weight: 600;
}

@media (max-width: 640px) {
.backend-blueprint .blueprint-lead {
font-size: 20px;
}

.backend-blueprint .blueprint-cards {
grid-template-columns: 1fr;
}
}
1 change: 1 addition & 0 deletions docs/.vitepress/theme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { defineComponent, h } from "vue";

import "./custom.css";
import "./home.css";
import "./blueprint.css";
import Playground from "./Playground.vue";
import TriplexExplorer from "./TriplexExplorer.vue";
import DecisionTrace from "./DecisionTrace.vue";
Expand Down
93 changes: 93 additions & 0 deletions docs/backends.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
---
title: Backend blueprints
description: Explore how SQLite, FoundationDB, Datomic, and XTDB primitives can support a Triplex storage layer, with implementation paths and explicit maturity labels.
pageClass: backend-blueprint
---

<p class="blueprint-eyebrow">Storage / Backend blueprints</p>

# Your database. Triplex's memory.

<p class="blueprint-lead">Build on the storage primitives you already trust. Keep facts, time, versioned rules, and derived work behind one TypeScript API.</p>

Triplex separates its application model from storage and query execution. These blueprints show
how a backend can provide that foundation: the data representation, the transaction boundary,
the query path, and the checks an implementation needs to pass.

<div class="blueprint-cards">
<a href="/sqlite">
<span class="blueprint-status">Supported</span>
<span><strong>SQLite</strong></span>
<span>A working reference for durable, embedded storage. Follow the rows, indexes, and SQL transaction.</span>
<span class="blueprint-card-link">Explore SQLite →</span>
</a>
<a href="/foundationdb">
<span class="blueprint-status">Experimental</span>
<span><strong>FoundationDB</strong></span>
<span>Ordered keys and distributed transactions underneath Triplex's existing hexastore engine.</span>
<span class="blueprint-card-link">Explore FoundationDB →</span>
</a>
<a href="/datomic">
<span class="blueprint-status">Proposed blueprint</span>
<span><strong>Datomic</strong></span>
<span>Assertion entities, immutable database values, and a design for bridging the transaction models.</span>
<span class="blueprint-card-link">Explore Datomic →</span>
</a>
<a href="/xtdb">
<span class="blueprint-status">Proposed blueprint</span>
<span><strong>XTDB</strong></span>
<span>A temporal SQL backend, with explicit fact identity and an atomic command submission design.</span>
<span class="blueprint-card-link">Explore XTDB →</span>
</a>
</div>

**Supported** means the backend is in Triplex's baseline behavioral test boundary.
**Experimental** means an implementation exists but has outstanding operational gates.
**Proposed blueprint** means a design to investigate; no adapter or installable integration exists.
The [current state](/current-state#backend-maturity) remains the maturity contract for implemented
backends, including PostgreSQL and Cloudflare.

## One application model, two implementation paths

| Your backend supplies | Composition path | You implement | Triplex supplies |
| ---------------------------------------------------------------------- | ---------------- | ------------------------------------ | -------------------------------------------------------- |
| Ordered byte keys, bounded range scans, atomic read/write transactions | `Runtime.fromKv` | `KvBackend` | Hexastore storage and Datalog execution |
| Fact-oriented or relational storage with its own execution model | `Runtime.define` | `StorageAdapter` and `QueryExecutor` | The public `Triples` service and higher-level primitives |

Both paths must preserve atomic facts and journal writes, ordered commit positions, unique command
receipts, bitemporal reads, and snapshot-stable query pages. Sharing a query language or a temporal
vocabulary alone does not establish those guarantees.

Configuration releases, decision proofs, and derivations use Triplex's existing services on top of
that contract. Your application still owns authorization, workflow effects, timer delivery, and
the meaning of reconciled work. [Host integration](/host-integration) describes that boundary.

## The same correction should mean the same thing

Use the [homepage's checked scenario](/#one-scenario) as the first design exercise. Maria's training
was recorded as valid from March 1 until June 1. An auditor later corrects its start date to March 5.

Every backend must retain the original assertion, record its retraction, and create a new assertion
in one command. A March 3 query using the earlier recorded basis must still show training. The
same query using current knowledge must show the gap. The placement's receipt must still point
to the configuration release that governed it, and an already-open query cursor must retain its
original results even after the correction.

## Build a backend

1. Choose an isolated physical database or keyspace and bind a `DatabaseScope` to it. Scope is
cursor identity; physical partitioning and authorization are separate host responsibilities.
2. Implement the base storage and query contract with `Capabilities.none`. Establish rollback,
duplicate-command behavior, typed values, history, and pinned pages before adding projections.
3. Run `runtimeConformance` from `@triplex-build/triplex-testkit` against a fresh isolated database.
Add backend-specific concurrency, interruption, crash, and migration exercises.
4. Add durable snapshot or change-emission providers explicitly if your deployment needs them.
Measure their failure behavior separately from source transaction correctness.

The [custom runtime guide](/custom-runtimes) contains checked composition examples and the full
adapter contract. Passing the shared corpus establishes behavioral compatibility within its
coverage; production durability and operational readiness need additional evidence.

Have a backend in mind? [Open a design issue](https://github.com/bjacobso/triplex/issues/new) with
its transaction model, proposed fact representation, and a plan for preserving this correction.
Start with a small conformance-backed implementation, then extend it.
4 changes: 4 additions & 0 deletions docs/custom-runtimes.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ For application-level database composition, see [Host integration](/host-integra
describes implementing a storage or execution backend. Workflows, authorization, routing, timers,
and external effects remain host responsibilities.

For concrete storage mappings and implementation plans, explore the [backend blueprints](/backends):
[SQLite](/sqlite) is a working reference, [FoundationDB](/foundationdb) is experimental, and
[Datomic](/datomic) and [XTDB](/xtdb) are proposed designs with no adapters yet.

## Runtime definitions

A definition supplies Effect layers. Binding a database identity produces a layer providing the
Expand Down
121 changes: 121 additions & 0 deletions docs/datomic.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
---
title: Building Triplex on Datomic
description: A proposed Triplex adapter blueprint using Datomic assertion entities, immutable database values, and atomic compare-and-swap submission.
pageClass: backend-blueprint
---

<p class="blueprint-eyebrow">Backend blueprint / Datomic</p>

# Building Triplex on Datomic

<p class="blueprint-lead">A fact-oriented foundation for a TypeScript application that needs to remember which rules governed every decision.</p>

<p class="blueprint-status">Proposed blueprint · No adapter exists</p>

This page proposes an implementation to investigate. There is no Datomic package, running
integration, or compatibility claim. The mappings below are design choices to validate against
Triplex's public adapter contract.

<nav class="blueprint-actions" aria-label="Datomic blueprint actions">
<a href="#storage-mapping">Explore the mapping →</a>
<a href="#build-the-first-slice">Build the first slice →</a>
<a href="/backends">All blueprints →</a>
</nav>

## Why Datomic

Datomic offers datoms, Datalog, transaction entities, immutable database values, historical views,
and ordered log access. Those primitives are useful ingredients for a Triplex adapter.
[Datomic's best practices](https://docs.datomic.com/reference/best.html) describe their use for
consistent reads and audit queries.

On top of an adapter, Triplex would supply its TypeScript domain model, immutable configuration
releases, pinned command provenance, and derivation reconciliation. Datomic would own persistence
and native query execution. A TypeScript-to-JVM client boundary would be part of the deployment;
choose and document that bridge before promising an embedded integration.

## Storage mapping

The proposed starting point is **one Datomic entity per Triplex assertion**, with a fixed adapter
schema. A direct mapping from a domain triple to one native datom cannot by itself preserve
independent `TripleId`s and valid-time intervals for repeated assertions of the same value.

| Triplex contract | Proposed representation |
| ---------------------------- | ---------------------------------------------------------------------------------------------------- |
| Stable entity and triple IDs | Original strings stored as adapter identity attributes; native entity IDs stay internal |
| Fact value | Assertion entity with domain entity ID, attribute keyword, entity type, value tag, and typed payload |
| Assertion history | Immutable assertion identity and payload; explicit assertion transaction and recorded position |
| Retraction | Retraction metadata on the assertion entity, retaining the original payload and valid interval |
| Valid time | Explicit `validFrom` / `validTo` fields filtered on every relevant read |
| Command receipt | A scoped command identity claimed atomically without overwriting an earlier receipt |
| Commit order | A per-logical-database position counter advanced in the same transaction as facts and journal |
| Causal journal | Triplex transaction envelopes and typed changes stored atomically with the command |
| Query execution | Translate validated Triplex Datalog to joins over assertion entities and shared temporal filters |

Keep Triplex's recorded timestamp and commit position explicit. Datomic's native transaction
coordinate can help identify an exact database value, but it is not a drop-in implementation of
`nextCommitPosition()`, which Triplex calls before inserting the transaction's rows.

## Bridge the transaction models

Triplex's `StorageAdapter.withTransaction` accepts an Effect callback whose reads and writes must
share an atomic view, including pending changes. Datomic accepts declarative transaction data;
its transaction functions inspect `db-before` and return additional data, without seeing one
another's output. An arbitrary TypeScript callback cannot simply become a native transaction
function. [Datomic transaction functions](https://docs.datomic.com/transactions/transaction-functions.html)
define that boundary.

One candidate design is an optimistic staging adapter:

1. Acquire one immutable database value, read the logical database's position and existing receipt,
and run the callback against that value plus an in-memory overlay of pending mutations.
2. Stage constraint checks, a new receipt, assertion/retraction metadata, the next position, and
the complete journal. Reads during the callback must see the overlay consistently.
3. Submit the staged changes with a compare-and-swap from the observed position to the next one.
Every writer of that logical database's source data must participate in this guard.
4. On a conflict, discard the staged result, acquire a fresh database value, and re-evaluate.
For a duplicate, `claimCommand` must return the original transaction ID so core raises
`CommandAlreadyCommittedError`; callers can load its receipt with `transactionByCommand`.
Publish a successful callback result only after confirmed commit.

This is a proposal, not an implemented algorithm. Test retries, typed failures, defects,
interruption, and uncertain submission outcomes before calling it conformant. Constraint queries
must read the same protected basis; direct writes that bypass the guard would invalidate the
design. A native transaction function could instead perform compatible checks, but translating
Triplex's enforcement logic would be additional work.

## Preserve both clocks and the query contract

For Maria's correction, stage a retraction of the March 1 assertion and a new March 5 assertion,
with distinct triple IDs, in the same command. Retain the old payload. The earlier recorded basis
must still expose the original training interval, while current knowledge exposes the gap on
March 3. Datomic's historical view alone does not implement the explicit valid-time filtering in
this proposed assertion schema.

Start with patterns, joins, and temporal filters, then implement the complete validated Triplex
query surface. Verify typed scalar comparisons, negation, aggregates, the constrained recursive
form, deterministic ordering, pagination, and `explain`. Identical-looking Datalog clauses do not
prove equivalent execution.

Pinned pages need the original query, temporal basis, database scope, and exact commit boundary.
Use retained assertion/retraction positions or a verified mapping to native database values;
later corrections must not change which rows an earlier cursor sees. Keep the Triplex journal's
typed changes and provenance even if native log access is used to locate commits.

## Build the first slice

1. Specify the JVM bridge, assertion schema, typed codecs, and physical database isolation.
2. Prototype the staged transaction boundary first: one correction, two competing writers, one
duplicate command, and one rejected constraint. Establish that failures leave no partial state.
3. Implement `StorageAdapter` and `QueryExecutor`, then compose them with `Runtime.define` and
`Capabilities.none`. See the [checked runtime skeleton](/custom-runtimes#runtime-definitions).
4. Run [runtime conformance](/custom-runtimes#correctness-contract-and-conformance) on a fresh
database. Extend it with bridge disconnects, lost commit acknowledgements, process restarts,
and schema upgrades.

Configuration and derivation services can then use the same `Triples` layer. Durable entity
snapshots and change emitters are separate capabilities to implement and verify.

[Open a Datomic design issue](https://github.com/bjacobso/triplex/issues/new) with a transaction
prototype and its test results, or compare the [working SQLite reference](/sqlite) and
[XTDB proposal](/xtdb).
Loading
Loading