
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
| Term | What it is |
|---|---|
| Workspace | A regular Project you develop in. When you turn it into an app source it becomes Project(app_role="app_source"). |
| AppVersion | An immutable, content-addressed snapshot (manifest + bundle hash). Published once, never mutated. |
| MarketplaceApp | The public identity anchor: slug, handle, category, reputation, approval state. One app has many AppVersions. |
| AppInstance | A single user’s installed copy. Installing mints a new Project(app_role="app_instance") with its own volume, containers, and permissions. |
wallet_mix | Per-install JSON declaring who pays for each billing dimension. Negotiated at install time. |
update_policy | auto, manual, or pinned. Controls whether new approved versions are pulled automatically. |
| Surface | The entrypoint kind the app exposes: ui (iframe), chat (scoped agent session), scheduled (cron), triggered (webhook or event), mcp-tool (callable by other agents). A headless app declares surfaces: []. |
orchestrator/app/models.py. The full service map is in docs/apps/CLAUDE.md.
Build your workspace
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.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.Let the Librarian write config.json
The Librarian agent analyzes the project and generates
.tesslate/config.json with containers, startup_command, and connections.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.Test locally
Start the project, hit the primary container URL, exercise schedules, verify secrets resolve.
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:
Compatibility check
compatibility.check() asserts the running server supports every declared required_features entry.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).
| Stage | What happens | Service |
|---|---|---|
| Stage 0 (intake) | Submission row created, bundle persisted. | publisher.py |
| Stage 1 (automated scan) | Re-parses manifest, confirms declared features are supported, checks MCP scopes against the safe-list, confirms disclosure and billing payers. Hard fails reject; warnings do not. | stage1_scanner.py |
| Stage 2 (sandbox eval) | Runs the app against an AdversarialSuite with a cheap model. Scores crashes, cost blowouts, prompt-injection resistance. Requires score >= STAGE2_SCORE_THRESHOLD (0.5). | stage2_sandbox.py |
| Stage 3 (human review) | OpenSail admins sign off in the Admin Marketplace Workbench. | admin_marketplace.py |
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.
| Dimension | Payer options | Default on_cap |
|---|---|---|
ai_compute | creator, platform, installer, byok | pause |
general_compute | creator, platform, installer (no BYOK) | degrade |
platform_fee | subscription, one-time, free | pause |
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.
forkable value | Meaning |
|---|---|
"true" | Anyone can fork |
"restricted" | Creator approval required per fork request |
"no" | Forks disabled |
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.
| Severity | Admin approvals | Typical reason |
|---|---|---|
low | One admin | Cosmetic bug |
medium | One admin | Functional bug |
critical | Two distinct admins | Security or legal. Enforced by DB CHECK ck_yank_critical_two_admin and service-layer NeedsSecondAdminError. |
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:
trigger_kind | execution | Semantics |
|---|---|---|
cron | job | Cron fires, a V1Job runs the entrypoint command in the primary container image. |
cron | http-post | Cron fires, POST to ${primary_url}${entrypoint} with an invocation-key auth header. |
webhook | job | External POST to /api/app-instances/{id}/trigger/{name} kicks off a job. |
webhook | http-post | External POST is forwarded to the primary container endpoint. |
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).
| Page | Purpose |
|---|---|
AdminSubmissionWorkbenchPage.tsx | Stage advance and per-check triage |
AdminMarketplaceReviewPage.tsx | Stage 3 final review |
AdminYankCenterPage.tsx | Yank requests, appeals, two-admin rule for critical severity |
AdminAdversarialSuitePage.tsx | Stage 2 eval suite management |
AdminCreatorReputationPage.tsx | Creator reputation overrides and audits |
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.