Library codegen for Kotlin, Swift, and TypeScript
Avo can emit generated code in two shapes for Kotlin, Swift, and TypeScript sources: the default single-source output (one file per source) and the library mode output that splits the runtime from the per-source types. Both modes are supported — they serve different use cases and you can generate either from the same source.
Library mode is the better fit when you have multiple Avo sources sharing the same app, when you’re building an internal tracking library reused across apps, or when you want the runtime code separated from the per-source event types.
1. The shape of library mode
avo pull emits multiple files per source instead of one. The shared source-independent runtime — Avo class, validation helpers, AvoEvent protocol/interface, AvoInvoke — lives in a separate “library” file. The per-source “app” file contains only the things that change when your tracking plan changes: per-event classes and the codegen-bound IDs. Optionally, a third file is extracted with everything needed for Avo initialization, so initialization can be decoupled from the apps too.
What gets generated
- Library file: the runtime.
AvoEnv,AvoAssert,AvoInvoke, theAvoEventprotocol/interface, and theAvoclass. Regenerated rarely. Every source generates an identical library file. Shared across all sources/apps. Optionally orchestrated from a single place — “the library”. - App file: per-source types and a thin config object. Regenerated whenever the tracking plan source changes.
- In TypeScript, a third file
AvoConfig.tsholds the source config withschemaId/actionId/branchId/sourceIdplus destination API keys. It has no runtime imports — just the values.
Mental model
Events are values you construct, not method calls. You build an instance of LoginSuccessEvent(timestamp: ..., teamId: ..., ...) and pass it to avo.process(event) (Swift/Kotlin) or avo.track(event) (TypeScript). The call returns a map keyed by destination name, with the per-destination payload (event name + properties) for each one.
The boundary: the library file knows nothing about your tracking plan. The app file knows nothing about how validation or invocation metrics work. You decide what to do with the per-destination map the call returns — either fan out manually, or hand off to destination implementations the runtime calls for you.
2. Quick start
Enable library mode on a per-source basis with the --forceFeatures flag on avo pull. The flag names match the language:
avo pull --forceFeatures SwiftLibraryInterface
avo pull --forceFeatures KotlinLibraryInterface
avo pull --forceFeatures TypeScriptLibraryInterfaceYou can also enable these features in your Avo workspace if you’d like them on by default — reach out and we’ll switch them on for your source.
Kotlin
avo pull produces:
Analytics.kt— per-event data classes andAvoTrackingPlanConfigAnalyticsLibraryInterface.kt— runtime (Avoclass,AvoEvent,AvoAssert,AvoInvoke)
Both files share the same package (inferred from the source path, default sh.avo). Treat them like any other Kotlin files in your project — drop them anywhere in your source tree that fits your module layout.
Initialize using the initAvo extension that’s generated alongside the per-event classes:
import sh.avo.Avo
import sh.avo.AvoEnv
import sh.avo.AvoVerificationError
import sh.avo.LoginSuccessEvent
import sh.avo.initAvo
val avo = Avo.initAvo(env = AvoEnv.DEV)
try {
val payloads = avo.process(LoginSuccessEvent(
timestamp = 1730000000,
teamId = "team_42",
teamDomain = "acme.example"
))
payloads["custom"]?.let { event ->
myAnalyticsSdk.logEvent(event.name, event.properties)
}
} catch (e: AvoVerificationError) {
// Validation failed and strict mode is on.
println("[avo] ${e.messages.joinToString(", ")}")
}process() returns Map<String, AvoDestinationEvent> keyed by destination name (lowercased identifier from your Avo workspace). It throws AvoVerificationError when validation fails and strict = true (the default); otherwise it logs a warning and continues. Returns an empty map in noop mode.
Swift
avo pull produces:
Analytics.swift—AvoTypesnamespace, per-event structs,AvoTrackingPlanConfig, and anextension Avowith a no-configinitAvoAnalyticsLibraryInterface.swift— runtime
The app file imports Library. You decide where the library file lives: package it as a Swift module called Library (SPM package, Xcode target, internal framework — whichever fits your project) so the import resolves, or drop both files into the same module and remove the import Library line from the app file by hand. Either approach works; the library file is just Swift source.
Initialize:
import Library
let avo = Avo.initAvo(env: .dev)
do {
let payloads = try avo.process(event: LoginSuccessEvent(
timestamp: 1730000000,
teamId: "team_42",
teamDomain: "acme.example"
))
if let event = payloads["custom"] {
MyAnalyticsSDK.logEvent(name: event.name, properties: event.properties)
}
} catch let error as AvoVerificationError {
// Validation failed and strict mode is on.
print("Avo verification: \(error.messages.joined(separator: \", \"))")
}process(event:) returns [String: AvoDestinationEvent]. It throws AvoVerificationError when validation fails and strict is on; otherwise it logs via NSLog and continues.
TypeScript
avo pull produces three files:
Avo.ts— per-event classes (LoginSuccessEvent, etc.) extendingBaseAvoEvent, plus re-exports of the public runtime surfaceAvoLibrary.ts— runtime (Avoclass,AvoEvent,AvoAssert,BaseAvoEvent)AvoConfig.ts— codegen-bound config (default export),DestinationKeyunion, and per-destination API keys
AvoConfig.ts is the only file that contains your schemaId/actionId/branchId. Move it (and update the import path) wherever you want Avo.init to live — at the app level, a library level, or any module boundary.
There are no runtime dependencies outside the standard library and fetch (used by AvoInvoke).
Avo.init takes two arguments: a runtime AvoConfig and the codegen-bound config (default-exported from AvoConfig.ts). The recommended starting point is to wire all destinations through the runtime:
import { Avo, AvoEnv, AvoVerificationError, LoginSuccessEvent } from './Avo';
import codegenConfig from './AvoConfig';
const avo = Avo.init(
{
env: AvoEnv.Dev,
destinations: {
Custom: {
make(env, apiKey) { /* initialize your SDK */ },
logEvent(event) { myAnalyticsSDK.logEvent(event.name, event.properties); },
setUserProperties(userId, props) { myAnalyticsSDK.identify(userId, props); },
},
},
},
codegenConfig,
);
try {
avo.track(new LoginSuccessEvent(1730000000, 'team_42', 'acme.example'));
} catch (e) {
if (e instanceof AvoVerificationError) {
console.error('[avo]', e.messages.join(', '));
} else {
throw e;
}
}AvoDestination (make, logEvent, setUserProperties) plays the same role as the custom destination interface in the single-source output — the shape is different but the idea is the same. destinations is all-or-nothing: if you provide any, you must provide an implementation for every DestinationKey or Avo.init will throw.
If the all-or-nothing model doesn’t fit (for example, you want to fan out manually for some destinations), omit destinations and fan out yourself:
const avo = Avo.init({ env: AvoEnv.Dev }, codegenConfig);
try {
const payloads = avo.track(new LoginSuccessEvent(1730000000, 'team_42', 'acme.example'));
const custom = payloads['Custom'];
if (custom) {
myAnalyticsSDK.logEvent(custom.name, custom.properties);
}
} catch (e) {
if (e instanceof AvoVerificationError) {
console.error('[avo]', e.messages.join(', '));
} else {
throw e;
}
}track() returns Record<string, AvoDestinationEvent> keyed by DestinationKey from AvoConfig.ts. It throws AvoVerificationError the same way Kotlin/Swift do.
3. Differences from the single-source output
If you’re switching an existing source from the single-source output to library mode, here’s what changes — and what stays the same.
Before / after
Kotlin
// Single-source
val avo = Avo(env = AvoEnv.DEV, customDestination = MyCustomDestination())
avo.loginSuccess(
timestamp = 1730000000,
teamId = "team_42",
teamDomain = "acme.example",
)
// Library mode
val avo = Avo.initAvo(env = AvoEnv.DEV)
try {
val payloads = avo.process(LoginSuccessEvent(
timestamp = 1730000000,
teamId = "team_42",
teamDomain = "acme.example",
))
payloads["custom"]?.let { event ->
myAnalyticsSdk.logEvent(event.name, event.properties)
}
} catch (e: AvoVerificationError) {
println("[avo] ${e.messages.joinToString(", ")}")
}Swift
// Single-source
let avo = Avo(env: .dev, customDestination: MyCustomDestination())
avo.loginSuccess(
timestamp: 1730000000,
teamId: "team_42",
teamDomain: "acme.example"
)
// Library mode
let avo = Avo.initAvo(env: .dev)
do {
let payloads = try avo.process(event: LoginSuccessEvent(
timestamp: 1730000000,
teamId: "team_42",
teamDomain: "acme.example"
))
if let event = payloads["custom"] {
MyAnalyticsSDK.logEvent(name: event.name, properties: event.properties)
}
} catch let error as AvoVerificationError {
print("Avo verification: \(error.messages.joined(separator: \", \"))")
}TypeScript
// Single-source
import Avo from './Avo';
Avo.initAvo({ env: Avo.AvoEnv.Dev }, /* destinations */ {});
Avo.loginSuccess({
timestamp: 1730000000,
teamId: 'team_42',
teamDomain: 'acme.example',
});
// Library mode
import { Avo, AvoEnv, AvoVerificationError, LoginSuccessEvent } from './Avo';
import codegenConfig from './AvoConfig';
const avo = Avo.init({ env: AvoEnv.Dev }, codegenConfig);
try {
const payloads = avo.track(new LoginSuccessEvent(1730000000, 'team_42', 'acme.example'));
const custom = payloads['Custom'];
if (custom) {
myAnalyticsSDK.logEvent(custom.name, custom.properties);
}
} catch (e) {
if (e instanceof AvoVerificationError) {
console.error('[avo]', e.messages.join(', '));
} else {
throw e;
}
}What stays the same
- Event names, property names, and validation rules. The tracking plan is unchanged — only the shape of the generated code differs.
- Strict / noop modes.
strict(throw on validation failure in non-prod) andnoop(drop everything) behave the same way. - Inspector integration. Construct your Avo Inspector instance from the SDK and pass it into the init call — same as the single-source output.
- Implementation status tracking. The CLI reports event-usage status the same way regardless of mode.
- Destination interface idea. TypeScript’s
AvoDestination(make,logEvent,setUserProperties) plays the same role as the single-source custom destination — the shape differs but the concept is the same. - The
avo pullworkflow. Same command, same authentication, same source/destination configuration in your Avo workspace. - Destination API keys. Still embedded by codegen. In TypeScript they’re in
AvoConfig.ts; in Swift/Kotlin they’re insideAvoTrackingPlanConfigand used by theinitAvoextension.
What changes
Avoconstructor signature. The single-sourceAvo(env:, customDestination:, ...)(Swift/Kotlin) and the free-functionAvo.initAvo(config, destinations)(TypeScript) becomeAvo.initAvo(env:)(Swift/Kotlin) andAvo.init(config, codegenConfig)(TypeScript).- Event method calls become event constructors and a single
process/trackcall. Eachavo.someEvent(...)becomesavo.process(SomeEvent(...))(Swift/Kotlin) oravo.track(new SomeEvent(...))(TypeScript). process()/track()throwsAvoVerificationError. In library mode all three languages throw a typedAvoVerificationErrorwhenstrictis on. Wrap calls intry/do-catch— see the quick-start examples.- TypeScript:
AvoDestinationinterface. Custom destinations implementAvoDestinationwithmake / logEvent / setUserProperties. All-or-nothing — provide implementations for everyDestinationKey, or omitdestinationsentirely and fan out manually. - System properties.
setSystemProperties(...)/Avo.setSystemProperties(...)becomesAppSystemProperties.configure(...)(Kotlin) orAppSystemProperties.shared.configure({...})(Swift, TypeScript).verify()andprocess()/track()throwAvoVerificationErrorwith the message “AppSystemProperties.configure() must be called before sending events.” if a tracked event needs system properties andconfigure()hasn’t run. - Imports. Add the library file to your build and import its types where you previously imported from the single file. In Swift this is
import Library(if you packaged it that way); in Kotlin it’simport sh.avo.*(or whatever package your source path resolves to); in TypeScript it’simport { ... } from './Avo'(the app file re-exports the public library surface).