v0.3.0 — six packages become one, and the credential boundary becomes a build failure
Important
Not on npm yet. npm install realtime-avatar will 404 until 2026-08-27 10:32 UTC.
Every name was unpublished on 2026-08-26, and npm blocks a fully-unpublished name for 24
hours — no version number gets around it. The release pipeline is verified green and waiting;
this tag marks the code, not the registry state.
One package
npm install realtime-avatar gives you the whole SDK. realtime-avatar-react is gone — its
entries are now subpaths.
| Import | What it is |
|---|---|
realtime-avatar · /server |
Server client. Holds your API key. 18.8 KB |
/nextjs /hono /express /tanstack-start |
Route adapters. Key-holding |
/react /react-native |
React and Expo bindings. Never hold a key |
/browser /tools |
Mic + playback, and the browser tool plane |
realtime-avatar-mcp stays a separate name because it is a CLI you run with npx and never install.
The credential boundary is now a build failure
Importing a key-holding entry into a browser bundle throws at bundle time. The six server
subpaths carry browser and react-native export conditions pointing at a module whose only
statement is a throw.
Verified on the built package: a browser bundle importing both halves contains 0 occurrences
of Bearer or apiKey.
This replaced a naming convention with a mechanism. The previous shape rested on the claim that an
exports condition "chooses which file is bundled, never whether the package is" — measured on a
fixture, that is false.
Two traps, if you copy this pattern. "browser": null is worse than nothing: webpack errors,
but Vite 8 / rolldown ignores it and bundles the server file with the secret. And
"sideEffects": false lets a bundler treeshake a throw-only module away — the first version of
this guard measured as doing nothing until sideEffects was narrowed to name it.
Fixed
- Eight demo apps threw
MODULE_NOT_FOUNDat startup and the Next.js quickstart imported a
package that 404s — they resolved npm names that had been unpublished. AvatarCallcould not be used by anyone. It requiredclient: RealtimeAvatarClient, a class
the browser half deliberately does not export, so the prop named a type no consumer could
produce. It now takesAvatarSessionClient, a five-method interface satisfiable by a plain object.- Rule 5 of the agent contract told you to import a subpath that throws
ERR_PACKAGE_PATH_NOT_EXPORTED. - The published packages shipped with no README.
Types now derive from the published contract
spec/realtime-avatar.openapi.json is vendored from the live OpenAPI document and generated into
types that the public surface indexes into, so a contract change is a typecheck failure rather than
a silent divergence. npm run spec:check reports drift and exits 0 offline.
Internals
| before | after | |
|---|---|---|
| Packages an app installs | 6 | 1 |
| Unreachable source files | 1,230 lines | 0 — now a build gate |
| tsup builds | 7 | 1 |
npm run build |
22.0s | 7.9s |
realtime-avatar/react exports |
82 | 31 |
npm run reachable walks the real import graph and fails if any source file is reachable from
nothing. 24 LiveKit re-exports were removed — they are peer dependencies, so import them from
LiveKit and get the version you installed.
Known gap
shouldReplayPendingTurn is a documented predicate that nothing calls and nothing tests: a turn
sent while the session is reconnecting is not replayed today. Annotated rather than deleted.