Skip to content

Deployment models (.deploy)

A deployment-projection layer over the other Phero Lang artifacts. The .mox domain model defines the data, .ddd designs the application services, and .ifml the interaction flows; the .deploy model declares the logical architecture (an application of named components and their connects), reusable profiles (a target plus defaults, policies, and resource mappings), and deployments (one application projected through one profile with per-application configure overrides) — with nothing about containers, manifests, or clouds written down. Parsed by phero-syntax (parse_deploy, the chumsky parser) at compile time and lowered and validated by phero-driver (compile_deploy_str) into the versioned phero_ir::deploy::DeployModel artifact (the phero-ir crate).

The separation the surface enforces is the issue-#49 discipline:

  • Architecture — what components exist and how they relate (application). Nothing here says where anything runs.
  • Profile — the constraints and runtime topology of a target (profile: target, defaults, policies, mappings). Profiles are reusable across applications.
  • Deployment — the concrete projection (deployment ... for ... use ... configure ...). The effective configuration is component baselines ⊕ profile defaults ⊕ overrides (later levels win).

Targets are implementation-provided, never authored: a profile names one of the closed targets (standalone, dockerCompose, kubernetes, cloudflareWorkers), and the driver’s capability, engine, and setting tables define what each can host. The same logical application deploys to every target it is compatible with — incompatibility is a compile error, not a generated artifact (“validate before generating”).

The .deploy DSL composes the other surfaces: import "<path>.ddd" / import "<path>.ifml" pull in pre-compiled design and flow artifacts, and component bindings (design "<path>#<Module>" / flow "<path>#<Module>") tie components to them by the import path as written. No in-tree backend exists yet: compose.yaml / Kubernetes manifest / Worker-bundle generation are the planned consumers of this artifact (the deployment-plan resource graph is the planned intermediate); the artifact deliberately carries authored intent only in slice 1.

The normative contract — entry points, resolution semantics, and the validation rules — is documented on phero_driver::compile_deploy_str; this page is the language reference.


The fence below is the canonical fixture’s shape (tests/conformance/deploy/svc.deploy), its imports repointed so it compiles from this document (the docs fences are compiled by CI):

import "../tests/conformance/ddd/library.ddd"
import "../tests/conformance/ifml/app.ifml"
application Svc {
component api: api {
scaling: autoscaled
port: 18082
health: "/healthz"
env { LOG_LEVEL = "info" }
}
component lending: api { design "../tests/conformance/ddd/library.ddd#media" }
component web: frontend { flow "../tests/conformance/ifml/app.ifml#Pagination" }
component search: worker { requires longRunningProcess }
component db: database { engine: postgres storage: managed }
connects web -> api
connects lending -> db
}
profile production_k8s {
target: kubernetes
defaults { api.runtime: container api.replicas: 3 }
mappings { api -> kubernetes.deployment database -> external.postgres }
require (api.replicas >= 3)
require (api.port == 18082)
prohibit (db.storage == "local")
}
deployment production for Svc {
use production_k8s
configure { api.replicas: 4 }
}

DeployModel is a standalone, versioned wire artifact following the phero-ir wire format contract (see also the phero_ir::deploy module docs):

  • Versioned independently of the domain-model marker via DEPLOY_MODEL_FORMAT_VERSION (= 1), the ActorModel precedent. DeployModel::from_json rejects absent/wrong versions with IrError::UnsupportedFormatVersion (an absent version reports as 0).
  • All struct field names serialize as camelCase (formatVersion). Unit-only enums serialize as bare camelCase strings: component kinds ("api" | "worker" | "database" | "queue" | "objectStore" | "frontend") and targets ("standalone" | "dockerCompose" | "kubernetes" | "cloudflareWorkers").
  • Setting values are adjacently tagged: {"type": "int" | "text" | "word", "value": <primitive>} — the wire-contract rule-2 shape.
  • Component env entries serialize as {"name": ..., "value": ...} pairs in declaration order, values verbatim; the key is omitted when a component declares none.
  • Closed semantic core, extensible platform surface: kinds and targets are the closed enums; capability names, setting paths, policy expressions, and mapping resources (kubernetes.deployment, cloudflare.d1, docker.volume, external.postgres) are plain strings, so new platform resources need no schema change.
  • Policy expressions are verbatim source text between the policy’s parentheses — the IR never parses or reformats them (the when-condition rule).
  • Resolution is validation-only: the artifact keeps every name, setting, and binding as authored (component baselines stay single-segment). Effective settings are computed for validation and never serialized back.
  • Unknown fields are ignored on deserialize; every field added after v1 must keep the #[serde(default)] + skip discipline.

Minimal artifact: an empty model serializes as exactly {"formatVersion":2}.

let designs: Vec<(String, phero_ir::ddd::DddModel)> = vec![];
let flows: Vec<(String, phero_ir::ifml::IfmlModel)> = vec![];
let compilation = phero_driver::compile_deploy_str("svc.deploy", source, &designs, &flows);
let json = compilation.model.unwrap().to_json_pretty()?; // versioned artifact
let model = phero_ir::deploy::DeployModel::from_json(&json)?; // version-gated reload

The normative EBNF for the .deploy surface lives exactly once, in the parser module docs under .deploy deployment sources (crates/phero-syntax/src/parser/mod.rs) — the single authority for deploy_file, application_decl, component_decl, profile_decl, policy_decl, deployment_decl, and the setting/mapping productions; this page keeps only the language-level rules. The .deploy keyword tables in crates/phero-syntax/src/deploy.rs are the keyword authority the parser and the formatter share.

Language-level rules:

  • application <name> { component <name>: <kind> { ... } ... connects a -> b ... } — the logical architecture. Kinds are the closed vocabulary api, worker, database, queue, objectStore, frontend. A component body carries name: value baseline settings, requires <capability>, ... clauses, env { NAME = "value" ... } blocks, and design/flow bindings.
  • profile <name> { target: <target> ... } — a reusable configuration. defaults { component.setting: value } apply to every deployment selecting the profile; mappings { kind -> platform.resource } say how a kind translates into platform resources; require (...)/prohibit (...) policies constrain every such deployment.
  • deployment <name> for <application> { use <profile> configure { ... } } — one projection. configure overrides beat profile defaults, which beat component baselines.
  • Policies are phero-expr conditions over the effective settings: api.replicas >= 3, db.storage == "local", &&/||/!, integer arithmetic. They are parsed and type-checked against the deployed application’s components (an unknown component or setting is a type error) and evaluated against each deployment’s effective settings — a violated require or satisfied prohibit (including through overrides) is a compile error. The expression rules R1 (checked arithmetic) and R4 (no zero divisor) apply. An unset setting referenced by a policy is an evaluation error: give the component a baseline or the profile a default. Env entries are not features of that universe — api.env.FOO is a type error, by design.
  • design/flow bindings are "<path>" or "<path>#<Module>" — the path must match one of the file’s imports as written and the module must exist in the imported design/flow.
  • Every .deploy word is a contextual identifier (the .ddd discipline): special only in its grammar position, an ordinary name everywhere else, escapable with ^. The kind and target vocabularies are deliberately not syntax — the driver validates them, so a word like container (a .mox keyword) works as a setting value. The same tolerance applies to env entry names: a bare word, keyword tokens included (when = "always" is an env entry).
  • Imports come first: import "<path>" (no trailing ;, the .actor production) must precede the first declaration; a later import is a reported, recovered syntax error. Only .ddd and .ifml imports are supported.
  • The three declaration kinds may be interleaved in any order; the artifact groups them per kind and the formatter canonicalizes to import → application → profile → deployment order (source order kept within a kind).
  • The -> arrow of connects and mappings is a real lexer token of the shared chumsky lexer; it never appears in a parsed grammar position of the other surfaces (raw bodies are span-sliced verbatim).

The per-kind setting tables carry the operational settings a deployment projection needs, typed and closed like every other setting:

  • port (integer) — the listener port, on every kind.
  • health (text) — the health-check path, on api and worker.
  • cors (text) — the comma-separated allowed-origin list, on the browser-facing kinds api and frontend (v1 keeps the list opaque; per-origin validation is a future concern).

They participate in everything settings do: defaults, overrides, effective-settings resolution, and policies for free (require (api.port == 18082) type-checks because the policy universe is built from the same tables — port types as int, health/cors as string). Values beyond the type are platform concerns and carried as authored.

Env is the opaque twin. An env { NAME = "value" ... } block declares string environment variables verbatim, on any component. Env entries are deliberately not settings: they never join the effective-settings map, are never policy-addressable, and are validated only for empty and duplicated names per component. Secrets and platform knobs (LOG_LEVEL, DATABASE_URL) belong here — author intent carried to the backend, semantics kept in the closed setting tables. Env values are quoted strings only.


A deployment file is self-contained for names (applications, profiles, components, deployments resolve within the file) and composition-open for content (design/flow artifacts arrive pre-compiled — the CLI gathers and compiles them exactly like pherolang check on those surfaces). The driver stays filesystem-free.

Validation rules (any error-severity diagnostic drops the artifact; warnings do not):

  1. Syntax — parse errors are reported against the .deploy file.

  2. Imports — every import must name a provided .ddd design or .ifml flow, else imported file "<path>" was not provided; other extensions are unsupported import.

  3. Kinds and capabilities — component kinds and requires names must be in the closed vocabularies (typos are errors, not vacuous requirements).

  4. Settings — component baselines are single-segment name: value lines from the kind’s vocabulary; defaults and overrides are two-segment component.setting: value lines naming a component of the deployed application; word values must be in their list; integer and text settings take the matching literal kind; a path is declared once per level. An env { NAME = "value" ... } block is not settings: entries are opaque strings lowered verbatim, checked only for empty (env name must not be empty) and duplicated (duplicate env 'NAME' on component 'C') names; values pass through untouched and never join the effective settings.

  5. Bindings — a binding’s path must match an import, and a #<Module> suffix must name a module of the imported design/flow.

  6. Targets — a profile must carry a target: line naming a known target.

  7. References — connects endpoints name components of their application; deployment ... for names an application and use names a profile of this file.

  8. Capabilities — per component: intrinsic requirements (a worker needs longRunningProcess, a database sqlDatabase), conditional requirements (engine: sqlite adds persistentFilesystem; runtime: container adds container), and declared requires must all be provided by the target. Target capability tables:

    • standalone — longRunningProcess, persistentFilesystem, sqlDatabase, backgroundWorker, statefulProcess;
    • dockerCompose, kubernetes — those plus container and horizontalScaling;
    • cloudflareWorkers — sqlDatabase, horizontalScaling, edgeExecution (no long-running processes, no local filesystem).

    The closed vocabulary is also the semantic graph’s platformCapability node kind: one node per application and word, with no declaration site (see the graph’s platform capabilities).

  9. Engines — a database’s effective engine must be one the target hosts (standalone: sqlite; dockerCompose: sqlite, postgres; kubernetes: postgres; cloudflareWorkers: d1). A postgres database is never silently mapped onto D1.

  10. Policies — see above: parsed, typed boolean, evaluated against effective settings; violations are errors naming the deployment and the policy. The typed operational settings are addressable (port, health, cors); env entries are not.

Diagnostics are ordered deterministically: parse diagnostics first, then the file’s semantic diagnostics in declaration order (applications, profiles, deployments — each in source order).

Components are also the anchor the .evt subscription layer resolves against: in a checked application (pherolang check <dir>, the union integrity pass phero_driver::check_integrity, register row V12) a subscription consumer must name a declared component — any application’s — else the event contract’s delivery is a dead inbox and the batch errors. The deploy file itself is never changed by that pass; checking a deployment alone stays per-surface and silent.


  • pherolang check <file>.deploy — compile and render diagnostics (including the imported designs/flows, which are gathered and compiled exactly like pherolang check on them; a non-compiling import is a clean error).
  • pherolang ir <file>.deploy [-o out] — emit the versioned DeployModel JSON.
  • pherolang fmt <file>.deploy — the token-based formatter (canonical section order, one member per line, verbatim policy conditions).
  • pherolang artifact check svc.deploy.json — wire-format validation; the artifact classifies as the DeployModel kind.
  • The LSP does not publish .deploy diagnostics yet (the .evt precedent): the surface compiles through the same one-shot entry points, so editor wiring is a follow-up, not a contract change.

The examples/ side ships support.deploy — one logical application bound to support.ddd#desk and support.ifml, projected through a kubernetes profile (postgres, mappings, policies) and a cloudflareWorkers profile (D1) — byte-pinned against examples/artifacts/ by crates/phero-driver/tests/examples_artifacts.rs.