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:
- calendars, task boards, trackers, editors, or study sessions;
- native forms, multi-step navigation, local interaction, and dense layouts;
- app-owned capabilities such as controlled attachments or local persistence;
- a feature users Add once and reopen from Chats.
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
| Concept | Meaning |
|---|---|
| Kit | A user-visible interactive feature compiled into the app. |
| Kit module | The Dart package that implements one Kit's UI and client behavior. |
| Kit host | App-owned code that controls registration, routing, identity, platform access, and capabilities. |
| Kit server module | Kit-owned server service and route adapters composed into the shared authenticated Worker. |
| Descriptor | Compiled metadata declared by the module, including version and supported platforms. |
| Catalog | Server-controlled metadata, audience, platform rollout, status, and kill switch. |
| Add | Put a Kit entry in the current user's Chats list. It does not download code. |
| Remove | Remove the Chats entry while preserving local cache and server business data. |
| Clear data | A separate, explicit destructive action owned by the Kit. |
Non-Negotiable Rules
- Native Kit source is reviewed and compiled into a normal app release.
- Never download or execute Dart, native code, templates, or unrestricted bridge definitions from the catalog.
- A Kit must not import
app/lib, another Kit, chat internals, global providers, raw platform plugins, or app-wide service locators. - A Kit never receives the session bearer token, unrestricted filesystem paths, picker objects, or another Kit's local data.
- The app registry and server catalog must both permit a Kit before it opens.
- Every Kit server request is authenticated and authorized again on the server.
- Client-supplied user or owner identifiers are never authorization input.
- Add, Remove, Clear local cache, and Clear server data are distinct operations.
- A Kit is not represented as a fake chat or
SpaceSummary. - Expected failures use typed results or stable error codes. Do not rely on parsing human-readable messages.
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:
| Field | Requirement |
|---|---|
id | Stable protocol and package identity, such as next-up. Never localized or reused. |
compiledVersion | Exact module version expected by the server catalog for the current rollout. |
displayName | Short fallback display name. Server-localized catalog content may override presentation text. |
description | Short fallback description. Do not put secrets or policy in it. |
supportedPlatforms | Platforms implemented and tested by this compiled package. |
avatarBuilder | Optional package-owned avatar builder using bundled, optimized assets. |
Supported wire platform names are:
ios, android, macos, web, windows, linux
Platform support is declared twice:
- The package declares what its compiled implementation supports through
KitDescriptor.supportedPlatforms. - 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:
| Identifier | Purpose |
|---|---|
kit_id | Immutable protocol/package identity used in registry, routes, storage namespaces, and migrations. |
handle | Public 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
- Add creates a per-user Chats entry and makes Kit business APIs available.
- Remove deletes only that Chats entry. Re-adding restores existing server data and local cache.
- Clear local cache clears only the current device profile's Kit cache.
- Clear my data is an explicit, confirmed Kit-owned operation that removes the authenticated user's server business data and relevant local state.
Never label Add or Remove as install or uninstall. No executable package is installed at runtime.
UI Requirements
- Support app light/dark theme behavior and text scaling.
- Localize user-visible strings; never localize protocol ids or handles.
- Keep fixed-format controls responsive without viewport-based font scaling.
- Use app-consistent navigation, dialogs, loading, error, and destructive confirmation patterns.
- Render explicit loading, empty, unavailable, conflict, retry, and offline states. Do not leave a blank feature surface.
- Optimize bundled avatars and media for their rendered sizes. Use decode size hints for large raster assets.
- Supply Kit avatars as square, edge-to-edge artwork. Do not bake rounded corners into the source image: the app host applies the canonical rounded square clip on Chats, search, Plaza, and other identity surfaces. Keep key artwork inside a conservative safe area so it remains legible at 48 px.
- Test narrow and wide layouts for every declared desktop/mobile platform.
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:
- bearer authentication and server-derived actor identity;
- catalog, audience, status, platform, and Add checks;
- public route dispatch;
- rate limits, operation replay primitives, and audit boundaries;
- the single deployment and ordered D1 migration history.
The Kit owns:
- business schemas and validation;
- owner-scoped queries and state transitions;
- conflict snapshots and stable business error codes;
- Kit-specific R2 object semantics and deletion reconciliation;
- service and route-adapter tests.
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:
| Status | Meaning |
|---|---|
400 | Invalid validated input or required confirmation missing. |
401 | Missing or invalid authenticated session. |
403 | Kit not added, platform unsupported, maintenance, or unavailable. |
404 | Unknown/inaccessible Kit or owner-scoped business entity. |
409 | Revision conflict or another deterministic state conflict. |
413 | Kit-owned bounded storage or payload limit exceeded. |
429 | Rate limited. |
503 | Retryable 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:
- temporary staging objects use a short TTL and a reconciler;
- ordinary messaging retention must not accidentally match Kit data prefixes;
- retained Kit objects without TTL require a hard quota, explicit user deletion, retryable physical deletion, and orphan reconciliation;
- an R2 key is never sufficient authorization to read an object.
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:
| Status | Behavior |
|---|---|
disabled | Hidden from discovery; Add, Open, and business APIs reject access. |
enabled | Eligible users may discover, Add, open, and use the Kit. |
maintenance | Existing entry renders unavailable; business mutations reject. |
retired | No 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
- [ ] Module source is reviewed and compiled into the app.
- [ ] Package does not import
app/lib, another Kit, or raw native plugins. - [ ] Descriptor id, version, and supported platforms match catalog policy.
- [ ] Server derives actor identity and checks
assertKitAccesson every route. - [ ] Every business query is owner-scoped or has an explicit shared-data policy.
- [ ] Request bodies and query parameters use schema validation.
- [ ] Mutations are retry-safe with operation replay before revision checks.
- [ ] Restricted audience membership and object keys are not leaked.
- [ ] Logs, analytics, and crash reports exclude user business content and secrets.
- [ ] Local state is isolated by app profile and Kit id.
- [ ] R2 prefixes have explicit TTL or bounded-retention policy.
- [ ] Clear-data behavior covers D1, R2, local cache, retries, and failure recovery.
- [ ] Unsupported platforms cannot discover, Add, open, or call the Kit.
- [ ] Any bundled runtime is platform-registered, hash-verified, offline-only, ephemeral, and covered by a dedicated security review.
Testing Requirements
Every Kit must provide automated coverage at the lowest appropriate layer.
Package Tests
- descriptor identity, compiled version, and supported platform declarations;
- controller/state transitions using fake
KitHttpClientandKitLocalStore; - local-first cache, refresh, offline, retry, and conflict behavior;
- typed capability denial, cancellation, and failure behavior;
- widget tests for loading, empty, data, conflict, unavailable, and destructive confirmation states;
- narrow and wide layout behavior for each declared platform.
Server Tests
- unknown, disabled, unsupported, and unauthorized Kit access;
- audience/allowlist enforcement without existence leakage;
- actor identity cannot be forged in request JSON;
- owner isolation for every read and mutation;
- operation replay, lost-response retry, and revision conflict ordering;
- migration, deletion, quota, upload finalization, and reconciliation behavior;
- R2 object reads require live business authorization.
Host Integration Tests
- registry and catalog version mismatch never opens a module;
- package and catalog platform declarations are intersected;
- search, Add, opening, pinning, Remove, and re-Add preserve expected data;
- Kit list items do not enter
SpacesStoreor acquire fake unread/message state; - profile switching never exposes another profile's cache or attachments;
- emergency disable blocks the route and server APIs.
- bundled runtime tests deny external navigation/network, verify exact asset hashes, and release listeners, WebViews, audio, and orientation on teardown.
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:
- Write or update the product and security RFC.
- Create the independent package, tests, server service, and migrations.
- Add an explicit host registry entry and disabled catalog record.
- Declare only platforms with complete implementation and validation.
- Pass package, server, host integration, migration, and platform build gates.
- Ship the binary while the Kit remains disabled or audience-restricted.
- Run real-device validation, then enable catalog visibility.
- 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
- No public Kit submission portal or package registry.
- No standalone developer integration app or scaffold command.
- No dynamic Dart/native package download.
- No generic Kit-to-bot action gateway.
- No unrestricted app navigation, contacts, chat history, microphone, camera, transcoding, notification, payment, or arbitrary network capabilities.
- Web Kits require a separate security model and are not enabled by this guide.
- Bundled Web runtimes are reviewed official exceptions, not a public Kit API.
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.