Troubleshooting and FAQ
For machine-readable error codes and recovery hints, see Error diagnostics.
Why does npm return 404 for a Triplex package?
Install @triplex-build/triplex; backend imports are subpaths such as /sqlite and /postgres, not separate npm packages. Cloudflare and FoundationDB ship as experimental, frozen subpaths. Check the full package name and clear a stale local npm cache if a newly published version returns 404. Do not switch new examples back to the superseded @bjacobso scope.
Use a compatible Effect peer version for the installed Triplex release. The current source tree targets Node.js 22+ and effect@4.1.1; Effect 3 is incompatible. Multiple Effect copies or a version mismatch can make service tags and runtime types disagree even when imports look correct.
Why is Triples (or another service) missing?
An Effect program describes requirements until a layer provides them. Supply a backend layer at the application boundary:
await Effect.runPromise(program.pipe(Effect.provide(KvTriples.layer)));For client startup, publish your release before acquiring Database.layer; see the complete getting-started composition. ConfigStore.layer itself requires Triples, so compose it over the same database:
const AppLayer = ConfigStore.layer.pipe(Layer.provideMerge(KvTriples.layer));Construct that layer once per intended database lifetime. Reconstructing an in-memory layer creates a new empty database; constructing per-request SQLite/PostgreSQL layers also defeats connection and runtime sharing.
Why is a query empty?
Check these in order:
- Identity and value type: direct entity/reference APIs require branded
EntityId/TripleIdvalues; raw Datalog terms are decoded by the query schema. - Temporal basis: omitted
validAtmeans now. A future-effective or expired fact will be absent even if it exists in history. - Retraction:
entityandmatchshow visible facts;history(entityId)also shows retracted assertions. - Join variables: a spelling difference such as
?siteversus?siteIdprevents clauses from joining. - Negation safety: variables in
notmust already be bound outside it; invalid programs fail preflight instead of producing a partial answer. - Attribute spelling: the Database client checks constant attributes against its release automatically. For raw
Triples, installSchemaBound.layer({ ref: "live" })from@triplex-build/triplex/configto check constant attribute keys against a committed release. A misspelled key such as":vendor/staus"then fails before execution withUnknownAttributeError, naming its path and suggesting":vendor/status". Formatch, the error is inReadError.cause. Without the layer, unknown keys still return[]. Build queries from yourAttributedefinitions'.key(or entity type handles'attributes.x.key) to avoid spelling mistakes. See Datalog for wiring. - Transaction provenance under a past
validAt: join the assertion's fourth-term transaction to:_tx/actorin the same query. These reserved metadata rows follow only recorded time. If the join is empty, check that the recorded cut includes the assertion's commit and that the assertion is valid at the requested point. See Datalog.
Use triples.explain(query) or CLI query explain, then run with debug output to inspect the compiled plan without changing the query.
Why are only 100 query rows returned?
Triples.query and queryPage are bounded and default to 100 rows. This is not evidence that only 100 matches exist. Follow nextCursor using the same canonical query, page size, temporal basis, and database scope. Cursors are opaque, versioned, and snapshot-stable; do not decode or edit them.
Use queryAll only for trusted internal/batch work that deliberately needs an unbounded complete result. Pagination bounds result transfer, not necessarily all database work, especially for joins, aggregation, or counts.
Entity timelines use a different newest-first contract: preserve the first page's snapshotPosition and pass its nextBeforePosition as beforePosition on the next request.
Which historical clock should I set?
recordedAt asks what the database knew; validAt asks what was true in the domain. Setting only recordedAt still uses the current valid time, and setting only validAt still uses the latest recorded knowledge. For “what did we believe then about what was true then?”, set both explicitly.
Recorded time is the commit clock and cannot be supplied or backdated; an import of historical data is recorded at the import's commit time. Put domain dates in valid time, and model "when we learned this" as an ordinary attribute if you need it.
Both are non-negative epoch milliseconds. Valid intervals are [validFrom, validTo), so a fact is not visible exactly at its validTo. For exact transaction order, use journal positions; multiple commits can share one millisecond. Read the dated example.
What should happen on a duplicate command ID?
With the Database client, retry the same command ID to receive the original receipt; the callback does not run again.
Command IDs are atomically unique within one Triplex database. With raw Triples, reusing a committed ID raises CommandAlreadyCommittedError with the original transaction ID; Triplex does not apply the write again. Treat the original transaction as the durable idempotency receipt and retrieve it with transactionByCommand(commandId) or CLI journal receipt.
Do not generate a replacement ID until you know you are issuing a genuinely new domain command. Command IDs must contain 1–1024 characters.
Why did invalid data commit?
The Database client pins and enforces the release on every command. If you use raw Triples, configuration metadata and enforcement are separate. meta.configSnapshot records which release governed a write, but constraints are enforced only when the command also passes meta.enforce.constraints. The built-in atomic rules cover requiredness, cardinality, uniqueness, and reference-target kinds.
For raw writes, derive or collect rules from the exact immutable snapshot you pin. Existing violations can remain during unrelated writes or repairs; enforcement rejects a new or worsened violation. Direct adapter writes, commands without enforce, authorization, and general Datalog invariants are outside this guarantee. Use validation observations when auditing or migrating existing data.
Why does an enforced write fail with ConstraintViolationError?
Enforcement checks required, cardinality, uniqueness, and reference-target rules at every valid-time edge of the affected entities, not only at the current time. The most common causes are time gaps rather than missing data:
- Facts of one entity start at different times. An assertion without
validFromstarts at the commit time. If one attribute is backdated and the others are not, the entity exists at the earlier instant without its required attributes. Give all of an entity's facts the samevalidFrom. - A reference points at an entity that is not valid yet. The target must have a fact of the target entity type valid at the referencing fact's
validFrom. Make referenced entities valid from an earlier (or the same) time. - A retraction erased an interval. Retracting a value removes it from all of valid time, so a required attribute can disappear from the entity's past. To end a value from a date on, retract it and re-assert it with
validToin the same transaction.
The error message names the first two violations with their constraint key, subject, and validAt. Inspect error.violations for the complete list; each entry's validAt is the epoch-millisecond instant at which the rule fails. See Constraints hold at every valid instant.
Why is a projection stale or empty?
Entity validation and derivation materialization are projections over already committed source facts. A projection failure cannot roll back the source transaction. Their read APIs expose freshness explicitly:
currentmeans the stored run matches the relevant current source position and basis;stalereturns last-known results, which must not be presented as current truth; andunmaterialized/unvalidatedmeans no run exists, so an empty result is not proof that no work or violation exists.
Run the materializer/validator after relevant journal changes, persist consumer checkpoints only after downstream effects complete, and schedule the next derivation run at nextTemporalBoundary. In-process change signals are wake-up hints; journal catch-up is the durable source.
Does moving a config ref roll back data?
No. setRef only points a name such as live at another existing immutable configuration snapshot. Operational facts, command receipts, configuration pins on old transactions, migrations, and external side effects remain unchanged. See the worked rollback example.
Where should authentication, authorization, and delivery live?
In the host application. Authenticate and authorize before selecting a trusted database mapping or providing Triples. The host also owns HTTP contracts, task lifecycle, inbox/outbox delivery, timers, retries, monitoring, retention, backup, recovery, and higher-order business invariants. Triplex supplies database primitives, not those operational guarantees. Continue with Host integration.