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.json manifest. 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) and taps_driver_id (the capture / replay contract). The endpoint requirements live on those Driver rows as required_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: publish builds a version, promote decides which version is served, release decides who may see the Application, deploy only 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_files does not re-enter location matching. It serves the matched file directly, so a location ~ \.br$ block written to attach Content-Encoding to precompressed assets never runs for a file reached through try_files. The asset arrives as application/octet-stream and the browser refuses to execute it. gzip_static is not location-dispatched and sidesteps this entirely.
  • add_header in 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>"}
FieldMeaning
nameApplication identifier. Used by every Application-scoped command.
read_meTyped product document: summary, what_this_does, who_it_is_for, primary_workflows, launch_modes, limitations.
read_me_agentTyped 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_idDriver 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_idDriver 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_endpoints bind to are declared.
  • Deployments — the runtime backing the Backend's published endpoint URLs.

Was this page helpful?