| Package | Responsibility |
|---|---|
@channel.io/app-sdk-core |
Context types, extension contracts, Zod schemas, generated protocol types |
@channel.io/app-sdk-server |
NestJS integration, decorators, discovery, routing, tokens, native clients, signatures, testing |
@channel.io/app-sdk-wam |
React provider and hooks for the WAM runtime |
@channel.io/app-sdk-wam-ui |
Optional Channel-consistent WAM UI components |
@channel.io/app-sdk |
CLI entry point |
Channel client
-> AppStore
-> PUT {Function Endpoint}/{systemVersion}
-> signature guard
-> ChannelAppController
-> ExtensionDiscoveryService
-> decorated handler
-> result/error
-> AppStore
-> Channel client
The default controller route is PUT /functions/:version. The developer portal stores the
/functions root. Versioned discovery appends the system version. Callers without a system version
can invoke the bare root, so a standalone ingress must rewrite only bare PUT /functions to the
default /functions/v1 route while preserving the raw body. Reuse the same SDK controller and
signature guard; do not add an unsigned second dispatcher. A hosting platform may provide this
mapping; otherwise configure it in the app's ingress.
Place decorated classes in a NestJS module's providers:
@Extension({ name: "calendar", systemVersion: "v1" })
export class CalendarExtension {
@Func("calendar.listCalendars")
@InputSchema(ListCalendarsInputSchema)
@OutputSchema(ListCalendarsOutputSchema)
listCalendars(@Ctx() ctx: Context, @Input() input: ListCalendarsInput) {
return this.calendarService.list(ctx.channel.id, input);
}
}ExtensionDiscoveryService reads decorator metadata, creates full names, generates the function catalog, and dispatches incoming calls. Extension classes remain normal NestJS providers and can use dependency injection.
const options = {
appId: process.env.APP_ID!,
appSecret: process.env.APP_SECRET!,
signingKey: process.env.SIGNING_KEY!,
autoRegister: true,
};
@Module({
imports: [ChannelAppModule.forRoot(options)],
providers: [CalendarExtension],
})
export class AppModule {}After the HTTP server starts listening, auto-registration:
- Gets a cached app token from
TokenManager. - Calls
registerExtensionfor each discovered extension name/system version. - Retries transient failures with exponential backoff.
- Lets AppStore call the versioned endpoint to read function/metadata schemas.
When no extension decorator exists, auto-registration uses the core extension fallback for standalone functions.
App Secretis used only for token exchange.TokenManagerowns app/channel token caching, refresh, and concurrent request deduplication.NativeFunctionClienttransports native and app-function calls with an access token supplied by the caller.SignatureGuardverifies incomingx-signatureagainst the raw request body and hex Signing Key.- WAM manager/user authorization belongs to the Channel runtime, not the server token manager.
For multiple server replicas, provide a shared TokenCacheStorage.
const app = await NestFactory.create(AppModule, { rawBody: true });@Module({
providers: [
{ provide: APP_GUARD, useFactory: () => new SignatureGuard(options) },
],
})
export class AppModule {}Never verify a re-serialized object; whitespace and property order change the signed bytes.
The server returns an action like:
{
type: "wam",
attributes: {
appId: process.env.APP_ID,
name: "booking",
wamArgs: { bookingId: "..." },
},
}AppStore/Channel loads ${WAM_ENDPOINT}/booking. The React app uses WamProvider, useWamData, useCallFunction, useNativeFunction, useWamSize, and useWamClose.
Keep secrets out of wamArgs; the WAM is client-side code.
DataSource metadata is exposed through extension functions. Query execution is a separate gRPC service under datasource/grpc. Do not route query streams through the JSON Function Endpoint.
Use this order when examples disagree:
- Public package exports.
- Core extension schemas and interfaces.
- Server discovery/router/token source.
- Reference documents and current examples.