Applications
An Application is a published front-end surface over a Backend. Its application.json manifest names the Drivers whose contracts a bound Backend must satisfy.
TL;DR
- An Application is a container serving a UI, bound to one or more Backends through an
application.jsonmanifest. It is served on demand: the container starts on first request and stops once unused. - The manifest carries
needs_driver_id(the live wire contract) andtaps_driver_id(the capture / replay contract). The endpoint requirements live on those Driver rows asrequired_endpoints, never inline in the manifest. - Endpoint URLs are a Backend property, and a live Deployment is what makes those URLs serve. A required endpoint resolves to a concrete URL on the Backend, not on a particular Deployment.
- The platform builds the image from your source. There is no way to hand it one built elsewhere.
- Four verbs, four distinct effects:
publishbuilds a version,promotedecides which version is served,releasedecides who may see the Application,deployonly starts the container early.
Mental model — needs ↔ aliases
A Driver's required_endpoints entry is the consumer half of a binding contract; the other half is a Backend endpoint alias. The endpoint-alias model — how aliases decouple the graph from the surface and map a (vertex, endpoint-name) to a role — is owned by /concepts/solutions; this page covers only how an Application points at a Driver and what that Driver then demands of the Backend.
The Application names a Driver; the Driver names the endpoints:
application.json driver row
┌────────────────────┐ ┌─────────────────────────────┐
│ needs_driver_id ──►│───►│ required_endpoints[] │
│ taps_driver_id │ │ name: upload │
└────────────────────┘ │ role: upload │
│ direction: ingress │
│ transports: [http] │
│ required: true │
└─────────────────────────────┘
The platform matches a required endpoint to an alias by role, then validates that the direction matches and that at least one of the listed transports overlaps with what the alias supports. If the alias does not exist, the direction does not match, or no transport overlaps, the resolution fails closed: a required: true endpoint blocks the Application, and a required: false one is dropped.
The Application does not own the Backend graph, the Deployment lifecycle, the Component implementations, the container layout, or the Runtime placement. Those are upstream concerns the Application consumes through resolved endpoint URLs.
Mental model — build, serve, show
publish ──────► version N exists what has been built
promote ──────► version N is served what visitors get
release ──────► listed in the catalog who may see it
Publishing does not change what visitors are served, which is what lets you build a version and inspect it before anyone else gets it. Promoting is the step that switches them over. Releasing never touches either.
Walkthrough — bind a UI to a Backend
Pre-requisites: a Backend with the endpoint aliases the UI needs (declared with ppl backend change-endpoint-alias), a Driver describing what the UI consumes, and a source directory containing a Dockerfile.
# 1. name the Driver contract in application.json:# {# "name": "doc_scanner_ui",# "read_me": { "schema_version": 1, ... },# "read_me_agent": { "schema_version": 1, ... },# "needs_driver_id": "<driver id>"# }# 2. register the Application + its contractppl application create --name doc_scanner_ui --manifest ./application.json# 3. build the source into a versionppl application publish doc_scanner_ui --dir ./ui -m "first cut"# 4. make that version the one visitors are servedppl application promote doc_scanner_ui# 5. list it in the workspace catalogppl application release doc_scanner_ui
When the Backend is live behind a running Deployment, each of the Driver's required endpoints resolves to a concrete URL on the Backend's published endpoint. The UI posts uploads to the upload URL and subscribes to the events URL; the Backend's vertex layout, container placement, and Runtime stay invisible to it.
The build contract
The tree at --dir is uploaded and built on the platform's build cluster. It must carry a Dockerfile that builds the project and serves it over HTTP on port 80.
Builds use Docker's classic builder, not BuildKit, so RUN --mount=type=secret and RUN --mount=type=cache fail. Read build-time registry credentials from the .npmrc the platform injects into the build context.
A project inside a monorepo shares its lockfile and packages with its siblings, so it cannot be uploaded alone. Upload the workspace and name the project inside it:
ppl application publish doc_scanner_ui --dir . --project apps/doc_scanner_ui
The Dockerfile is read from the project; the build still sees the whole workspace.
Serving performance
An Application is a static origin, so the response headers it emits are the entire caching story. Compress once at build time rather than on every request:
RUN find out -type f \( -name '*.js' -o -name '*.css' -o -name '*.html' \ -o -name '*.json' -o -name '*.svg' \) -exec gzip -9 -k {} \;
Then serve the precompressed artifact, and cache immutable assets separately from the shell:
gzip_static on;gzip on;gzip_comp_level 6;gzip_vary on;# Content-hashed paths: the name changes whenever the bytes do.location /_next/static/ { add_header Cache-Control "public, max-age=31536000, immutable"; add_header Vary "Accept-Encoding";}# The shell names the current asset hashes, so caching it pins visitors# to a build that no longer exists.location / { add_header Cache-Control "no-cache"; try_files $uri $uri/ /index.html;}
Three rules worth stating outright, because each has taken a live Application down:
try_filesdoes not re-enter location matching. It serves the matched file directly, so alocation ~ \.br$block written to attachContent-Encodingto precompressed assets never runs for a file reached throughtry_files. The asset arrives asapplication/octet-streamand the browser refuses to execute it.gzip_staticis not location-dispatched and sidesteps this entirely.add_headerin a nested location replaces the inherited set rather than adding to it. Every header a location needs must be stated inside that location.- The container must listen on port 80.
Variations
Register only — local iteration. ppl application register doc_scanner_ui creates an internal-mode Application row with no container and no contract; it takes a name and nothing else. The row is visible only to you; pair it with a local dev server while iterating, attach contracts with update, then publish when the UI is ready.
Serve an older version. ppl application promote doc_scanner_ui --version <version_id> serves a specific published version instead of the newest, which is how a bad build is rolled back. The flag takes the version's id from ppl application versions, not the seq shown beside it.
Bind or re-bind the contract. ppl application update doc_scanner_ui --needs-driver <driver_id> --taps-driver <driver_id> sets the two contracts directly; --manifest does the same from a file. --new-name renames the Application.
Skip the cold start. ppl application deploy doc_scanner_ui starts the container before the first request arrives, so the first visitor does not wait for it. --force replaces a container that is already serving.
Publish to everyone. ppl application release doc_scanner_ui --public lists the Application in the public catalog and serves it by name without a login.
Reference snapshot — application.json
{ "name": "doc_scanner_ui", "read_me": { "schema_version": 1, "...": "..." }, "read_me_agent": { "schema_version": 1, "...": "..." }, "needs_driver_id": "<uuid>", "taps_driver_id": "<uuid>"}
| Field | Meaning |
|---|---|
name | Application identifier. Used by every Application-scoped command. |
read_me | Typed product document: summary, what_this_does, who_it_is_for, primary_workflows, launch_modes, limitations. |
read_me_agent | Typed selection contract: summary, pick_when, do_not_pick_when, interaction_model, required_launch_modes, workflows, limitations. Both positive and negative criteria are mandatory, so an agent cannot infer relevance from compatibility alone. |
needs_driver_id | Driver whose contract a Backend must satisfy on the ingress side — the live wire surface the UI consumes. Optional; omitting it leaves the Application unconstrained. |
taps_driver_id | Driver for the capture / replay side. Optional, and often the same Driver row as needs_driver_id. |
The endpoint requirements the Backend must meet are read from those Driver rows, as required_endpoints entries of the form {name, role, direction, transports, required}.
Run it
ppl application create --name <name> [--manifest ./application.json] [--readme <path>] [--agent-manifest <path>] [--needs-driver <id>] [--taps-driver <id>]ppl application register <name> # internal stub, no container, no flagsppl application publish <name> [--dir <source_dir>] [--project <path>] [-m <note>] [--no-cache] [--node <id>]ppl application versions <name>ppl application promote <name> [--version <version_id>]ppl application release <name> [--public]ppl application deploy <name> [--force]ppl application stop <name>ppl application update <name> [--manifest <path>] [--needs-driver <id>] [--taps-driver <id>] [--new-name <new>] [--public | --private]ppl application listppl application delete <name>
Related
- Solutions — the full stack; the Application is the UI on top of a Backend and its Deployment.
- Backends — where the endpoint aliases the Driver's
required_endpointsbind to are declared. - Deployments — the runtime backing the Backend's published endpoint URLs.