Production
This page describes the recommended production integration.
The short version:
store raw events
store latest Tiaude state
use track() as the fast path
use scoreUser() as the recovery pathWhat to persist
Persist raw events in your own database.
Also persist the latest Tiaude state per user.
await saveUserChurnState(userId, {
state: result.state,
riskScore: result.riskScore,
riskLevel: result.riskLevel,
});The state enables fast incremental scoring.
The raw events enable safe rebuilds.
Why raw events still matter
track() is an optimization.
It avoids replaying all events on every update.
It is not a replacement for event storage.
You need raw events to recover from:
- config changes;
- added or removed signals;
- out-of-order delivery;
- invalid or missing state;
- historical corrections;
- audits and debugging;
- SDK migrations.
The operational rule is:
state is fast
events are authoritativeEvent ingestion flow
Use track() when a new event arrives.
import { createChurnScorer } from "tiaude";
const scorer = createChurnScorer(config);
export async function ingestEvent(
userId: string,
event: { name: string; timestamp: string | Date },
) {
await saveRawEvent(userId, event);
const user = await loadUser(userId);
const result = scorer.track({
userId,
previousState: user.churnState ?? null,
event,
});
await saveUserChurnState(userId, {
state: result.state,
riskScore: result.riskScore,
riskLevel: result.riskLevel,
});
return result;
}Save the raw event first or in the same transaction if your architecture allows it.
Safer ingestion with fallback
In production, wrap the incremental path.
import {
ConfigMismatchError,
OutOfOrderEventError,
StateValidationError,
} from "tiaude";
function shouldRecompute(error: unknown) {
return (
error instanceof ConfigMismatchError ||
error instanceof OutOfOrderEventError ||
error instanceof StateValidationError
);
}
export async function ingestEvent(
userId: string,
event: { name: string; timestamp: string | Date },
) {
await saveRawEvent(userId, event);
const user = await loadUser(userId);
try {
const result = scorer.track({
userId,
previousState: user.churnState ?? null,
event,
});
await saveUserChurnState(userId, {
state: result.state,
riskScore: result.riskScore,
riskLevel: result.riskLevel,
});
return result;
} catch (error) {
if (!shouldRecompute(error)) {
throw error;
}
const events = await loadUserEvents(userId);
const result = scorer.scoreUser({
userId,
events,
});
await saveUserChurnState(userId, {
state: result.state,
riskScore: result.riskScore,
riskLevel: result.riskLevel,
});
return result;
}
}This is the recommended production convention.
Scheduled refresh flow
Use refreshScore() when no new event arrived but time should still affect the score.
export async function refreshUserScore(userId: string) {
const user = await loadUser(userId);
if (!user?.churnState) return null;
try {
const result = scorer.refreshScore({
userId,
previousState: user.churnState,
now: new Date(),
});
await saveUserChurnState(userId, {
state: result.state,
riskScore: result.riskScore,
riskLevel: result.riskLevel,
});
return result;
} catch (error) {
if (!shouldRecompute(error)) {
throw error;
}
const events = await loadUserEvents(userId);
const result = scorer.scoreUser({
userId,
events,
});
await saveUserChurnState(userId, {
state: result.state,
riskScore: result.riskScore,
riskLevel: result.riskLevel,
});
return result;
}
}Typical use cases:
- nightly score aging;
- account health dashboard refresh;
- CRM sync;
- customer success queues;
- proactive churn review jobs.
Config changes
When config changes, old states may become incompatible.
Examples:
- adding a signal;
- removing a signal;
- changing a signal event name;
- changing a weight;
- changing a threshold;
- changing a half-life;
- changing score levels.
When this happens, Tiaude throws ConfigMismatchError.
Recovery:
load raw events -> scoreUser() -> persist new stateChanging only reason or action does not break compatibility.
User identity
Pass userId whenever possible.
It protects you from accidentally using one user’s saved state for another user.
If the state belongs to a different user, Tiaude throws StateUserMismatchError.
Production checklist
- store raw events;
- store latest Tiaude state;
- call
track()for normal event ingestion; - call
refreshScore()from scheduled jobs when needed; - catch rebuildable errors and call
scoreUser(); - keep event names stable;
- store timestamps as UTC ISO strings;
- pass
userId; - treat
scoreUser()as the canonical rebuild path.