
Overview
A Tesslate App is a distributable package built on top of an OpenSail project. A creator develops a workspace normally, publishes it as a versionedAppVersion, and end users install it from the marketplace. Each install mints a fresh Project(app_role="app_instance") with its own Hub volume, containers, permissions, and billing relationship. The infrastructure for Apps sits on top of the same Project, Container, and Volume primitives you already use, with extra layers for versioning, approval, billing, and distribution.
Immutable versions
Each publish is an append-only
AppVersion with a content-addressed bundle hash. Never mutated, always rollback-safe.Four-stage approval
Intake, automated scan, sandbox eval, and human review before a public install works.
Flexible billing
Three dimensions (AI compute, general compute, platform fee) with creator / platform / installer / BYOK payers.
Concepts
Backing columns live in
orchestrator/app/models.py. The full service map is in docs/apps/CLAUDE.md.
Build your workspace
1
Create a project
Start from the dashboard or
POST /api/projects. Use chat to describe what you want, or import code via the Desktop client’s connected directories.2
Add containers
Each container is a separate service (web, api, db, worker). The AI agent wires them through
setup-config by reading .tesslate/config.json.3
Let the Librarian write config.json
The Librarian agent analyzes the project and generates
.tesslate/config.json with containers, startup_command, and connections.4
Declare container dependencies
Use
env_injection edges so the manifest knows, for example, that the api container needs DATABASE_URL from the db container. See seeds/apps/crm-with-postgres/app.manifest.json for a full worked example.5
Test locally
Start the project, hit the primary container URL, exercise schedules, verify secrets resolve.
6
Promote to app source
Set
app_role = "app_source". The Creator Studio UI does this on first publish.The app manifest
The manifest is a JSON document describing everything a fresh installer needs: containers, surfaces, billing, scopes, schedules, MCP tool schemas. See the authoritative 2025-02 spec on GitHub. The schema file isorchestrator/app/services/apps/app_manifest_2025_02.schema.json.
Top-level shape
Minimal single-container app
Common fields
slugis kebab-case, globally unique, and immutable after first publish.versionis strict semver. Each publish must be strictly greater than the priorAppVersionfor the same app.primary: truemarks the container whose URL backsui/chatsurfaces. Exactly one container must be primary whencontainers[]is non-empty.- Secret-ref env values use the convention
"${secret:<name>/<key>}". They resolve at pod-spec time viaenv_resolver.py. connectors[]declares OAuth scopes the installer must consent to.eval_scenarios[]supplies at least three happy-path prompts per entrypoint for public listings (used by Stage 2 sandbox eval).
Headless apps (cron- or webhook-only) set
surfaces: []. The workspace falls through to the Schedules tab as the primary layout.Submit for publishing
From Creator Studio (app/src/pages/CreatorStudioPage.tsx) open your source project and click Publish version, or drive the REST API:
orchestrator/app/services/apps/publisher.py) runs atomically:
1
Parse and validate
Manifest is validated against the frozen 2025-02 JSON Schema.
2
Compatibility check
compatibility.check() asserts the running server supports every declared required_features entry.3
Get or create MarketplaceApp
By slug. First publish creates the public identity anchor.
4
Guard duplicates
(app_id, version) must be unique.5
Publish the bundle
Bundle is written to Volume Hub’s CAS and
bundle_hash captured.6
Insert rows
AppVersion(approval_state="pending_stage1") and AppSubmission(stage="stage0") enter the review pipeline.listing.visibility: "private" or "team:<uuid>") still pass through Stage 0 and Stage 1 for structural checks, but never surface to the public marketplace.
Approval pipeline
Every public submission walks a four-stage state machine. Transitions are enforced byorchestrator/app/services/apps/submissions.py (VALID_TRANSITIONS).
On Stage 3 approval,
AppVersion.approval_state becomes stage2_approved and MarketplaceApp.state becomes approved. The version is installable from the public marketplace.
Stage 1 writes one
SubmissionCheck row per check, so creators can see exactly which check failed. Admins manage the adversarial suite itself from AdminAdversarialSuitePage.tsx.Billing configuration
Billing has three independent dimensions. Each declares a payer and caps independently. The resolved payer for each spend event is thewallet_mix entry on the AppInstance, negotiated at install time.
A
promotional_budget lets the creator fund AI costs up to fund_usd. When exhausted, the payer automatically flips to the installer so the app keeps working.settlement_worker.settle_spend_batch with SELECT ... FOR UPDATE SKIP LOCKED for safe concurrency. The platform keeps markup_pct (default 10%) and credits the creator the rest. BYOK on ai_compute records a no-op SpendRecord with reason='byok_no_op'.
Creators set billing from app/src/pages/CreatorBillingPage.tsx. Installers see the resolved numbers in AppInstallWizard.tsx before installing.
Installing apps
One-click install launches theAppInstallWizard (app/src/components/apps/AppInstallWizard.tsx). It collects:
- Team under which to install.
- OAuth consents for every
connectors[]entry withoauth: true. wallet_mixconsent (which payer is accepted per dimension).- MCP scope consent per declared MCP server.
- Update policy (
auto,manual,pinned).
POST /api/apps/installs reaches installer.install_app(), which:
- Verifies the AppVersion is in
stage1_approvedorstage2_approved. - Re-runs compatibility against the current server.
- Dedupes against an existing active
AppInstance. - Calls
hub_client.restore_bundle()to materialize a new volume from the CAS bundle. AnAppInstallAttempt(state="hub_created")row is written before the DB commit so the background reaper (install_reaper.py) can clean up crashed installs. - Inserts a new
Project(app_role="app_instance")plus oneContainerper entry incompute.containersandContainerConnectionrows forcompute.connections. - Inserts the
AppInstance, attachesMcpConsentRecordrows, flips the attempt tocommitted.
{app_instance_id, project_id, volume_id, node_name}. Containers start through the normal orchestrator factory at orchestrator/app/services/orchestration/factory.py. Installed apps live in app/src/pages/MyAppsPage.tsx. The running UI is hosted by app/src/components/apps/IframeAppHost.tsx with a signed URL scoped to the install.
Bundles
Bundles group multiple AppVersions that should install together (for example the Tesslate Starter Pack: a CRM + nightly digest + center dashboard that embeds the others).- Model:
AppBundle+AppBundleItem. - Service:
orchestrator/app/services/apps/bundles.py. - Router:
orchestrator/app/routers/app_bundles.py. - UI:
app/src/pages/BundleDetailPage.tsxplusapp/src/components/apps/BundleInstallWizard.tsx.
consolidated_manifest_hash hashes member manifest hashes in sorted order, so semantically identical bundles deduplicate. The install wizard shows a single consolidated consent screen covering every member’s OAuth scopes. The center-dashboard pattern uses WorkspaceSurface.tsx to render an app that embeds its bundle siblings via signed iframes.
Forking
If the manifest declaresforkable: "true", any user can clone the app:
orchestrator/app/services/apps/fork.py restores the source bundle to a new Hub volume, inserts a MarketplaceApp(state="draft", forked_from=...) row, and hands the forker a Project(app_role="app_source") they can edit and republish under their own slug. The marketplace surfaces fork lineage on AppDetailPage.tsx.
Yanking
If a version needs to be pulled, anyone with creator or admin rights can file a yank request viaorchestrator/app/services/apps/yanks.py and orchestrator/app/routers/app_yanks.py.
Approved yanks flip
AppVersion.yanked_at and state = "yanked". Running installs stop being able to mint new runtime keys via orchestrator/app/services/apps/runtime.py, which refuses yanked and deprecated apps.
Creators can file a YankAppeal against a decision. Admins review at app/src/pages/AdminYankCenterPage.tsx.
Creator reputation
MarketplaceApp.reputation is a JSON blob refreshed by orchestrator/app/services/apps/monitoring.py and its sweep worker monitoring_sweep.py. Inputs:
- Star ratings and install count.
- Review score from post-install feedback.
- Uptime and crash metrics pulled from the compute tier.
- Approval history (rejections, prior yanks, appeal outcomes).
orchestrator/app/routers/creators.py) and in admin tooling at app/src/pages/AdminCreatorReputationPage.tsx.
SDK usage
For apps that call back into OpenSail (publish from CI, open a session against an install, invoke an MCP tool), use the versioned SDKs inpackages/tesslate-app-sdk/.
tsk_...) via Authorization: Bearer. CSRF is not needed because cookie sessions are not used. For the agent runtime that powers chat and hosted-agent surfaces, read the tesslate-agent docs.
MCP surface
Any app can expose one or more of its entrypoints as MCP tools callable by other agents.tool_schema is JSON Schema for the tool input. Stage 1 scans the declared MCP scopes; Stage 2 exercises the tool against the adversarial suite. At install time the user consents to scopes via AppInstallWizard.tsx and consent is recorded in McpConsentRecord. Runtime bridging lives in orchestrator/app/services/mcp/ (client, bridge, manager).
stateless or shared-db state models are recommended for MCP tools because they are called concurrently across invocations.Scheduled triggers and webhooks
Everyschedules[] entry gets an AgentSchedule row scoped to the install. Two dimensions combine into four shapes:
Webhook authentication uses HMAC-SHA256 of the request body against
trigger_config.webhook_secret in header X-Tesslate-Signature.
Ingestion is fast: schedule_triggers.ingest_trigger_event() does a single INSERT into ScheduleTriggerEvent. process_trigger_events_batch() drains them into ARQ tasks using SELECT ... FOR UPDATE SKIP LOCKED. See orchestrator/app/services/apps/schedule_triggers.py and the routers app_schedules.py / app_triggers.py.
Installers manage schedules from the My Apps UI; creators declare defaults and editability in the manifest. The nightly_digest seed at seeds/apps/nightly_digest/app.manifest.json is a full headless cron example (surfaces: [], one cron schedule).
Admin review
Tesslate staff drive the approval queue from these pages, all behind superuser auth on the router side (orchestrator/app/routers/admin_marketplace.py).
Where to next
Deployment Targets
Ship a hosted build to Vercel, Netlify, Cloudflare, and 19 other providers.
Communication Gateways
Put your installed app into Slack, Telegram, Discord, WhatsApp, or Signal.
Desktop Install
Distribute your app on the Tauri desktop shell.
Need the agent authoring reference for hosted-agent surfaces? See the tesslate-agent docs. Looking to trigger an installed app from outside OpenSail? See the external agent API at
orchestrator/app/routers/external_agent.py.