@appport/appboundry
Build and verify an AppPort application for AppBoundry. The default commands operate on the current repository's authoritative contract, implementation, and WASM artifact.
The canonical AppBoundry product tooling is available under the explicit product
subcommand, so it cannot be confused with a customer application build:
# Wrap the customer application in the current directory
npx appport add
npx @appport/appboundry test
npx @appport/appboundry build
npx @appport/appboundry verify
# Build the canonical AppBoundry product
npx @appport/appboundry product build
npx @appport/appboundry product certify
The package ships templates/appboundry.gitignore for generated or copied
AppBoundry workspaces. Installers copy it to .gitignore; it excludes macOS
metadata (.DS_Store and .DS_changes) and node_modules/.
npx @appport/appboundry product build --out AppBoundry.app
npx @appport/appboundry product certify # defaults to AppBoundry.app
test opens AppPort Studio against the current repository's running application so its capability boundary can be exercised before deployment. build emits a repository-named immutable AppPort/application-bundle/1 .app directory plus its browser-uploadable .appbundle. verify checks that application and its declared capabilities. product build and product certify are reserved for the canonical AppBoundry platform artifact.
Upgrade note for 1.11.0: artifacts that declare accounts or tenancy are refused
From 1.11.0, appboundry run refuses an artifact whose manifest declares
identity.accounts or identity.tenancy (FeltKit writes these for auth.accounts and
auth.tenant), with ARTIFACT_IDENTITY_UNSUPPORTED. This host does not serve FeltKit's
sign-up, sign-in, invitation or tenant routes (/api/auth/*, /api/tenants/*), so such an
artifact would run with operator-issued sessions only, and its tenant-scoped collections closed.
1.10.0 and earlier ran it that way without saying so.
To keep running it with operator-issued sessions, accept that explicitly:
appboundry run my-app.app --state ./state --accounts operator-sessions
# or: APPBOUNDRY_ACCOUNTS=operator-sessions appboundry run my-app.app --state ./state
The acceptance is recorded as a runtime warning. Artifacts built before FeltKit declared these
fields, and artifacts that don't use accounts or tenancy, are unaffected. Deployments that start
the runtime themselves (containers, the Hetzner runner) must pass the flag or the variable before
moving to 1.11.0. The proposed identity host interface is in docs/identity-host-interface.md
(FK-GAP-013).
Self-contained artifacts (appboundry.app/1)
From 1.2.0, appboundry run serves a self-contained artifact: a manifest.json plus a
module.wasm that imports nothing. FeltKit's feltkit build --target appboundry produces one.
npx @appport/appboundry@^1.5.0 run my-app.app --state ./state
appboundry --version --json # format, guest ABI, host interfaces (migrations@1 from 1.4.0, transactions@1 from 1.5.0), services, stateContract: "enforced"
What the host provides to application code:
- storage, with FeltDB state on the
--statedirectory; - identity and sessions;
- files, notifications, configuration, jobs (run as the enqueuer) and webhooks (emit is recorded; no delivery, since the artifact has no network).
The artifact's data guarantees (1.3.0). The manifest's state.contract, a FeltDB state
contract, is enforced by FeltDB, as it is on the producer's own runtime:
- required fields and types;
- unique constraints (a duplicate is
CONFLICT); - references with restrict on delete (
FAILED_PRECONDITION); - no undeclared collections.
How a contract is installed. It is validated, composed with the AppPort Services collections the artifact uses, and installed before anything is served:
- A new artifact version is accepted only when every stored record satisfies its contract.
- Otherwise, and when it drops a populated collection, the host refuses to start with
SCHEMA_INCOMPATIBLE, naming what changed and the failing record. The previous version keeps running on unchanged data. - An artifact built before contracts runs as before, with a warning.
The artifact's data migrations (1.4.0). A manifest may list migrations
(state.migrations: { format: "appboundry.migrations/1", order: [...] }, host interface
migrations@1). They are the application's own functions, bundled into module.wasm like
every other function (FeltKit: app.migration()); the host never receives code as data.
Before serving, the host:
- runs each pending migration once, in the listed order, in the guest (invocation
{ migration: name }). A migration may only read application records (migration.records); it returns the writes it staged, and a later migration reads what an earlier one wrote; - commits those writes, the new contract and a history record in one FeltDB evolution; FeltDB re-validates every record against the new contract. A collection the new contract drops is emptied first, in an intermediate contract version, recorded once;
- records in
_appboundry_contractwhat it actually applied: the migrations, how many records were put and deleted, the contract versions, the artifact and who authorized the start (authorizedBy;appboundry runrecords"appboundry run").
A migration that fails, or leaves a record the new contract refuses, refuses the start with
SCHEMA_INCOMPATIBLE (the migration, its error code, or FeltDB's violation) and changes
nothing: the previous artifact keeps running on unchanged data. An applied migration never
runs again. A format other than appboundry.migrations/1 is refused when the artifact loads
(MIGRATIONS_UNSUPPORTED). appboundry run --no-migrate refuses to start an artifact that
would change the schema or run a migration, so a deployment can keep authorizing changes
separately from starting a runtime.
Sequences and after-commit effects (1.5.0). An artifact that requires the host interface
transactions@1 (FeltKit: ctx.db.sequence, ctx.afterCommit) gets two guest operations:
db.sequence { name }: the next number of a stream, drawn in the mutation's transaction and written with it, guarded on the version read. Within a stream, numbers are strictly increasing and gapless in commit order; a failed mutation consumes none. Different streams never contend. Concurrent writers to one stream serialize (CONFLICT). A manifest function withretry: { sequenceConflicts: n }is re-run instead, when it used no service.outbox.stage { effects }: after-commit notifications, webhook events and jobs, validated and committed with the mutation's writes (_appboundry_outbox), or not at all. After the commit, the host performs them as the caller and recordsdoneorfailed. Its worker recovers records a stopped runtime left behind. A failed one waits for an operator:appboundry admin outbox list [--status failed]andappboundry admin outbox retry <id>. From 1.6.0 a job or webhook delivery is created in the same FeltDB transaction that records the effect done (AppPort Services' transaction context,@appport/services0.13.1). So it exists once per committed effect, across a stop and a recovery; a refused creation leaves neither the job nor a done mark. Notifications carry the record id as their idempotency key. Creation, not execution: a job's run and a webhook's delivery to its endpoint are retried by the services.
A host without the interface refuses such an artifact when it loads, and refuses an unsupported
interface version. The contract is FeltKit's docs/TRANSACTIONS.md; FeltKit's
tests/e2e/transactions-parity.test.mjs requires the same results here and on FeltKit's runtime.
MCP (1.11.0). A running artifact also serves its published AppPort capabilities as MCP
tools at POST /mcp (Streamable HTTP, stateless, JSON responses; @appport/mcp). It uses the
same host sessions (Authorization: Bearer abs_…), roles and guest decisions as /appport: a
token that does not verify gets 401, and anonymous callers reach only public capabilities.
Both artifact formats (1.11.0). appboundry run, appboundry artifact verify|inspect and
appboundry app install|inspect recognize the artifact by its manifest. An appboundry.app/1
artifact runs on the artifact host as before. An AppPort application bundle
(AppPort/application-bundle/1, from appport app package) is verified and then started with
appport app start. The managed deploy path still accepts appboundry.app/1 only
(docs/artifact-formats.md).
How the release is checked. pnpm release:appboundry:verify packs this package, installs it
into an empty directory, and runs a FeltKit application built from npm against it.
One AppPort runtime (1.7.0). The package depends on the published @appport/core, @appport/sdk
and appport at exact versions instead of shipping private copies of them, so an installation
runs one AppPort runtime, shared with the application's own @appport/sdk. It follows AppPort's
current semantics: an application contract carries each capability's explicit effect and
authorizationContract, so rebuilding an application yields a new package identity, while a
package built by an earlier release still verifies at its original identity; and a capability that
declares permissions is refused (UNAUTHORIZED) unless the host supplies an authorizer.
The Boundry design foundation (1.7.1). The artifact portal served at /portal
(portal/app.js, portal/portal.css, inlined into AppBoundry.wasm) uses the same
foundation as the cloud portal: StackUI states for deployments, connections and credentials,
failed host reads shown as unknown instead of missing, and the shared theme switch. The package
depends on appport 1.3.0, whose bundled Studio carries the AppBoundry host console on the same
foundation.
WASM providers
AppBoundry reuses the same .app package for a verified Synapse WASM
capability. Synapse owns module discovery, validation, the capability contract,
and WASM execution semantics; AppBoundry owns packaging, provider composition,
binding, authorization, and invocation routing:
.app
└── WASM provider
↓
Synapse capability
↓
AppBoundry discovery → bind → invoke
For example, a packaged change-context-resolver continues to advertise
[email protected]. A consumer requires that capability through the
normal AppBoundry composition API and does not need to know the artifact path,
module filename, or runtime. Inspecting or installing the package reports
runtime: wasm while preserving the Synapse capability identity.
To share a private HTTP application through an AppBoundry edge, keep the application bound to localhost and run:
npx @appport/appboundry connect 3000 --edge https://edge.example
Or set APPBOUNDRY_EDGE_URL and optionally APPBOUNDRY_CREDENTIAL.
The connectivity path is provider-neutral and does not require an AppPort application runtime. The client makes an authenticated outbound connection to the configured edge, receives an edge-issued share and endpoint, and proxies a bounded HTTP subset to the local port.
The package also exposes an in-memory capability composition runtime plus snapshot-backed inspection commands so provider/consumer/binding state stays observable without requiring direct application-to-application integration:
# publish a local capability provider into a runtime snapshot
npx @appport/appboundry capabilities publish llm.generate@1 \
--provider local-llm \
--application example.local-llm
# register a consumer requirement
npx @appport/appboundry capabilities require llm.generate@1 \
--consumer my-app \
--application example.consumer
# inspect currently reachable providers
npx @appport/appboundry providers llm.generate@1
# create a logical binding
npx @appport/appboundry bind llm.generate@1 \
--consumer-application example.consumer \
--provider-application example.local-llm
# inspect the application view
npx @appport/appboundry capabilities --application example.consumer
By default these commands read and write .appboundry/runtime.json in the
current working directory. Pass --runtime <path> to target a different
snapshot file, and --json for machine-readable output.
Current connectivity surface:
- shared
AppBoundry/connectprotocol withhello,ready,request,response,heartbeat,close, anderrormessages - edge-owned
connection_id,share_id,endpoint_id, and authenticatedapplication_id - explicit bounded buffering with:
- max request body: 524288 bytes
- max response body: 524288 bytes
- max frame size: 2097152 bytes
- supported methods:
GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS - hop-by-hop header filtering in both directions
- heartbeat/liveness, disconnect, reconnect, revoke, and expire handling
Current limitations:
- buffering is bounded rather than streamed
- the development edge is single-node and keeps share/endpoint state in memory
status/revokeCLI commands are not yet exposed; revocation and expiry are edge capabilities