trieur.

The server

@trieur/server is Bun plus SQLite and nothing else. It fits in four files.

TRIEUR_TOKEN=secret bun packages/server/src/serve.ts
# trieur โ†’ http://localhost:4747

What it keeps, and why

The events, not just the model. An online model cannot be retrained backwards: if you change feature extractor, model or hyperparameter, the only way to benefit on past data is to replay the events. Delete the models row, restart, and history goes through the new model. An online model without its events is a dead end.

The serialised model, so we do not replay on every boot.

The vectors, so we do not pay twice for an embedding call already made.

The protocol

A single types file (@trieur/learn/protocol), imported by both sides: client and server cannot drift apart without TypeScript saying so.

RouteRole
POST /v1/decks/:deck/eventspushes a batch of filings
GET /v1/decks/:deck/model?since=model snapshot, or null when the client is up to date
POST /v1/decks/:deck/predictserver-side ranking (sparse models + embeddings)
GET /v1/decks/:deck/statsmeasured accuracy, vocabulary size, expert weights
GET /healthopen even when a token is required

Two structural decisions:

Embeddings

EMBED_URL=https://api.openai.com/v1 EMBED_MODEL=text-embedding-3-small EMBED_KEY=sk-โ€ฆ \
  bun packages/server/src/serve.ts

Any OpenAI-compatible API will do โ€” including a local one (Text Embeddings Inference, llama.cpp, Ollama). Without EMBED_URL and EMBED_MODEL the rung is simply disabled and the server runs without it.

The computation starts after the response to the client: nobody waits on a third-party provider for a filing to be accepted. Every card is first classified by the index as it stands, then added โ€” so the accuracy of embeddings is measured on cards never seen, exactly like the sparse models. That is what gives them their weight in the blend, for free, since the vector has just been computed anyway.

Environment variables

VariableDefaultRole
PORT4747HTTP port
TRIEUR_DBtrieur.sqliteSQLite file
TRIEUR_TOKENโ€”when set, Bearer is required on data routes
TRIEUR_ORIGIN*Access-Control-Allow-Origin
EMBED_URLโ€”OpenAI-compatible API, e.g. https://โ€ฆ/v1
EMBED_MODELโ€”embedding model; absent = rung disabled
EMBED_KEYโ€”API key

Mounting it elsewhere

createApi() returns a Request โ†’ Response function: it can be tested without opening a port and mounted in any server instead of imposing its own.

import { Embedder, VectorIndex, createApi, openDb } from '@trieur/server';

const db = openDb('trieur.sqlite');
const api = createApi({ db, token, vectors: new VectorIndex(db, new Embedder({ url, model, key })) });

Bun.serve({ fetch: (req) => api.handle(req) });

api.flush() writes the in-memory models โ€” call it on SIGINT/SIGTERM, otherwise it is lost sorting work. The bundled server already does.

One model per deck

The deck in the URL isolates models: two unrelated card sets (links, photos) do not mix. Events, vectors and the snapshot are all stored per deck.