PitchStation Apps Apps home Markdown for agents
PitchStation Apps · Architecture & separation design

Built as a module, ready to stand alone

Apps started inside PitchStation because the first users were PitchStation users with prototypes to show. Its scope is growing, and it may later run as a separately hosted service or become a platform of its own. This page records how it is built today, the exact places it leans on the rest of PitchStation, and the staged design that lets it leave without breaking an app, a URL or a client.

1 The pieces2 What Apps borrows3 Principles4 Ports5 Staged path6 What stays stable7 Open decisions8 Milestone 29 Milestones 3 and 4

§01The pieces

Apps componentsClients — the web page, the create-pitchstation-app skill, the six MCP app tools, the Apps client and your own REST calls — reach the Apps control plane only through /api/apps over HTTPS, with the origin from PITCHSTATION_URL. The control plane, inside PitchStation today, has the API, the operation worker, its SQLite state and the host adapter, and borrows identity, the target registry and SSH transport, the DEPLOY_SECRET key, the hostname namespace, owner alerts, the database file, the web shell and docs, and the edge from the rest of PitchStation. It drives each app host over SSH. Each host runs nginx with one vhost per hostname, certbot with a renew hook, the policy sidecar on a loopback port, the release folders and one Compose project per app on loopback ports. The PitchStation code on a host is the policy verifier, the host reporter, the renew hook and, on restricted hosts, the egress unit. Clients /apps.html (web)create-pitchstation-appskillMCP: 6 app toolspitchstation-apps.cjs(client + packager)your REST callsOpenAPI / GPT Action all speak HTTPS to/api/apps only, originfrom PITCHSTATION_URL Apps control plane (inside PitchStation today) APIOperation workerState (SQLite)Host adapter routes/apps.jsroutes/app-publishing.jsapp-targets: owner or pool durable operations, leasesfences, retry, cancelreadiness + monitor apps, releases, configs,plans, hosts, operations,leases, hostname claimsin data/website-deploy.db compose deploy plannginx per-host vhost + TLSpolicy runtime (pinned)atomic apply / restore Borrowed from PitchStation (the seams) identity + app.deploy · target registry + SSH transport (Websites module)DEPLOY_SECRET encryption · hostname namespace shared with Websitesowner email alerts (notifier) · database file · web shell, docs, downloadsedge: /api/* reaches it through the CDN in front of production Each app host nginx: neutral default +one vhost per hostnamecertbot (webroot) + renew hookpolicy sidecar ps-app-policy127.0.0.1:19780, pinned image/etc/pitchstation/apps/<id>/versions/ + current (relative)/srv/apps/<slug>/releases/compose project ps-<slug>127.0.0.1:41000–41999PitchStation code here:the policy verifier, thehost reporter, the renew hookand, on restricted hosts,the egress unit. Over SSH. SSH Apps componentsClients — the web page, the create-pitchstation-app skill, the six MCP app tools, the Apps client and your own REST calls — reach the Apps control plane only through /api/apps over HTTPS, with the origin from PITCHSTATION_URL. The control plane, inside PitchStation today, has the API, the operation worker, its SQLite state and the host adapter, and borrows identity, the target registry and SSH transport, the DEPLOY_SECRET key, the hostname namespace, owner alerts, the database file, the web shell and docs, and the edge from the rest of PitchStation. It drives each app host over SSH. Each host runs nginx with one vhost per hostname, certbot with a renew hook, the policy sidecar on a loopback port, the release folders and one Compose project per app on loopback ports. The PitchStation code on a host is the policy verifier, the host reporter, the renew hook and, on restricted hosts, the egress unit. Clients /apps.html (web)create-pitchstation-app skillMCP: 6 app toolspitchstation-apps.cjs (client, packager)your REST calls, OpenAPI / GPT Action all speak HTTPS to /api/apps only,origin from PITCHSTATION_URL HTTPS API routes/apps.jsroutes/app-publishing.jsapp-targets: owner or pool Operation worker durable operations, leasesfences, retry, cancelreadiness and monitor State (SQLite) apps, releases, configs, plans, hosts,operations, leases, hostname claimsin data/website-deploy.db Host adapter Compose deploy plannginx per-host vhost and TLSpolicy runtime (pinned)atomic apply and restore Borrowed from PitchStation identity and the app.deploy capabilitytarget registry and SSH transport(the Websites module) · DEPLOY_SECRETencryption · the hostname namespaceshared with Websites · owner email alerts(notifier) · the database file · the webshell, docs and downloads · the edge:/api/* reaches it through the CDN Apps control plane (inside PitchStation today) SSH Each app host nginx: a neutral default and one vhostper hostnamecertbot (webroot) and a renew hookpolicy sidecar ps-app-policy on127.0.0.1:19780, pinned image/etc/pitchstation/apps/ID/ (versions/,current) · /srv/apps/SLUG/releases/Compose project ps-SLUG on127.0.0.1:41000–41999 PitchStation code here: the policyverifier, the host reporter, the renewhook and, on restricted hosts, theegress unit. Reached over SSH.
Clients reach the control plane only through the Apps API. The control plane drives each app host over SSH. The amber box lists everything Apps currently takes from the rest of PitchStation — the seams a separation has to cut.
LayerWhat it isWhere
APIApps, targets, env, deploy, rollback, preview, stop; publishing configuration, plans, verification, operations, readiness, health and receiptsbackend/routes/apps.js
backend/routes/app-publishing.js
RegistryApps, releases, encrypted env, budgets, placement grants, quotasapp-store.js
TenancyThe one place a code path decides whether an app may use a host and loads its credential: the host's owner is the app's owner, or the app holds a pool grant. Placement, per-person quota and memory admissionapp-targets.js
Guard rails and budgetsCompose rules checked on the upload and on the host's resolved model; per-app systemd slices through a platform override and COMPOSE_FILEapp-compose-policy.js · app-deployer.js
Operator surfaceHost check and onboarding, live/pool approval, quotas, diagnostics and the Compose report, reconciliation — on the admin page, no restartsroutes/admin-app-hosts.js · app-host-onboarding.js ·
app-host-diagnostics.js · app-reconcile.js
Runtime viewContainer status, redacted logs, usage against the budget; failed-step outputapp-runtime-status.js · app-redact.js
Publishing configurationDesired and applied domains and access, revisions, plans, per-host state, hostname claimsapp-config-store.js · app-domain-service.js ·
app-policy-service.js · app-publishing-schema.js
OperationsDurable operations with steps, leases, fences, retry and cancelapp-operation-store.js · app-operation-worker.js ·
publishing-target-lock.js
Host adapterCompose release plan; nginx vhost, certificates and the policy runtime; atomic apply and restoreapp-deployer.js · app-host-config.js
EvidenceReadiness probes, publication monitor, certificate statusapp-readiness.js · app-publication-probes.js ·
app-publication-monitor.js · app-certificate-status.js
PackagingDirectory → validated tar.gz (.pitchstationignore, local-database exclusions); Vite + React and Next.js (standalone, npm or pnpm, workspaces) scaffoldsapp-source-bundle.js · app-framework-package.js
Host runtimeThe code that runs on app hosts: the policy verifier; also the host reporter that onboarding installs, the certificate renew hook and, on hosts with restricted egress, the egress unitbackend/runtime/app-policy-server.cjs
app-host-telemetry.js · app-host-config.js ·
app-host-egress.js
ClientsWeb page, standalone client (generated), MCP tools, skill, OpenAPI, these docsfrontend/apps.html · mcp/pitchstation-apps/ ·
mcp/pitchstation-mcp-server.js ·
mcp/create-pitchstation-app/ ·
backend/config/openapi-apps.json ·
frontend/apps-docs/

§02What Apps borrows from PitchStation

Everything a separation must replace or connect. Measured from the code's imports and shared tables, not from intent.

DependencyTodayCoupling
Identity and capabilityverifyToken (JWT and pst_ tokens) and requireCapability('app.deploy') from the core auth middleware; users live in the core user storeEvery request
Target registryHosts are rows in the Websites module's website_deploy_targets, promoted with a runtime column; one host can serve sites and appsShared table
SSH transportrunPlan, remote/rsync runners, credential handling and per-target queues come from website-deployer.jsCode import
SecretsTarget keys and app env are encrypted with the server's DEPLOY_SECRETShared key
Hostname namespaceClaims are checked against website publications and base domains so a site and an app can never take the same nameCross-product rule
Owner alertsThe publication monitor emails owners through the core notifier and user storeCode import
StorageApps tables live in data/website-deploy.db beside the Websites tables; backed up with everything elseShared file
Hosting of the surface/apps.html, these docs, the downloads, the OpenAPI file and the llms.txt entry are served by the main serverDeployment

§03Principles that make leaving cheap

The API is the boundary

Every client uses only /api/apps over HTTPS, with the origin from PITCHSTATION_URL. Moving the API moves nothing in the clients but one variable.

Docs travel as one folder

frontend/apps-docs/ holds its own HTML, stylesheet and script and links between its pages relatively. It can be served from any origin.

The host protocol is pinned

The policy verifier, its image digest, the neutral nginx default and the renewal hook are checked by hash on every apply. Hosts do not care where the control plane runs.

Operations are durable

Intent, source and progress are persisted, so the worker can run as its own process and survive restarts; idempotency keys make every retry safe.

Seams become named ports

Each dependency in §02 gets a small interface with an in-PitchStation adapter today. Leaving means writing the other adapter, not rewriting Apps.

Contracts are tested at the seam

The 27 September outage was a seam bug: the host view of a path differed from the control plane's. Every seam gets a contract test like the relocated-mount test that now guards it.

§04Ports to extract

PortContractAdapter inside PitchStationAdapter when separate
Identityauthenticate(request) → {userId, email, capabilities}Core auth middlewareVerify PitchStation-issued tokens via a published signing key (JWKS) or introspection; later, its own accounts with PitchStation as one sign-in provider
Entitlementmay(user, "app.deploy"), quotas per userCapability grantPlans and quotas (apps, targets, regions)
TargetslistTargets(user), targetWithSecret(id)Websites target registryOwn target table; self-serve host onboarding
TransportrunRemote, runRsync with capped output, deadlines and fencesImported from the Websites deployerMoved into the Apps package unchanged
Hostname registryclaim / release / assertAvailable(hostname)Shared claims tableA small shared registry service, or disjoint base domains per product
Secretsseal(value) / open(blob)DEPLOY_SECRETA managed key service
Notificationsalert(owner, event)Core notifierOwn email and webhooks

§05Staged path

StageWhat changesRiskState
A · SurfaceDedicated docs folder; standalone client; MCP app tools; OpenAPI and every default on the canonical origin; clients take the origin from one variableNone to running appsdone 27 Sep 2026
B · Module boundaryMove the Apps services and routes into one module directory with a ports file; a test forbids any other import from core; move Apps tables into their own SQLite file (copy, verify row counts, switch), leaving the hostname claims sharedLow: data move is verified and reversiblenext
C · Separately hostedThe same code runs as its own container (API + worker) on its own host. Users keep one domain: the CDN routes /api/apps/* to the new origin (or it gets apps-api.pitchstation.ai). Identity through PitchStation-issued tokens. App hosts admit SSH from the new service instead of production. Either side can be down without taking the other with it.Medium: identity and routing cut-over; rehearse on stagingwhen scope or load needs it
D · Derived platformOwn name and domain; own accounts with PitchStation as a sign-in provider; self-serve host onboarding; billing per app or host; wildcard-TLS and Caddy adapters (refused as ADAPTER_UNSUPPORTED today); multi-region placement; public status pageProduct decision, not a refactoroption
Trigger for Stage C. Any one of: Apps load (builds, uploads, SSH fan-out) measurably affects document sharing on the shared production host; a customer needs Apps without PitchStation accounts; or Apps needs a release cadence independent of PitchStation's.

§06What stays stable for users

§07Open decisions

§08What milestone 2 added

PieceWhereSeam it adds
Policy verifier v2 (sign-in page, cookie, access links)backend/runtime/app-policy-server.cjs, versioned file on each hostHost protocol: reads version-1 policies; the runtime step replaces a known earlier verifier in place and refuses an unknown one. Keys are derived from DEPLOY_SECRET; the control plane stores none, and each host holds them only in the app's root-only applied policy file.
Strict hosts and new rulesapp-compose-policy.js, the platform overrideNone new: the same checker at dry run and on the host
Egress policyapp-host-egress.js, one nftables table and a oneshot unit per hostHost protocol (never nftables.service, which flushes Docker's rules)
Abuse, takedown, termsapp-abuse.js, /api/app-abuse-reports, /abuse.htmlA public, unauthenticated endpoint (rate-limited)
Backupsapp-backups.js; control-plane copies in /data/pitchstation/backups/appsStorage: copies leave the server with the platform's backup mirror (history/apps/<id> in the bucket); app hosts hold no cloud credential
Per-app DNSapp-dns.js (Route 53, SigV4 in-repo)A zone-scoped DNS credential on the control plane
Developer hosts and spendapp-host-requests.js, app-spend.js, scripts/pitchstation-app-host.shThe operator's scoped AWS profile (scripts/iam/pitchstation-apps-host-policy.json); the control plane holds no cloud credential
Moves and evacuationapp-move.jsTwo host leases in sequence; routing through the normal operation path
Compose exemptionsapp-policy-exemptions.jsNone new: an admin-granted, audited list the one checker consults at the dry run, on the host, in moves and in the report; shared hosts ignore it
Host reporter (telemetry, growth proposals)app-host-telemetry.js, POST /api/app-hosts/:id/report, a systemd timer that onboarding installsA per-host report token: only its hash is stored; on the host it sits in a root-only header file, never on a command line. The route accepts nothing else.

Operator runbook: per-app DNS

  1. In Route 53, create hosted zones apps.pitchstation.ai and apps-ap.pitchstation.ai. In each, recreate the wildcard the registrar holds today (*.apps → Oregon's Elastic IP, *.apps-ap → Singapore's) and any other record under those names.
  2. At Namecheap (Advanced DNS for pitchstation.ai), add NS records for the hosts apps and apps-ap pointing at each zone's four name servers, then remove the old wildcard A records there.
  3. Create an IAM user limited to route53:ChangeResourceRecordSets and route53:ListResourceRecordSets on those two zones (plus route53:GetChange), and put its key in production's .env.production as APP_DNS_AWS_ACCESS_KEY_ID / APP_DNS_AWS_SECRET_ACCESS_KEY, with APP_DNS_PROVIDER=route53 and APP_DNS_ZONES=apps.pitchstation.ai=<id>,apps-ap.pitchstation.ai=<id>.
  4. Admin page → Developer hosts → Per-app DNS → Read the records now: every app's record appears at its next routed operation.

Operator runbook: developer hosts

  1. Create the IAM policy PitchStationAppHostProvisioner from scripts/iam/pitchstation-apps-host-policy.json and a user pitchstation-apps with only that policy; aws configure --profile pitchstation-apps on the operator machine.
  2. For each reservation on the admin page: scripts/pitchstation-app-host.sh launch ID --approve (PITCHSTATION_TOKEN= an admin's token, in the environment or in ~/.pitchstation/.env; the script does not read PITCHSTATION_APPS_TOKEN, and the Apps setup's “Save the token” box rewrites that file). It launches, reports, onboards, checks and approves; without per-app DNS it prints the one wildcard record to create.
  3. When a developer releases a host: scripts/pitchstation-app-host.sh terminate ID.

§09What milestones 3 and 4 added

Built from the development plan of 30 September 2026: faster deploys (prebuilt images, a build ceiling, deploy settings, packaging) and private previews (callback paths, caching, account sign-in, link use).

PieceWhereSeam it adds
Image archives (prebuilt images)app-image-archives.js; PUT /api/apps/:id/images; control-plane copies in <APP_ARTIFACT_ROOT>/imagesStorage: up to APP_IMAGE_MAX_MB (1 GB) per archive, three kept per app plus the one the current release uses; left out of the backup mirror (they can be rebuilt from source). The upload streams through the platform's edge, so its body-size limit must allow it.
Build ceilingapp-host-builder.js; pitchstation-build.slice and the buildx builder ps-builder on each hostHost protocol: onboarding installs both (level build, never gating); a deploy uses the builder only when the host reports it installed
Deploy settingsapp-deploy-settings.js; PATCH /api/apps/:id/settingsNone new: commands run inside a service's container with docker compose run --rm, split into words; the host shell never sees them
Account sign-in for private appsroutes/app-enter.js (/api/app-enter/:id); policy verifier v3A browser round trip through PitchStation: the platform checks the person against the applied policy and hands the app a five-minute token signed with the app's link key. The host never calls PitchStation.
Link useThe verifier's PSEVENT log lines; host reporter v2None new: the reporter already posts every five minutes; it now adds link opens
PitchStation Apps documentation · updated 2 October 2026 · Markdown version · Overview · User manual · Deploy guide · API reference · Apps home: pitchstation.ai/apps