Buko Docs

Kit Development

This is the canonical development guide for native Kits.

Kits are reviewed, interactive product modules compiled into the app. They are intended for workflows that need a real application surface rather than a chat transcript. A Kit is not a bot, a chat, a remotely downloaded mini program, or an arbitrary WebView.

Status

Kit development is currently a Private Developer Preview limited to official modules and explicitly approved source-review partners. Public enrollment and third-party submission are not open. There is no public self-service submission portal, published Kit SDK, or dynamic package installation.

Official Kits that appear in a released app are compiled into that binary and reviewed with the app release. The server cannot deliver or activate a Kit that the binary did not compile.

The current kit_api package is a repository-local contract at version 0.0.1. It may change together with official Kits in one reviewed app release. Do not treat the Preview API as a stable public compatibility promise.

When To Build A Kit

Use a Kit when the experience needs continuous, stateful interaction:

Use the Bot API when the experience is primarily conversational, message-driven, or hosted by an external agent. A bot may later deep-link to an added Kit through an explicitly reviewed integration, but it must not automate the Kit UI or write Kit storage directly.

Core Concepts

ConceptMeaning
KitA user-visible interactive feature compiled into the app.
Kit moduleThe Dart package that implements one Kit's UI and client behavior.
Kit hostApp-owned code that controls registration, routing, identity, platform access, and capabilities.
Kit server moduleKit-owned server service and route adapters composed into the shared authenticated Worker.
DescriptorCompiled metadata declared by the module, including version and supported platforms.
CatalogServer-controlled metadata, audience, platform rollout, status, and kill switch.
AddPut a Kit entry in the current user's Chats list. It does not download code.
RemoveRemove the Chats entry while preserving local cache and server business data.
Clear dataA separate, explicit destructive action owned by the Kit.

Non-Negotiable Rules

Repository Layout

Each Kit is a vertical package with its own UI, state, tests, assets, and server business module:

app/
  lib/features/kits/
    kit_host.dart
    kit_route_screen.dart
    kit_catalog_store.dart

packages/
  kit_api/

kits/
  example_kit/
    assets/
    lib/
      example_kit.dart
      src/
    server/
    test/
    pubspec.yaml

workers/server/
  src/kits/
    kit_catalog.ts
    kit_server_api.ts
    routes.ts
  migrations/

kits/<kit_id> is a local Dart package. app and the Kit both depend on packages/kit_api by path. Shared UI or utility packages are extracted only after more than one real Kit proves the shared boundary; do not create a broad foundation layer in advance.

Server code remains part of one deployment and one ordered migration history. A Kit owns its business service, but the shared Worker owns public routing, authentication, catalog policy, rate limits, idempotency primitives, and audit boundaries.

Create A Module

Package Setup

A minimal package is private and depends only on Flutter, kit_api, and its own reviewed pure-Dart dependencies:

name: example_kit
description: Example reviewed Kit module.
publish_to: none
version: 0.1.0

environment:
  sdk: ^3.12.2

dependencies:
  flutter:
    sdk: flutter
  kit_api:
    path: ../../packages/kit_api

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^6.0.0

Do not add camera, media picker, FFmpeg, notification, location, or other native plugins directly to a Kit. Request a narrow host capability when a real feature requires one.

Module Contract

Every compiled module implements KitModule:

abstract interface class KitModule {
  KitDescriptor get descriptor;

  Widget build(KitContext context);
}

Minimal implementation:

import 'package:flutter/material.dart';
import 'package:kit_api/kit_api.dart';

class ExampleKitModule implements KitModule {
  static const kitId = 'example-kit';

  @override
  KitDescriptor get descriptor => const KitDescriptor(
    id: kitId,
    compiledVersion: '1.0.0',
    displayName: 'Example',
    description: 'A short user-visible description.',
    supportedPlatforms: {KitPlatform.ios, KitPlatform.macos},
  );

  @override
  Widget build(KitContext context) => ExampleKitPage(context: context);
}

The host registers modules explicitly. There is no reflection, filesystem discovery, dynamic dependency resolution, or remote module loading.

Descriptor Contract

KitDescriptor is immutable compiled metadata:

FieldRequirement
idStable protocol and package identity, such as next-up. Never localized or reused.
compiledVersionExact module version expected by the server catalog for the current rollout.
displayNameShort fallback display name. Server-localized catalog content may override presentation text.
descriptionShort fallback description. Do not put secrets or policy in it.
supportedPlatformsPlatforms implemented and tested by this compiled package.
avatarBuilderOptional package-owned avatar builder using bundled, optimized assets.

Supported wire platform names are:

ios, android, macos, web, windows, linux

Platform support is declared twice:

  1. The package declares what its compiled implementation supports through KitDescriptor.supportedPlatforms.
  2. The server catalog declares where the current rollout is enabled.

Effective support is the intersection. Catalog policy may immediately narrow a rollout, but it can never enable a platform omitted by the compiled descriptor. A Kit must not appear in search, Plaza, Add, Open, or business APIs on an unsupported platform.

Identity And Handle

kit_id and handle have different purposes:

IdentifierPurpose
kit_idImmutable protocol/package identity used in registry, routes, storage namespaces, and migrations.
handlePublic search identity shown with @, stored without @.

Kit handles are lowercase, immutable, and globally unique across users, bots, groups, channels, and Kits. A disabled, retired, or removed Kit retains its handle so another identity cannot impersonate it. Display names may be localized; handles are never localized.

KitContext

The host builds a namespaced KitContext for one authenticated profile and one Kit:

class KitContext {
  final KitHttpClient http;
  final KitLocalStore localStore;
  final KitAttachmentCapability? attachments;
  final KitEmbeddedWebRuntimeCapability? embeddedWebRuntime;
  final Future<String> Function() timeZoneId;
  final VoidCallback onBack;
}

These are the capabilities available today. Planned capabilities are not part of the contract until they exist in kit_api and have host implementations, tests, and catalog policy.

Every Kit root app bar must expose onBack with a familiar back icon. The callback is owned by the host so wide macOS layouts keep the app navigation rail and return to Chats without a Kit creating its own product Navigator.

Authenticated HTTP

KitHttpClient sends requests only inside the current Kit's server namespace:

final response = await context.http.request(
  KitHttpMethod.post,
  'items',
  body: {
    'title': 'Prepare release notes',
    'operation_id': operationId,
  },
);

if (!response.ok) {
  final code = response.jsonMap['code'];
  // Map the stable code to a localized, actionable UI state.
}

The host adds authentication, the Kit id, and the current platform. A Kit must pass a relative path and must not construct the public API base URL, add an authorization header, or use a separate networking client to bypass the host.

KitHttpResponse.data is intentionally untyped at the transport boundary. Parse and validate it into Kit-owned DTOs before state reaches the UI.

Local Store

KitLocalStore is a small profile-and-Kit-isolated JSON string store:

await context.localStore.write('snapshot.v1', jsonEncode(snapshot));
final cached = await context.localStore.read('snapshot.v1');

Use it for recoverable cache, local preferences, and local-first snapshots. Do not use it as the only copy of durable server business data. Keys and values must not contain credentials. clear() affects only the current profile and current Kit namespace.

Bundled Embedded Runtime

embeddedWebRuntime is a platform-controlled capability for a very small set of reviewed official native Kits. It is not a browser, a remote Web Kit, or a general extension point. A module can request only a runtime id already compiled into the App; the host owns the WebView and may run only the exact signed-bundle asset manifest registered for that id.

The host verifies asset hashes before serving, denies remote network access, navigation, popups, downloads, permissions, cookies, and persistent Web storage, and exposes only a closed lifecycle/persistence bridge. The runtime receives no session token, generic HTTP client, user identifier, contacts, or chat data. Generated JavaScript and WebAssembly remain untrusted even when their build is reproducible, so containment is the security boundary.

This capability is unavailable to partner and third-party Kits without a separate platform security review. A Kit must render its typed unavailable state rather than constructing its own WebView or falling back to remote content.

Time Zone

Call timeZoneId() when calendar semantics need an IANA zone:

final zone = await context.timeZoneId();

Do not use a fixed UTC offset as calendar authority. The host may return UTC when a platform cannot provide a native IANA identifier, so server contracts must define a safe fallback.

Attachments

attachments is optional. Check both presence and availability before showing attachment controls:

final capability = context.attachments;
if (capability == null ||
    await capability.availability() != KitAttachmentAvailability.available) {
  // Hide the picker or render a typed unsupported state.
  return;
}

final picked = await capability.pickImages(limit: 4);
switch (picked) {
  case KitAttachmentSuccess<List<KitAttachmentSelection>>(
    value: final selections,
  ):
    // Queue the selected opaque handles for upload.
  case KitAttachmentError<List<KitAttachmentSelection>>(
    failure: final failure,
  ):
    // Render the appropriate cancelled, denied, or unavailable state.
}

Attachment selection ids are opaque and short-lived. A Kit never receives raw paths or picker objects. Upload and load return observable, cancellable operations:

final operation = capability.upload(selection);
final subscription = operation.progress.listen(updateProgress);
final result = await operation.result;
await subscription.cancel();
await capability.releaseSelection(selection.id);

Always release selections after success, cancellation, or failure. Handle every KitAttachmentFailure, including cancellation, permission denial, size limits, unsupported platforms, storage pressure, network failure, server rejection, and temporary unavailability.

Remote attachments must be represented by KitRemoteAttachment. Loading, sharing, and saving continue to pass through the host so authentication, local archive isolation, and platform behavior remain centralized.

Client State And UI

Routing

Each Kit opens at a stable host-owned route:

/kits/:kitId

The host performs catalog, audience, Add state, platform, registry, and exact compiled-version checks before calling module.build(context). A module must not create a second product router or mutate the app root navigator.

The Kit owns navigation inside its feature surface only where the host contract permits it. Product-level back, close, wide-layout presentation, and deep-link handling remain host responsibilities.

Chats Integration

An added Kit appears in the same Chats timeline as conversations. It uses the ordinary row, pin, selection, and remove interactions, with a Kit badge as its identity distinction. It remains a typed Kit item internally and is not placed in SpacesStore or converted to a fake SpaceSummary.

Opening updates last_opened_at; pinning is per user. Neither operation changes Kit business data.

Add, Remove, And Data Deletion

Never label Add or Remove as install or uninstall. No executable package is installed at runtime.

UI Requirements

Server Module

Ownership Boundary

One Kit has one business service. UI routes and any future reviewed adapters must call that service rather than duplicate validation or write D1 directly.

Kit server modules may import only the narrow kit_server_api surface and approved pure utilities. They must not import another Kit or unrelated Worker business domains.

The shared Worker owns:

The Kit owns:

Route Convention

Kit business endpoints live below:

/kits/:kitId/...

The host authenticates the request and calls assertKitAccess before the Kit handler. Access requires an enabled native catalog row, admitted audience, supported platform, and an existing Add record. Restricted Kits should return the same not-found response for unknown and unauthorized callers so catalog existence is not leaked.

Do not accept owner_sub, actor role, catalog status, or capability grants from request JSON. Derive them from the authenticated request context and catalog.

Validation And Errors

Validate all request bodies and query parameters at the route boundary with a schema parser. Return a stable JSON envelope:

{
  "ok": false,
  "code": "ITEM_CONFLICT"
}

Use appropriate HTTP status codes. Examples:

StatusMeaning
400Invalid validated input or required confirmation missing.
401Missing or invalid authenticated session.
403Kit not added, platform unsupported, maintenance, or unavailable.
404Unknown/inaccessible Kit or owner-scoped business entity.
409Revision conflict or another deterministic state conflict.
413Kit-owned bounded storage or payload limit exceeded.
429Rate limited.
503Retryable service or finalization failure.

Never expose stack traces, storage keys, raw internal user ids belonging to other users, or internal exception messages.

Idempotency And Conflicts

Every retriable mutation carries an operation_id of at most 128 characters. The server checks operation replay before checking the current revision. This ordering is required when a mutation succeeded but its response was lost.

Updates to mutable records should use expected_revision and return a safe, owner-scoped current snapshot on conflict:

{
  "ok": false,
  "code": "ITEM_CONFLICT",
  "item": {
    "id": "item_opaque",
    "revision": 7
  }
}

The client retains the same operation id after transport failure or a retryable response. It removes the pending operation after a definitive rejection such as 400, 403, 404, or 409.

D1 And R2

Kit tables live in the shared D1 database but use Kit-specific names and owner-scoped indexes. Migrations join the repository's single forward-only sequence; a Kit does not create an independent migration stream.

R2 namespaces require an explicit lifecycle policy:

R2 and D1 are not one transaction. Model upload, commit, logical deletion, and physical deletion as explicit retryable states rather than pretending they are atomic.

Catalog And Availability

The server catalog is declarative metadata and policy, not executable content. It includes fields such as:

{
  "kit_id": "example-kit",
  "handle": "example",
  "kind": "native",
  "display_name": "Example",
  "description": "A short catalog description.",
  "category": "Utilities",
  "compiled_version": "1.0.0",
  "supported_platforms": ["ios", "macos"],
  "status": "disabled"
}

Status meanings:

StatusBehavior
disabledHidden from discovery; Add, Open, and business APIs reject access.
enabledEligible users may discover, Add, open, and use the Kit.
maintenanceExisting entry renders unavailable; business mutations reject.
retiredNo new Adds; existing users can see retirement and Remove the entry.

A Kit is effectively available only when:

catalog status is enabled
AND catalog kind is native
AND audience admits the authenticated user
AND client build meets the catalog minimum for the current platform
AND app registry contains kit_id
AND compiled versions match exactly
AND current platform appears in both declarations

The minimum build is only a compatibility and presentation filter. Platform and build values are reported by the client and must never authorize access. The signed binary's compiled registry determines whether the client contains a Kit, while authenticated server routes independently enforce audience and business permissions.

Ship new native code disabled or with an internal audience first. Enable it only after a compatible binary is available and declared platforms pass their test gates. The catalog kill switch must be able to disable a Kit without an app release.

Security And Privacy Checklist

Testing Requirements

Every Kit must provide automated coverage at the lowest appropriate layer.

Package Tests

Server Tests

Host Integration Tests

Manual Release Gate

Run the complete user flow on every declared platform: discovery or search, Add, cold launch, primary mutations, offline/retry, wide/narrow navigation, Remove, re-Add, Clear local cache, and Clear server data. A successful compile alone is not a platform support claim.

Review And Release Process

Official Kit development follows this sequence:

  1. Write or update the product and security RFC.
  2. Create the independent package, tests, server service, and migrations.
  3. Add an explicit host registry entry and disabled catalog record.
  4. Declare only platforms with complete implementation and validation.
  5. Pass package, server, host integration, migration, and platform build gates.
  6. Ship the binary while the Kit remains disabled or audience-restricted.
  7. Run real-device validation, then enable catalog visibility.
  8. Monitor errors and retain a server-side emergency disable path.

Approved partners will follow a private source-intake and review process. They will not upload precompiled binaries to user devices. KYC, contracts, dependency and license review, SBOM, secret scanning, forbidden API checks, capability review, signing, and app-store release remain platform-controlled.

The partner intake workflow and standalone integration test app do not exist yet. Contact the platform team before beginning an external Kit. Do not infer a submission API from this Preview guide.

Current Limitations

These limitations are intentional. New capabilities are added only for a real, reviewed Kit through a narrow typed interface with availability, cancellation, failure, lifecycle, privacy, and fake-test semantics defined together.