@odla-ai/apps
v0.8.1
Published
The odla platform management SDK — create apps, toggle services (db, o11y, blog, ai, calendar), and delete apps via the registry. Control plane only; data-plane SDKs like @odla-ai/db are what running apps link.
Maintainers
Readme
@odla-ai/apps
⚠️ Early access — pre-1.0. Agents work from bounded runbooks; humans approve credentials, production changes, releases, and merges. APIs and exact package availability can change. Review the documented guarantees and limitations; this software is MIT-licensed and provided without warranty.
The odla platform management SDK — the control-plane client for the app
registry, the way gcloud is to GCP. Use it from agents, CLIs, and CI to
create apps, toggle services (db, o11y, blog, ai, calendar, …), and delete apps.
It is not a data-plane SDK: apps talk to their database with @odla-ai/db,
emit telemetry with @odla-ai/o11y, and so on — those SDKs are what a running
app links; this one is what manages apps.
An app is the platform's project primitive. Services keep their own
credentials and data planes; the registry answers exactly three questions:
does the app exist, who owns it, and which services are enabled. Registration
is platform-level and à la carte — an app can enable blog without ever
enabling db.
Install
npm i @odla-ai/appsUse from a Worker (service binding)
import { createAppsClient } from "@odla-ai/apps";
const apps = createAppsClient({ fetcher: env.APPS, token: env.APPS_TOKEN });
const app = await apps.resolveApp("my-app");
if (!(await apps.isEnabled("my-app", "o11y", { env: "dev" }))) {
return new Response("o11y not enabled for this app", { status: 403 });
}Use over HTTP
const apps = createAppsClient({ endpoint: "https://odla-apps.example.workers.dev", token: MACHINE_TOKEN });
const created = await apps.createApp({ name: "My App", appId: "my-app" });
await apps.setService(created.appId, "o11y", true, { env: "dev" });
// Record where each environment's deployment RUNS — shown on the app in
// Studio and readable via the public-config endpoint. Null clears it.
await apps.setLink("my-app", "prod", "https://my-app.example.com");
const app = await apps.resolveApp("my-app");
app?.links.prod; // "https://my-app.example.com/"
// Enable + configure the ai service (non-secret half: provider + default
// model, surfaced on public-config). The provider API KEY goes in the
// platform vault — odla-db tenant secrets — never in the registry.
await apps.setAi("my-app", "prod", { provider: "anthropic", model: "claude-sonnet-5" });
// Configure and connect the read-only Google Calendar mirror. This call stores
// non-secret intent only; a human opens the returned consentUrl.
await apps.setCalendar("my-app", "dev", {
provider: "google",
access: "read",
calendars: ["primary"],
match: { organizerSelf: true, requireAttendees: true },
attendeePolicy: "full",
bookingPageUrl: "https://calendar.google.com/calendar/appointments/schedules/…",
});
const attempt = await apps.beginGoogleCalendarConnect("my-app", "dev");
console.log(attempt.consentUrl);
const status = await apps.getCalendar("my-app", "dev");For an interactive chooser, call
beginGoogleCalendarConnect(appId, env, { deferInitialSync: true }). After
consent, state === "needs_sync" and connected === true: calendar discovery
works, but no default event has been mirrored. Confirm settings with
setCalendar; that owner route starts the first sync. Preconfigured automation
can omit the option and auto-sync after consent.
Calendar OAuth is a separate human checkpoint. The management SDK receives
only an opaque attempt and authorization URL; Google codes, access tokens, and
refresh tokens stay inside the platform connector. Use
listGoogleCalendars, resyncCalendar, and disconnectCalendar for the
owner-safe lifecycle. The intentionally public booking-page URL is
configuration, not a credential.
This management surface configures and operates the current read/embed
reconciliation slice only. It has no native booking-write UI/API, rolling
window or age-out controls, bounded quota-backoff policy, mirror
retention/purge workflow, provider-derived apply mutation ids, or per-calendar
public health. Passing { purge: true } to disconnectCalendar is rejected;
public config exposes only aggregate connection/freshness state.
disconnectCalendar stops watches and deletes only that app/environment
connection's encrypted platform token, cursors, and connector state. It does
not revoke the shared user-to-Google-OAuth-project grant, because a project-wide
revoke could invalidate the user's other odla connections. A future explicitly
global revoke operation would need cross-connection accounting and warning.
New automation should always name the environment. For backward compatibility,
passing services to createApp enables them in both default environments,
and omitting env from setService/isEnabled targets the legacy prod alias.
Those shortcuts are intentionally not the dev-first golden path.
Credential caveats (agents provisioning with a dev token)
resolveApp()works with machine and developer tokens: it reads the/svcsurface and falls back to the operator-scoped registry read when denied, so "null" reliably means the app does not exist (and a blindcreateAppon an existing appId is a 400 you will not hit).- Minting a db key (
POST /admin/apps/:tenant/keys) is additive: existing keys keep working, so provisioning reruns never break deployed Workers. setAuth(appId, env, { publishableKey })needs only the Clerk publishable key; the issuer is derived and applied to that env's db tenant. Workers should fetch the resulting public-config at runtime (short cache) instead of baking auth into env vars — key rotation then needs no redeploy.
Provisioning contract
Service Workers implement POST /svc/enablement (PROVISION_PATH); the
registry calls it when an operator toggles the service for an app, and stores
the returned service-owned config (e.g. an o11y serviceId). Hosted services
are enabled only after their controller acknowledges the operation. Missing
bindings/credentials and unknown services fail closed; ai and blog are
explicit registry-only capabilities. Deletion calls destroy for every
provisioned environment, including disabled services whose data was retained.
Partial failures leave the app record in place and return structured operation
status so an idempotent retry can finish cleanup. calendar is a
registry-owned connector: enablement stores non-secret intent, disable stops
watches while retaining connector state, and destroy stops watches and deletes
that app/environment's encrypted local token/cursors/state without revoking the
shared Google project grant. Neither operation purges odla-db mirror rows. The
SDK preserves structured failures on AppsError.details.
