Getting started
Start with the Database client from @triplex-build/triplex/database. This guide declares entity types, publishes a release, opens the client, and runs one command, a typed read, a Schema-projected query and a historical read.
Pre-1.0 package
The @triplex-build/triplex package is available from npm and requires effect@4.1.1. Effect 3 is incompatible. Do not use the superseded @bjacobso package names for new work.
Prerequisites
- Git
- Node.js 22.19.0 or newer
- Corepack and pnpm 10.11.0 (the repository declares the exact package-manager version)
For an application, install the single published package and its Effect peer dependency:
npm install @triplex-build/triplex effectRun the schema-bound client example
Clone the repository and install its locked dependencies:
git clone https://github.com/bjacobso/triplex.git
cd triplex
corepack enable
pnpm install --frozen-lockfileRun the checked quickstart directly from the checkout:
pnpm exec tsx --tsconfig docs/snippets/tsconfig.json docs/snippets/client-getting-started.tsThe complete program is:
import { Effect, Layer, Schema } from "effect";
import { EntityId, KvTriples } from "@triplex-build/triplex";
import { Attribute, ConfigStore, EntityType } from "@triplex-build/triplex/config";
import * as Database from "@triplex-build/triplex/database";
const Company = EntityType.make("Company", {
name: Attribute.text(":company/name").required(),
});
const Person = EntityType.make("Person", {
name: Attribute.text(":person/name").required(),
employer: Attribute.ref(":person/employer", Company).required(),
});
const alice = EntityId.make("person:alice");
const acme = EntityId.make("company:acme");
const Store = ConfigStore.layer.pipe(Layer.provideMerge(KvTriples.layer));
const Client = Database.layer({ schema: { types: [Person, Company] } });
const publish = Effect.gen(function* () {
const config = yield* ConfigStore.ConfigStore;
yield* config.commit({
label: "people-v1",
objects: yield* EntityType.nodesOf(Person, Company),
ref: "live",
});
});
const program = Effect.gen(function* () {
const db = yield* Database.Database;
const receipt = yield* db.command(
{ actor: "user:pat", commandId: "quickstart/create-alice/v1" },
(tx) =>
Effect.gen(function* () {
yield* tx.create(Company, acme, { name: "Acme" }, { validFrom: 0 });
yield* tx.create(Person, alice, { name: "Alice", employer: acme }, { validFrom: 0 });
}),
);
const person = yield* db.get(Person, alice);
const relationships = yield* db.query(
{
find: ["?person", "?company"],
where: [
...Person.where("?id", { name: "?person", employer: "?employer" }),
...Company.where("?employer", { name: "?company" }),
],
},
{ decode: Schema.Struct({ "?person": Schema.String, "?company": Schema.String }) },
);
const then = yield* db.asOf({ recordedPosition: receipt.position, validAt: 0 });
return {
person: person?.values,
relationships: relationships.results,
knownThen: (yield* then.get(Person, alice))?.values,
};
});
// Publish before acquiring the client, and share the same Store for both effects.
export const result = await Effect.runPromise(
publish.pipe(Effect.andThen(program.pipe(Effect.provide(Client))), Effect.provide(Store)),
);
console.log(JSON.stringify(result, null, 2));It prints:
{
"person": { "name": "Alice", "employer": "company:acme" },
"relationships": [{ "?person": "Alice", "?company": "Acme" }],
"knownThen": { "name": "Alice", "employer": "company:acme" }
}What the Effect code is doing
EntityTypedeclares aliases, attribute types, required fields and reference targets.ConfigStore.commitpublishes the model as an immutable release and movesliveto it.Database.layerchecks the published model before providing theDatabase.Databaseservice.db.commandrequires actor and command ID, pins and enforces the release, and commits the builder's Effects atomically. Retrying the same ID returns the persisted receipt.db.getusesType.decodeto return alias-keyed typed values.db.querychecks attributes against the release and decodes each binding with the supplied Effect Schema.db.asOfcaptures a stable read-only view. The receipt's exact position selects what was known at that command;validAtselects business time.
Use the Why service at the same basis to explain actors, commands, and pinned releases. The homepage example demonstrates a correction followed by a historical read and its explanation.
Effect.gen sequences Effects with generator syntax. Effect.provide supplies services and Effect.runPromise runs the fully provided program. Publishing precedes client acquisition and both share one Store. Real hosts generally publish during deployment.
Create and share layers at the application boundary. A new KvTriples.layer runtime owns a fresh in-memory database, whose contents disappear when that runtime/process ends. The query returns one bounded page (100 rows by default); follow nextCursor for more. Client command callbacks should contain database work, with external delivery handled by the host.
Raw Triples quick start
Use the primitive service when building host infrastructure or exploring facts without a model. This second program writes raw facts and runs a Datalog join:
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");
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: "quickstart",
commandId: "quickstart/create-alice/v1",
},
);
const { results } = yield* triples.query({
find: ["?personName", "?companyName"],
where: [
["?person", ":person/name", "?personName"],
["?person", ":person/employer", "?company"],
["?company", ":company/name", "?companyName"],
],
});
return results.map((row) => ({
person: row["?personName"],
company: row["?companyName"],
}));
});
const relationships = await Effect.runPromise(program.pipe(Effect.provide(KvTriples.layer)));
console.log(JSON.stringify({ relationships }, null, 2));Run it with pnpm exec tsx --tsconfig docs/snippets/tsconfig.json docs/snippets/getting-started.ts. It prints a relationships array containing { "person": "Alice", "company": "Acme" }. Raw Triples does not automatically pin/enforce a release, check attribute spelling, decode an entity, or recover retry receipts. Prefer the client for application commands and reads; see Underneath the client for the explicit primitive contracts.
Use durable SQLite
SQLite is the supported local persistent backend. Install it alongside core and Effect:
npm install @triplex-build/triplex effect @effect/sql-sqlite-nodeIn the first program, import SqliteTriples from @triplex-build/triplex/sqlite and replace KvTriples.layer inside Store with SqliteTriples.layer({ filename: "./triplex.db" }). Keep ConfigStore.layer over that same store and provide the client after publishing. On a persistent database, issue a new command ID only for a new domain command; rerunning the example with its original ID returns the original receipt.
The convenience layer opens the file and applies Triplex's current migration. Construct it once for the application lifetime. Hosts that separate schema migration from runtime startup should use the explicit composition in Host integration.
Use Triplex from a source checkout
To try unreleased changes in an outside project, build and pack the single public package from main. Its exports point to generated files, so build before packing:
cd triplex
pnpm install --frozen-lockfile
pnpm turbo run build --filter=@triplex-build/triplex
pnpm --dir packages/triplex pack --pack-destination /tmpInstall the resulting tarball in your project alongside Effect (and the SQLite driver if needed):
cd ../my-project
npm install /tmp/triplex-build-triplex-*.tgz effect @effect/sql-sqlite-nodeRebuild and pack again after changing the checkout. One tarball contains all public subpaths; there are no separate backend packages to link or install.
Don't use docs/snippets/tsconfig.json for an outside project. It maps package names to source so repository examples can type-check unbuilt changes, bypassing the public exports your application will use.
Next steps
- Read Database client for commands, typed reads, projections and historical views.
- Experiment without installing anything in the browser Playground.
- Read Core concepts before modeling a domain with historical facts.
- Follow Configuration releases and rollback to version rules and pin operational writes to them.
- Learn query clauses and pagination in Datalog.
- Explore a database through the CLI and dashboard.
- Check the current maturity contract before choosing a production backend.