smartchats-backend
The typed backend contract for SmartChats: the SmartChatsBackend
interface, the shared error type, HTTP helpers for NDJSON streams, the
AuthProvider interface and media storage helpers.
Two packages implement it:
smartchats-backend-local(LocalBackend,LocalAuthProvider), which talks tosmartchats-local-serversmartchats-backend-firebase(closed source), which talks to the cloud Firebase Functions
The app imports types from this package. Its bootstrap code picks which implementation to construct.
What’s in the package
| Module | Purpose |
|---|---|
types.ts | SmartChatsBackend, BackendCapabilities, every sub-API (LLMAPI, TTSAPI, EmbeddingsAPI, DataAPI, UsageAPI, KeysAPI, BillingAPI, ToolsAPI, IntegrationsAPI, InsightsAPI, StorageAPI), BackendError |
auth_provider.ts | AuthProvider, AuthUser, AuthCapabilities, SignInMethod, EmailCredentials |
http/ | postStream, readNdjson, base64ToPcmBuffer, statusToBackendErrorCode, toServerArgs, createLLMStreamResult, createLLMTTSStreamResult |
storage.ts | Media upload limits, MIME allow list, key and cursor helpers, argument validators |
The contract
interface SmartChatsBackend {
readonly capabilities: BackendCapabilities;
readonly id: string; // "firebase" | "local" | user-defined
readonly llm: LLMAPI; // stream(), streamWithTTS(), warmup?()
readonly tts: TTSAPI; // stream(), warmup?()
readonly embeddings: EmbeddingsAPI;
readonly data: DataAPI; // query(), healthCheck()
readonly usage: UsageAPI;
readonly keys: KeysAPI; // BYO provider keys
readonly billing: BillingAPI; // throws 'not_supported' when !capabilities.billing
readonly tools: ToolsAPI; // search(), fetchUrl()
readonly integrations: IntegrationsAPI;
readonly insights: InsightsAPI; // emit(events)
readonly storage: StorageAPI; // throws 'not_supported' when !capabilities.storage
health(): Promise<HealthReport>;
providers(): Promise<ProvidersReport>;
}LLM and TTS are streaming only. llm.streamWithTTS(args) takes
LLMCallArgs plus voice, speed, instructions, tts_provider and an
optional verdict block ({ model, effort?, cap_ms?, judge?, threshold? }),
and yields LLMTTSEvents: text, verdict, text_end, audio_start,
audio, audio_end and server timing events. See
llm-service for the verdict.
BackendCapabilities
All flags are booleans. The UI uses them to show or hide features.
| Flag | Meaning |
|---|---|
billing | Credit and tier billing is active (billing UI, checkout) |
byoKeys | Server-side bring-your-own key management |
embeddings | Embedding generation |
search | Web search tool |
urlFetch | URL fetch and readability parsing |
integrations | Third-party integrations (Google Docs, Gmail, Calendar) |
insights | Telemetry ingestion |
storage | Media storage (StorageAPI) |
providers() is separate from capabilities: it reports which LLM and TTS
providers can serve requests right now, given server keys and saved BYO keys.
BackendError
class BackendError extends Error {
constructor(
code: BackendErrorCode,
message: string,
retryable: boolean = false,
details?: unknown,
);
}BackendErrorCode is one of insufficient_credits, provider_error,
not_supported, network_error, rate_limited, invalid_request,
not_found, server_error, aborted or unknown.
statusToBackendErrorCode(status) maps 401 to invalid_request, 402 to
insufficient_credits, 429 to rate_limited, 5xx to server_error and
anything else to provider_error.
postStream
import { postStream, readNdjson } from 'smartchats-backend';
const response = await postStream(url, body, headers, signal); // Promise<Response>
for await (const frame of readNdjson(response)) {
// one parsed object per line; malformed lines are skipped
}postStream(url, body, headers, signal?) POSTs a JSON body and returns the
Response. Headers, including auth, are the caller’s. Failures become
BackendErrors:
- aborted (by
signalorAbortError) becomesaborted, not retryable - a network error becomes
network_error, retryable - a non-2xx status becomes the mapped code, retryable when the status is 500 or above
Retryable errors are retried twice, after 250 ms and 750 ms, unless the signal has aborted. Retries happen before any body is read.
Safari reports an aborted fetch as TypeError: Load failed, not
AbortError. postStream checks signal.aborted as well as the error
name. Code that copies the pattern should do the same.
createLLMStreamResult(response) and createLLMTTSStreamResult(...) turn a
streaming response into the LLMStreamResult and LLMTTSStreamResult
shapes. toServerArgs(args) converts LLMCallArgs to the server payload
(it builds text_format from schema and schema_name).
Authentication
Auth is not part of SmartChatsBackend. A backend gets its bearer token
when it is constructed. The app uses a separate AuthProvider:
interface AuthProvider {
readonly capabilities: AuthCapabilities; // { required: boolean, methods: SignInMethod[] }
getCurrentUser(): AuthUser | null; // { uid, email, displayName }
getIdToken(): Promise<string | null>;
onAuthChange(callback: (user: AuthUser | null) => void): () => void;
signIn(method: SignInMethod, credentials?: EmailCredentials): Promise<void>;
signOut(): Promise<void>;
sendPasswordReset?(email: string): Promise<void>;
}SignInMethod is 'google' | 'email' | 'anonymous'. EmailCredentials
is { email, password, signup? }. sendPasswordReset is optional: a
provider without email accounts leaves it out and the UI hides the link.
LocalAuthProvider (in smartchats-backend-local) reports
required: false, always returns one local user and makes sign-in and
sign-out no-ops. LocalBackend takes { baseUrl, apiKey? } and sends
Authorization: Bearer <apiKey> when a key is set.
Storage helpers
storage.ts is shared by client and server so both enforce the same rules:
STORAGE_ALLOWED_MIME, isAllowedMime, kindForMime, buildMediaKey,
usesMultipart and planParts (multipart above
STORAGE_MULTIPART_THRESHOLD_BYTES, 50 MiB), thumbnail helpers,
encodeMediaCursor and decodeMediaCursor, and the validators
validateCreateUploadArgs and validateListMediaArgs.
Other exports
SMARTCHATS_REQUIRED_TABLES, SMARTCHATS_DEFAULT_LOCAL_PORT (4242),
SMARTCHATS_DEFAULT_LOCAL_URL and LLM_PROVIDERS (openai, anthropic,
google, xai, together, azure).
See also
- llm-service for the server side of the streams
- smartchats-database for what goes through
data.query - Source:
packages/smartchats-backend/src/index.ts