Building on Telha (Developers)¶
How to integrate Telha into applications: what belongs in it, how to talk to it, and the patterns that keep you out of trouble.
The routing rule: what goes where¶
Telha does not replace your application database. The split (normative in the data-split-routing spec, ROUTING.md in the app-layer scaffold):
| Store | Put here | Example |
|---|---|---|
| PostgreSQL | Transactional app state: rows you UPDATE, uniqueness you enforce, joins you need hot | Orders, carts, user settings, queues |
| Telha | Versioned business FACTS: anything where "what was true when, and how do we know" matters | Contract versions, risk assessments, policies, evidence, documents |
| Object storage | Large binaries | Original PDFs, media (Telha keeps the extracted text + spans and a reference) |
Rule of thumb: if you would ever be asked to prove its history, it is a Telha fact. If you only ever need its latest value fast, it is a Postgres row. Many entities are both: an Order row in PG for the transaction, a CONTRACT_VERSION fact in Telha for the obligation it created, linked by id.
Talking to Telha¶
TypeScript (@telha/sdk): REST transport for services, gRPC transport for the app layer (UDS/TCP with signed tenant tokens). Python (telha): REST, pydantic-typed. Both expose the same surface: records (create/get/history), relationships, query, snapshot, compare, schema, ingest, generate.
// Write a fact with business validity and a relationship
const { ids } = await telha.records.create({
label: "CONTRACT_VERSION",
properties: { number: "C-1042", clause: "Net 30" },
validTime: { start: effectiveDate },
relationships: [{ type: "AMENDS", target: previousVersionId }],
});
// Bitemporal read
const rows = await telha.query({
find: "CONTRACT_VERSION",
where: { number: { $eq: "C-1042" } },
atValidTime: asOf, atTxTime: knownAt,
expand: { types: ["AMENDS"], direction: "out", depth: 2 },
});
In the app layer, resolve a scoped client per request: TelhaConnector.forSession(session) gives you a channel bound to the caller's tenant; you never pass tenant ids in payloads (the server derives scope from credentials, always; payload tenant ids are ignored by design).
Patterns that matter¶
Updates are appends. update creates a new version; the old one remains at its transaction time. Do not build "fix it in place" flows; build "record the correction" flows and let the history be the feature.
Valid time is yours to state. If the fact was true from a business date, say so (validTime.start = effectiveDate). If you omit it, validity starts now, which is usually wrong for imported history.
Idempotency and retries. The SDKs retry only idempotent calls (reads, and creates carrying your idempotency key). Writes return the version's transaction time; treat a duplicate-submit as safe replay, not an error path.
Deletes are tombstones. DELETE /v1/records/:id cascades tombstones over relationships atomically. As-of queries before the tombstone still see everything; that is correct behavior, not a bug report.
Cursors are opaque and signed. Persist them as strings, send them back verbatim. They pin the query shape; changing the query invalidates the cursor (you get a BAD_CURSOR error, start over). Vector-ranked queries do not paginate.
Errors are typed. Every REST error is {code, message, request_id}; SDKs raise typed exceptions (QueryInvalidError carries a JSON pointer to the offending clause). Log the request_id; it joins your logs to Telha's traces.
Schema is observed, not declared. Write records; Telha infers per-label property types and versions the schema. GET /v1/schema?at=... shows what the schema WAS at any moment, which is how you debug "this field used to be a string".
Ingesting content from your app¶
For documents your app handles (uploads, generated reports), POST /v1/ingest with a format hint; poll the job. For structured payloads use format: "json": synchronous, no worker fleet, schema inference applies. If your source has authorship/ownership metadata, pass options.source_meta; those become PERSON-linked edges that power clarification routing (who gets asked about this document later).
Building AI features¶
Prefer the MCP tools over reimplementing retrieval: findRecords, semanticSearch, compareSnapshots, getHistory, generate, getTrace are already permission-scoped, LLM-shaped, and audited. The app layer's chat engine (runChat) shows the full pattern: session → role → allowlisted tools → audit sink. If you need generation inside your own flow, call /v1/generate and store the returned trace id next to your feature's output; that link is what makes your feature auditable.
Testing against Telha¶
Spin a real binary on a temp dir (it is one process): create a key, point your tests at it. The SDK contract-test pattern runs the shared query corpus against a live server; reuse it. For CI without a binary, the TS SDK's transports are interface-typed for mocking, but run the live leg somewhere: the corpus catches grammar drift that mocks hide.