Warning
Triplex is pre-1.0. The new @triplex-build packages are not yet published; use a source checkout
for evaluation. The current tree requires effect@4.0.0-rc.112; Effect 3 is not compatible. KV
and SQLite are the supported baseline, PostgreSQL is a production candidate, and Cloudflare and
FoundationDB are experimental. See Current state for the exact maturity
contract.
An Effect-native fact database for applications that have to explain themselves.
Documentation · GitHub · Security
Triplex stores what is true, what was true, which versioned rules governed each write, and what work the data implies. One store answers "what does this record look like today", "what did we believe last Tuesday", "who changed it and under which policy", and "what tasks should exist right now", without stitching together a database, an audit log, a config service, and a job queue.
Most applications keep state in a database and everything that explains that state somewhere else: policy versions in a config service, audit trails in a log, derived to-dos in a queue. The moment a regulator, customer, or engineer asks why something happened, the answer has to be reconstructed across systems that never shared identity or time.
Triplex keeps those concerns separate in the API but gives them one identity model, one clock, and one provenance chain:
| Primitive | What it provides |
|---|---|
| Triples | Typed facts, atomic assertions/retractions, history, batching, and pluggable storage |
| Datalog | Structural joins, negation, aggregation, recursion, and stable pagination |
| Journal | Ordered commits with actor, command, causation, correlation, and config provenance |
| Config | Typed Merkle graphs, immutable releases, movable refs, impact, and decision proofs |
| Derivations | Content-addressed candidates, source provenance, reconciliation, and temporal wakeups |
| Projections | Entity snapshots and validation/materialization state with explicit freshness |
The layers meet at an application command without collapsing into one model:
flowchart LR
C[Config release] -->|configSnapshot| T[Atomic transaction]
T --> F[Bitemporal facts]
T --> J[Ordered causal journal]
F --> Q[Datalog derivation]
C --> Q
Q --> D[Provenance-carrying candidates]
D --> H[Host-owned workflows and projections]
Good fit: compliance and onboarding systems, policy engines, entitlement and eligibility logic, back-office workflows, and any domain where "as of when" and "under which rules" are first-class questions. It also fits agent-driven systems that need a durable, queryable world model with a causal record of every change.
Not a fit: high-volume telemetry, blob storage, or workloads that only need a relational table and never ask about history or provenance. Triplex is a system of record, not a cache or a queue.
As of September 10, 2026, npm returns 404 for the new @triplex-build package family. Run the
checked examples from a source checkout until the first release gates are complete:
git clone https://github.com/bjacobso/triplex.git
cd triplex
corepack enable
pnpm install --frozen-lockfile
pnpm exec tsx --tsconfig docs/snippets/tsconfig.json docs/snippets/getting-started.tsDo not use the superseded @bjacobso canaries for new work. Triplex is ESM-only, targets Node.js
22+, and is aligned to effect@4.0.0-rc.112. The browser-safe core also runs in modern browsers and
edge runtimes. The website quickstart includes expected output, Effect
layer lifetime, and SQLite persistence. The coordinated package publication process is documented
in Releasing Triplex.
Triples is the primary service. The core package includes an in-memory ordered-KV layer, so the
same program can write facts, read patterns, and run Datalog without another package:
import { Effect } from "effect";
import { EntityId, KvTriples, Triples, ref, string } from "@triplex-build/triplex";
const program = Effect.gen(function* () {
const triples = yield* Triples;
const alice = EntityId.make("person:alice");
const acme = EntityId.make("company:acme");
const tx = yield* triples.transact(
[
{
op: "assert",
entityId: alice,
entityType: "Person",
attribute: ":person/name",
value: string("Alice"),
},
{
op: "assert",
entityId: alice,
entityType: "Person",
attribute: ":person/employer",
value: ref(acme),
},
{
op: "assert",
entityId: acme,
entityType: "Company",
attribute: ":company/name",
value: string("Acme"),
},
],
{
actor: "user:ben",
commandId: "create-person:alice",
correlationId: "request:123",
},
);
const facts = yield* triples.entity(alice);
const { results } = yield* triples.query({
find: ["?personName", "?companyName"],
where: [
["?person", ":person/name", "?personName"],
["?person", ":person/employer", "?company"],
["?company", ":company/name", "?companyName"],
],
});
return { position: tx.position, facts, results };
});
Effect.runPromise(program.pipe(Effect.provide(KvTriples.layer)));Assertions may declare validFrom and validTo. Reads accept one
{ recordedAt?, validAt? } basis, which applies coherently to every clause in a query. Retracting a
fact closes its recorded visibility without deleting its assertion history.
Direct writes, entity/pattern reads, reference values, and transactional retractions require
EntityId or TripleId values. Construct or decode them once at the application boundary instead
of scattering unchecked string casts through domain code. Raw Datalog keeps string literals because
one term can represent an entity, attribute, transaction, rule, or scalar value; its runtime schema
validates identity positions before execution.
Run
pnpm exec tsx --tsconfig docs/snippets/tsconfig.json examples/demo/demo.ts
for the smallest executable example from a fresh source checkout.
The quick start shows the store. This walkthrough shows the shape of a real host: a small HR system that places workers on sites and must make sure each worker has current safety training for every site they are placed on. The same six steps apply to most Triplex integrations.
import { Effect, Layer } from "effect";
import { EntityId, KvTriples, Triples } from "@triplex-build/triplex";
import { Attribute, ConfigStore, EntityType, GraphConstraint } from "@triplex-build/triplex/config";
import * as Derivation from "@triplex-build/triplex/derivation";
import { ConsumerCheckpoint } from "@triplex-build/triplex/operational";
import { Runtime, DatabaseScope, Capabilities } from "@triplex-build/triplex/runtime";
// 1. Describe the domain once. Attributes own identity and type; entity types own usage rules.
const WorkerName = Attribute.text(":worker/name");
const Worker = EntityType.make("Worker", {
attributes: { name: Attribute.use(WorkerName, { required: true }) },
});
const PlacementWorker = Attribute.ref(":placement/worker", Worker);
const PlacementSite = Attribute.text(":placement/site");
const Placement = EntityType.make("Placement", {
attributes: {
worker: Attribute.use(PlacementWorker, { required: true }),
site: Attribute.use(PlacementSite, { required: true }),
},
});
const SafetyTraining = Attribute.text(":evidence/site-safety");
const workerId = EntityId.make("worker:maria");
const placementId = EntityId.make("placement:1");
const program = Effect.gen(function* () {
const triples = yield* Triples;
const config = yield* ConfigStore.ConfigStore;
// 2. Publish the schema as an immutable, content-addressed release.
const release = yield* config.commit({
label: "hr-2026.1",
objects: [...(yield* Worker.nodes), ...(yield* Placement.nodes), yield* SafetyTraining.node],
ref: "live",
});
const enforce = GraphConstraint.enforcement([...Worker.constraints, ...Placement.constraints]);
// 3. Handle an application command as one atomic, attributed transaction.
yield* triples.transact(
[
{
op: "assert",
entityId: workerId,
entityType: "Worker",
...Worker.attributes.name.assertion("Maria"),
},
{
op: "assert",
entityId: placementId,
entityType: "Placement",
...Placement.attributes.worker.assertion(workerId),
},
{
op: "assert",
entityId: placementId,
entityType: "Placement",
...Placement.attributes.site.assertion("site:harbor"),
},
],
{
actor: "user:ben",
commandId: "placement:create:1", // retried commands return the same receipt
correlationId: "request:8f2c",
configSnapshot: release.snapshot.id, // the rules this write was governed by
enforce, // required/unique/ref-target checks in the same transaction
},
);
// 4. Declare the work the data implies, as a content-addressed Datalog derivation.
const training = yield* Derivation.make({
name: "task.site-safety-training",
query: {
find: ["?worker", "?site"],
where: [
["?placement", PlacementWorker.key, "?worker"],
["?placement", PlacementSite.key, "?site"],
["not", ["?worker", SafetyTraining.key, "?site"]],
],
},
identity: ["?worker", "?site"],
configSnapshot: release.snapshot.id,
});
// 5. A background consumer follows the journal and reconciles the derivation.
const consumer = "hr/training-materializer";
const checkpoint = yield* ConsumerCheckpoint.get(triples, consumer);
const from = checkpoint?.position ?? 0;
const page = yield* triples.transactions({ after: from, limit: 100 });
if (page.next !== undefined) {
const run = yield* Derivation.Materialization.materialize(triples, training, {
basis: { validAt: Date.now() },
});
for (const candidate of run.reconciliation.added) {
// Replace with your own task/notification system; identity and revision are stable.
yield* Effect.log(`open training task ${candidate.id} (revision ${candidate.revision})`);
}
// run.nextTemporalBoundary tells your scheduler when evidence will expire
yield* ConsumerCheckpoint.advance(triples, {
consumer,
expectedPosition: from,
nextPosition: page.next,
meta: { actor: consumer },
});
}
// 6. Answer audit questions later, from the same store.
const receipt = yield* triples.transactionByCommand("placement:create:1");
const yesterday = Date.now() - 86_400_000;
const asOfYesterday = yield* triples.entity(EntityId.make("placement:1"), {
recordedAt: yesterday,
});
return { release: release.snapshot.id, receipt, asOfYesterday };
});
const AppLayer = ConfigStore.layer.pipe(Layer.provideMerge(KvTriples.layer));
// Production: ConfigStore.layer.pipe(Layer.provideMerge(SqliteTriples.layer({ filename: "app.db" })))
await Effect.runPromise(program.pipe(Effect.provide(AppLayer)));The division of labour is deliberate:
| Triplex owns | Your application owns |
|---|---|
| Facts, history, and the bitemporal read basis | Domain vocabulary and entity identifiers |
| Atomic transactions, command receipts, and the ordered journal | HTTP/queue handlers that turn requests into commands |
| Config identity, releases, refs, and constraint enforcement | What a release contains and when live moves |
| Derivation candidates, provenance, and reconciliation diffs | Task, notification, or workflow lifecycle for each diff |
| Consumer checkpoints and temporal wakeup boundaries | Timer delivery, retries, and worker scheduling |
Swapping storage changes only the provided layer. The compliance host demo runs the complete version of this scenario, including hypothetical previews, evidence expiry, and reopening work without a new source transaction.
Configuration is a tree-shakeable part of the primary package:
import { Attribute, EntityType } from "@triplex-build/triplex/config";
export const EmployerName = Attribute.text(":employer/name");
export const Employer = EntityType.make("Employer", {
attributes: {
name: Attribute.use(EmployerName, {
required: true,
unique: true,
}),
},
});
Employer.attributes.name.key; // ":employer/name"
Employer.attributes.name.assertion("Acme", { validFrom: Date.now() });The attribute owns its global identity and value type. The entity type owns how it uses that attribute—requiredness, cardinality, uniqueness, and reference-target constraints. Those nodes can be committed with forms, policies, routines, and other application-defined config into one content-addressed release.
ConfigStore.layer persists immutable config objects, revisions, release snapshots, and refs
through the same atomic Triples boundary. ConfigRuntime evaluates against a pinned release;
EntityValidation records queryable observations; GraphConstraint can opt a command into atomic
required/cardinality/uniqueness/reference-target enforcement.
Read the configuration guide for releases, refs, validation, enforcement, and the exact tamper-evidence guarantee.
Raw Datalog is the query API. Patterns join by shared variables, and the portable contract includes predicates, negation, disjunction, aggregation, ordering, snapshot-stable keyset pagination, and a bounded binary recursive-rule form. Queries are schema-decoded and semantically validated before either the KV or SQL engine runs.
@triplex-build/triplex/derivation pins a structural query, result identity, type, configuration
snapshot, and dependency set into a content-addressed definition. Evaluation returns candidates
with source triple and assertion-transaction provenance. Pure reconciliation reports added,
removed, changed, and unchanged identities; the application decides whether that means opening a
task, cancelling work, or doing nothing.
Materialized runs retain a source position and return current, stale, or unmaterialized.
Future validFrom and validTo edges produce a nextTemporalBoundary, allowing a host scheduler
to reopen expiry-driven work without polling the whole database. Read-only overlays preview
hypothetical facts without mutating the source store or journal.
- Datalog guide
- Derivations guide
- Compliance host demo — config, facts, feed catch-up, reconciliation, hypothetical planning, and expiry-driven reopening in one standalone scenario
Run the standalone Foldkit dashboard to inspect a real browser-local Triplex database:
pnpm dashboardOpen http://localhost:4173. The standalone layer publishes a typed classroom configuration,
records students, teachers, a course, a quiz form, submissions, and a materialized grading
candidate. The dashboard itself is domain-independent: it discovers entity types, attributes,
references, configured derivations, journal entries, and configuration objects from the supplied
Triplex services. The entity browser lists discovered types and opens each as a reflected,
cursor-paginated table pinned to one temporal basis. The form preview renders nodes that explicitly
opt into the portable triplex.form/v1 contract. The configuration inspector exposes every logical
object and immutable revision, canonical stored bodies, dependency closures, release ancestry, and
moving refs. Swap the demo layer for a host database without changing the inspector.
The package lives at packages/dashboard. It uses one app-lifetime Effect
layer as a Foldkit resource; named commands resolve Triples and ConfigStore from that layer,
while @foldkit/ui supplies accessible controls and Tailwind supplies styling.
@triplex-build/triplex-http compiles persisted entity configuration into validated REST handlers and
OpenAPI 3.1 without retaining the TypeScript DSL declarations. Hosts provide Triples,
ConfigStore, an authorization layer, and their chosen HTTP server; KV and SQLite share the same
Fetch-level integration suite. Wire objects use full global keywords, writes are atomic and pinned
to one immutable config snapshot, and historical snapshot URLs are read-only by default.
See the HTTP API guide and standalone host.
@triplex-build/triplex-cli exposes the same database and configuration services through Effect v4's
typed CLI modules. It is JSON-first, non-interactive, schema-validates external input, and supports
SQLite, PostgreSQL, and database-scoped PostgreSQL:
pnpm turbo run build --filter=@triplex-build/triplex-cli...
triplex() { node --disable-warning=ExperimentalWarning packages/cli/dist/cli.js "$@"; }
triplex --sqlite ./app.db describe
triplex --sqlite ./app.db entity types
triplex --sqlite ./app.db --pretty config object form quiz/bitemporal-facts
triplex --sqlite ./app.db query run --input query.jsonAgents can inspect entity types and bitemporal facts, page entity audit history, run or explain raw Datalog, inspect config objects/revisions/releases/refs and deploy impact, apply attributed atomic transactions, move refs, and resolve command receipts. Write commands require an actor and unique command ID. See the CLI contract and examples.
Every successful application transaction receives a monotonically increasing commit position and
a queryable _Transaction envelope. It can retain:
- actor and atomically unique command ID;
- correlation and causation IDs;
- the governing config snapshot; and
- complete typed assertion and retraction changes, including valid intervals and source transactions.
triples.transactions({ after, limit }) is the durable at-least-once catch-up feed.
ConsumerCheckpoint persists a compare-and-retract-protected cursor. Best-effort change emitters
are wakeup hints, never a substitute for journal catch-up.
For one entity's audit timeline, use the indexed journal view rather than filtering the global feed:
const timeline = Effect.gen(function* () {
const first = yield* triples.transactionsForEntity(workerId, { limit: 50 });
const next =
first.nextBeforePosition === undefined
? undefined
: yield* triples.transactionsForEntity(workerId, {
limit: 50,
snapshotPosition: first.snapshotPosition,
beforePosition: first.nextBeforePosition,
});
return { first, next };
});Each page contains complete TransactionRecord values, newest first. The first page's commit
position pins later pages while concurrent commands continue to commit.
queryPage cursors are versioned, schema-decoded, and bound to the canonical query, ordering,
temporal basis, and database scope. The first page pins an exact commit-position snapshot, so later
writes cannot move rows between pages.
Read Operational primitives for the transaction, concurrency, pagination, projection, and host/runtime boundaries.
“Supported” describes the current behavioral test boundary, not merely whether a package compiles.
| Package | Surface | Status |
|---|---|---|
@triplex-build/triplex |
KvTriples.layer |
Supported in-memory baseline |
@triplex-build/triplex-sqlite |
SqliteTriples.layer({ filename }) |
Supported durable baseline |
@triplex-build/triplex-postgres |
PgTriples.layer(config) |
Candidate; shared integration/conformance is currently opt-in |
@triplex-build/triplex-cloudflare |
CloudflareTriples.layer(...) |
Experimental |
@triplex-build/triplex-foundationdb |
FdbTriples.layer(config) |
Experimental |
@triplex-build/triplex-sql contains shared migrations and SQL query execution rather than a database.
@triplex-build/triplex-testkit contains the behavioral conformance corpus used by adapters.
@triplex-build/triplex-host is a private experimental package for portable tenant identity,
authorization, lifecycle, and protocol contracts. The deployable reference wiring is in
examples/tenant-host-cloudflare.
Switching the supported quick start to SQLite changes only the provided layer:
import { SqliteTriples } from "@triplex-build/triplex-sqlite";
const SqliteLive = SqliteTriples.layer({ filename: "app.db" });The standalone layer owns its pool and applies Triplex migrations as a convenience:
import { PgTriples } from "@triplex-build/triplex-postgres";
const TriplexLive = PgTriples.layerFromUrl(process.env.DATABASE_URL!);Production hosts that already own an Effect SQL client can provide that exact client to Triplex. This path creates no pool and runs no migration:
import { Effect, Layer } from "effect";
import { SqlClient } from "effect/unstable/sql";
import { EntityId, Triples, string } from "@triplex-build/triplex";
import { PgTriples, makePostgresqlLayerUnmigratedFromUrl } from "@triplex-build/triplex-postgres";
const HostSql = makePostgresqlLayerUnmigratedFromUrl(process.env.DATABASE_URL!);
const AppDatabase = PgTriples.layerFromSqlClient({ scope: "app/main" }).pipe(
Layer.provideMerge(HostSql),
);
const command = Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient;
const triples = yield* Triples;
yield* sql.withTransaction(
Effect.gen(function* () {
yield* sql`INSERT INTO host_records (id) VALUES ('record:1')`;
yield* triples.transact(
[
{
op: "assert",
entityId: EntityId.make("record:1"),
attribute: ":record/status",
value: string("accepted"),
},
],
{ commandId: "command:1", actor: "user:1" },
);
yield* sql`INSERT INTO host_outbox (id) VALUES ('delivery:1')`;
}),
);
}).pipe(Effect.provide(AppDatabase));Effect SQL's fiber-local transaction connection is shared by the host statements and nested
Triples.transact, so facts, journal, command claim, commit position, host rows, and outbox commit
or roll back together. Nested transactions use Effect SQL savepoints.
For a server-owned logical database mapping, validate the stored ID and create a schema-bound
runtime with PgTriples.layerForDatabase(config, DatabaseId.make("customer-a")). Every connection
in that pool starts in the deterministic Triplex schema, and its scope is embedded in pagination
cursors. The schema and v1 migration must already exist; layerForDatabaseMigrated is the explicit
provisioning convenience for setup tools and tests.
Triplex exports the ordered migrations and runMigrations from @triplex-build/triplex-sql.
Production hosts should run them from their own deployment process against the same database or
schema before constructing an unmigrated runtime. Triplex uses triplex_schema_migrations, not the
host application's migration table.
import { Triples, KvTriples } from "@triplex-build/triplex";
import { DatalogQuery } from "@triplex-build/triplex/datalog";
import { SubscriptionManager } from "@triplex-build/triplex/subscriptions";
import { ConfigStore, TypeExpr } from "@triplex-build/triplex/config";
import { ContentId } from "@triplex-build/triplex/content";
import * as Derivation from "@triplex-build/triplex/derivation";
import { ConsumerCheckpoint } from "@triplex-build/triplex/operational";Application code should use these public surfaces. Adapter authors use the runtime subpath to
compose storage and query layers with a validated database scope and explicit capability providers.
@triplex-build/triplex/internal remains unstable implementation support for legacy adapter code.
Public exports resolve only to built dist files.
| Document | Purpose |
|---|---|
| Getting started | First runnable program, output, and persistence |
| Playground | Local in-browser database with selectable domains |
| Core concepts | Facts, time, configuration, derivation, and ownership |
| Configuration walkthrough | Publish, promote, pin, inspect, and roll back config |
| CLI and dashboard | Operate and explore a source-checkout database |
| Troubleshooting | Common failures and surprising semantics |
| Current state | Delivered behavior, maturity, limitations, releases |
| Datalog | Query syntax and backend-independent semantics |
| Configuration reference | Types, releases, refs, validation, and proofs |
| Derivations | Candidates, provenance, materialization, overlays |
| Operational primitives | Transactions, journal, concurrency, projections |
| Host integration | Runtime composition and data-migration guidance |
| Custom runtimes | Public runtime builders, capabilities, and backend contracts |
| Architecture | Package boundaries and dependency direction |
| Roadmap | Release gates and future work |
| Source provenance | Imported repository history |
The focused configuration explorer remains under examples/config-explorer,
and the full database dashboard lives under packages/dashboard.
pnpm install --frozen-lockfile
pnpm check
pnpm pack:checkPostgreSQL and FoundationDB integration suites are opt-in:
pnpm test:postgres:integration
pnpm test:foundationdb:integrationSee CONTRIBUTING.md for the complete contribution contract.
MIT © 2026 Ben Jacobson.