Teardown

API Reference

Complete API reference for createTeardown, the Teardown object, the domain services, and the cross-cutting ports.

createTeardown

Wire a storage implementation and configuration into the Teardown runtime.

function createTeardown(options: TeardownOptions): Teardown
import { createTeardown } from "@teardown/server";

const td = createTeardown({
  storage: createMemoryStorage(),
  config: { sessionSecret: process.env.SESSION_SECRET! },
});

Throws if config.sessionSecret is absent and no custom signer is supplied.

TeardownOptions

PropertyTypeRequiredDescription
storageTeardownStorageYesThe pluggable storage layer
configTeardownConfigNoRuntime configuration
tenantTeardownTenantNo{ orgId, projectId } → single-tenant: skips API-key auth and makes td-api-key/td-org-id/td-project-id unnecessary (hosted-only). Omit for multi-tenant
cacheCachePortNoCache for version/session lookups. Default: no-op
loggerLoggerPortNoStructured logger. Default: no-op
metricsMetricsPortNoMetrics sink. Default: no-op
clockClockPortNoClock + uuid source. Default: system
signerTokenSignerNoSession-token signer. Default: HS256 (jose) using config.sessionSecret
onVersionStatusChangeOnVersionStatusChangeNoHook fired on a real version-status change
onBuildStatusChangeOnBuildStatusChangeNoHook fired on a real build-status change

Teardown

The object returned by createTeardown.

PropertyTypeDescription
servicesTeardownServicesThe wired domain services
router(info?)(info?: RuntimeInfo) => TdRouterBuild the ingest HTTP router
storageTeardownStorageThe storage you passed in
configResolvedConfigThe configuration with defaults applied
cacheCachePortThe resolved cache port
loggerLoggerPortThe resolved logger port
metricsMetricsPortThe resolved metrics port
clockClockPortThe resolved clock port
signerTokenSignerThe resolved token signer

SDK_NAME

import { SDK_NAME } from "@teardown/server";
// "@teardown/server" — a stable namespace for cache keys, headers, etc.

Services

td.services exposes eight domain services. For a self-hosted ingest server, the only ones the mounted router uses are identify and events (with forceUpdate/session behind them) — the rest are for version management and custom integrations. Unless noted, methods return an AsyncResult<T> ({ success: true; data: T } | { success: false; error: string }); the version/build services return a typed error union instead.

ServicePurpose
identifyIdentify a device/user → session + version_info
eventsIngest a batch of events
forceUpdateResolve + provision version/build status (the ingest read path)
sessionCreate / reuse / look up device sessions
environmentResolve, create, and delete environments
pushTokensUpsert / invalidate / read a device's push token
versionsVersion management (CRUD + status cascade)
buildsBuild management (CRUD)

identify

identify(headers: IdentifyHeaders, request: IdentifyRequest): Promise<
  | { success: true; data: IdentifyData }
  | { success: false; error: string }
>

IdentifyData is { session_id, device_id, user_id, token, version_info }. This is the full payload the POST /v1/identify route returns.

events

processEvents(headers: EventsHeaders, request: EventsRequest): Promise<
  | { success: true; data: ProcessEventsResult }
  | { success: false; error: string }
>

ProcessEventsResult is { eventIds: string[]; processedCount: number; failedCount: number }. The handler maps it to the snake_case wire payload (event_ids / processed_count / failed_count). A batch larger than config.maxEventsPerBatch is rejected. A user_signed_out event invalidates the device's push token.

forceUpdate

MethodReturnsDescription
getOrCreateVersionBuild(projectId, versionName, buildNumber, platform)AsyncResult<{ versionId; buildId }>Race-safe get-or-create of the version + build; caches the resolved ids for 15 minutes
checkVersionStatus(input)AsyncResult<VersionInfo>Resolve the client version_info (most-restrictive of version vs build status)

input is { projectId, versionName, buildNumber, platform } where platform is "IOS" | "ANDROID".

session

MethodReturnsDescription
getOrCreateSession(deviceId, deviceInfo, context, versionBuildInfo, sdkInfo?)AsyncResult<{ session; token; isExisting }>Reuse a valid session (extending its expiry) or create a new one with a signed JWT
createSession(deviceId, deviceInfo, claims, versionBuildInfo, sdkInfo?)AsyncResult<{ session; token }>Create a new session
extendSessionExpiry(sessionId, deviceId)AsyncResult<void>Extend a session by config.sessionReuseExtendMs
getSession(sessionId)AsyncResult<SessionEntity | null>Look up a session by id
getSessionByToken(token)AsyncResult<SessionEntity | null>Look up a session by token (cache-first)
getValidSessionForDevice(deviceId)AsyncResult<{ session; token } | null>The current valid session for a device (verifies the token)

deviceId here is the internal device id (DeviceEntity.id).

environment

MethodReturnsDescription
getEnvironment(projectId, slug)AsyncResult<EnvironmentEntity | null>Find an environment by project + slug
createEnvironment(projectId, name, slug, type)AsyncResult<EnvironmentEntity>Create an environment (slug unique per project)
deleteEnvironment(environmentId)AsyncResult<void>Delete an environment (enforces the last-env / last-PRODUCTION / last-DEVELOPMENT rules)

pushTokens

MethodReturnsDescription
upsertPushToken(orgId, projectId, deviceId, notificationsInfo)AsyncResult<{ token; isNew }>Upsert the device's push token (skips unless enabled + granted + token present)
invalidateToken(deviceId)AsyncResult<void>Mark the device's push token(s) invalid
getTokenByDeviceId(deviceId)AsyncResult<PushTokenEntity | null>Read the device's push token

versions

MethodReturnsDescription
getVersionById(projectId, versionId)AsyncResult<VersionEntity, VersionServiceError>Get one version
getVersionsByIds(projectId, versionIds)AsyncResult<VersionEntity[], VersionServiceError>Get many (≤ 100)
searchVersionsByProject(params)AsyncResult<VersionsPage, VersionServiceError>Paginated/sortable search
updateVersion(projectId, versionId, data, options?)AsyncResult<VersionEntity, VersionServiceError>Update status and/or notes (status cascades to builds)

data is { status?: ProjectVersionStatus; notes?: string | null }; options is { sendNotification?: boolean }. See Version Management.

builds

MethodReturnsDescription
getBuildById(projectId, buildId)AsyncResult<BuildEntity, BuildServiceError>Get one build
getBuildsByIds(projectId, buildIds)AsyncResult<BuildEntity[], BuildServiceError>Get many (≤ 100)
searchBuildsByProject(params)AsyncResult<BuildsPage, BuildServiceError>Paginated/sortable search
updateBuild(projectId, buildId, data, options?)AsyncResult<BuildEntity, BuildServiceError>Update status and/or notes (marks the build overridden)

TeardownConfig

interface TeardownConfig {
  sessionSecret?: string;       // HMAC key signing client session tokens; required unless a custom signer is supplied
  sessionTokenExpiry?: string;  // jose-format, default "15m"
  sessionTtlMs?: number;        // session row TTL, default 900000 (15m)
  sessionReuseExtendMs?: number;// reuse extension, default 300000 (5m)
  maxEventsPerBatch?: number;   // default 100
}

resolveConfig(config?) applies the defaults and is exported alongside the ResolvedConfig type if you need the resolved values directly.

Cross-cutting ports

All cross-cutting ports are optional, with a no-op or system default. Import their types from @teardown/server/ports.

CachePort

Matches @teardown/redis's CacheInterface, so a Redis cache is structurally assignable. Default: a no-op NullCache.

interface CachePort {
  get<T>(key: string): Promise<T | null>;
  set(key: string, value: unknown, ttlSeconds?: number): Promise<boolean>;
  del(key: string): Promise<boolean>;
}

LoggerPort

Default: noopLogger.

interface LoggerPort {
  debug(message: string, context?: Record<string, unknown>): void;
  info(message: string, context?: Record<string, unknown>): void;
  warn(message: string, context?: Record<string, unknown>): void;
  error(message: string, context?: Record<string, unknown>): void;
}

MetricsPort

Generic counter/histogram sink. Default: noopMetrics. Attributes are { orgId?, projectId?, environment?, status? }.

interface MetricsPort {
  counter(name: string, value: number, attributes?: MetricAttributes): void;
  histogram(name: string, value: number, attributes?: MetricAttributes): void;
}

ClockPort

Injectable time + uuid source (edge-safe and test-deterministic). Default: systemClock.

interface ClockPort {
  now(): Date;
  uuid(): string;
}

TokenSigner

Session-JWT signer. Default: an HS256 jose signer built from config.sessionSecret.

interface TokenSigner {
  sign(claims: SessionTokenClaims): Promise<string>;
  verify(token: string): Promise<SessionTokenPayload | null>;
}

interface SessionTokenClaims {
  sessionId: string;
  deviceId: string;
  userId: string;
  environmentId: string;
  projectId: string;
  orgId: string;
}

createJoseSigner({ secret, expiry? }) builds the default signer and is exported if you want to construct it explicitly (or run it on a custom secret/expiry):

import { createJoseSigner } from "@teardown/server";

const signer = createJoseSigner({ secret: process.env.SESSION_SECRET!, expiry: "30m" });
const td = createTeardown({ storage, signer });

Subpath exports

ImportContents
@teardown/servercreateTeardown, TeardownOptions, Teardown, TeardownTenant, SDK_NAME, createJoseSigner, config + hook types
@teardown/server/portsTeardownStorage, the *Repository interfaces, the *Entity/New*/*Patch types, StorageError, and the cross-cutting ports
@teardown/server/contractsThe wire-contract DTO types + validateIdentifyRequest / validateEventsRequest
@teardown/server/httpThe router builder, normalized Td* primitives, the authenticator, CORS constants
@teardown/server/http/fetchtoFetchHandler (Web-standard adapter)
@teardown/server/http/elysiateardownElysia (Elysia plugin)
@teardown/server/adapters/memorycreateMemoryStorage, MemorySeed