Project schema
Project selection
Section titled “Project selection”Hoplite consumes a normal Hara project containing source paths, a main namespace, capabilities, and profiles. The selected profile must use :profile/language :hoplite.
Hoplite profile
Section titled “Hoplite profile”| Field | Type | Required | Validation |
|---|---|---|---|
:profile/language |
keyword | Yes | Must equal :hoplite |
:profile/main |
qualified symbol | Yes | Must evaluate to a tagged application or internal config |
:profile/options |
map | No | Non-map values are rejected |
:profile/extensions |
map | No | Host-owned configuration; Hoplite reads :extension/hoplite |
:profile/options :port |
integer | No | Must be between 1 and 65535; defaults to 8080 |
:profile/options :workers |
positive integer | No | Defaults to 1 in development and available CPU count in production |
Hoplite extension
Section titled “Hoplite extension”Optional package activation remains inside project.edn:
{:profile/extensions {:extension/hoplite {:hoplite/modules [{:module/id "gh:example:service-package" :module/version "0.1.0" :module/export :example/service :module/as :service :module/config {}}]}}}The canonical Hoplite package coordinate is gh:greenways-ai:hoplite.
gh:OWNER:REPOSITORY coordinates resolve .harp archives from GitHub releases.
Every activation selects one qualified :module/export, binds it to a unique
local :module/as alias, and uses an exact SemVer. A package may be activated
more than once when selecting different exports. Configuration must be inert
EDN without reader tags or metadata.
During hoplite serve check and hoplite serve build, Hoplite verifies the
locked archive, resolves the selected export’s :export/namespace, follows only
package-local :require edges, and compiles the resulting namespace closure into
the application HBX0 bundle. Package source is a build input and is not reopened
by hoplite-server during production startup.
Package resolution binds the coordinate, release asset, export, version,
archive digest and identity revision to project.lock.edn; Hoplite does not
perform live package lookup at production runtime. Storage and other native
authority are selected by the distribution, not by application request data.
The default build rejects :hoplite/authentication. Account, session, and
authorization policy belong to the application or an explicitly composed
distribution; see Application authentication.
Package lock
Section titled “Package lock”Every explicit module must have an exact archive and namespace inventory in
project.lock.edn:
{:lock/format "0.0.0-alpha" :packages {"gh:example:service-package" {:version "0.1.0" :tap "gh" :registry-commit "0123456789abcdef0123456789abcdef01234567" :identity-revision "89abcdef0123456789abcdef0123456789abcdef" :archive-sha256 "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" :namespaces [example.service]}}}The commit fields are lowercase 40-character Git revisions. The namespace inventory must be non-empty and include the namespace selected by every active export. Two locked packages cannot claim the same namespace.
The requested version, lock entry, installed registration, content-addressed root name, HARP identity, selected export, namespace inventory, and every file digest must all agree before module code is evaluated.
One authored manifest
Section titled “One authored manifest”Hoplite does not load a root hara.extension.edn. HTA or WASM extension
contracts belong under the applicable profile’s :profile/extensions map,
alongside :extension/hoplite. A project containing the legacy root file fails
validation with a migration message instead of silently combining two sources
of truth.
Application definition
Section titled “Application definition”| Field | Type | Required | Behavior |
|---|---|---|---|
:name |
text-like | No | Defaults to app-<id> |
:handler |
callable or Var | Unless resources or generated channel hooks | Creates one ANY /*path catch-all route |
:resources |
sequence | Unless catch-all | Flattened into method/path/handler routes |
:route/adapter |
keyword | No | Default route boundary: :raw, :request (default), or :request+hta |
:request/body |
map | No | Enables a bounded native request-body handle for the application |
:proxies |
sequence | No | Closed fixed-prefix, fixed-origin Nginx upstream declarations |
:channels |
sequence | No | Values constructed with hoplite.nchan/channel; hoplite.core/channel remains compatible |
:peers |
sequence | No | Values constructed with hoplite.core/peer |
:openapi :path |
text-like | No | Explicit OpenAPI route; development otherwise defaults to /openapi.json |
Each resource operation accepts the same :route/adapter field and overrides
the application default for that route. The selected value is recorded in the
version 2 apps.hta manifest.
The closed :request/body map requires positive integer :max-bytes and may
include positive :max-chunk-bytes. The chunk limit defaults to the smaller of
64 KiB and the total limit and cannot exceed :max-bytes. An application with
this profile cannot contain :request+hta routes. Native-body handlers receive
an opaque :body-handle; the handle is scoped to the owning request and work.
:route/auth is not a Hoplite transport field and is rejected. Authorization
must run inside the HAL handler.
Channel definition
Section titled “Channel definition”Every item under :channels must be constructed with
hoplite.nchan/channel. hoplite.core/channel remains a compatibility
constructor for the current pre-1.0 line. The map is closed; unsupported keys
are errors.
| Field | Type | Required | Default and validation |
|---|---|---|---|
:name |
text-like | Yes | 1–64 ASCII letters, digits, -, or _; unique per app |
:path |
text-like | Yes | Safe absolute path ending in /:channel; unique and non-overlapping with proxies |
:profile |
keyword | No | Defaults to and must equal :ephemeral |
:authorize |
callable or Var | Yes | Prepared as the subscriber-authorization handler |
:admit |
callable or Var | Yes | Prepared as the publisher-admission handler |
:transports |
sequence of keywords | No | Defaults to [:websocket :sse]; only those values; non-empty |
:message-buffer |
integer | No | Defaults to 8; range 0–64 |
:message-timeout-seconds |
integer | No | Defaults to 30; range 1–300 |
:max-channel-id-bytes |
integer | No | Defaults to 128; range 22–256 |
:max-subscribers |
integer | No | Defaults to 4; range 1–64 |
Hoplite generates private GET and POST routes for Nchan authorization subrequests. They are prepared like other handlers and recorded in the runtime manifest, but omitted from public OpenAPI output. The application package cannot inject raw Nginx directives or replace the statically linked Nchan module.
Peer definition
Section titled “Peer definition”Every item under :peers must be constructed with hoplite.core/peer. The map
is closed; unsupported keys are errors.
| Field | Type | Required | Default and validation |
|---|---|---|---|
:name |
text-like | Yes | 1–64 ASCII letters, digits, -, or _; unique per app |
:channel |
text-like | Yes | Must name a channel in the same application |
:handler |
callable or Var | Yes | Application-owned peer policy handler |
:label |
text-like | No | Defaults to peer name; 1–64 ASCII letters, digits, -, _, or . |
:max-message-bytes |
integer | No | Defaults to 65536; range 1–1048576 |
:idle-timeout-seconds |
integer | No | Defaults to 120; range 5–3600 |
Peer declarations do not create portable session identities. Runtime handles
created through hoplite.rtc stay inside one Nginx worker.
Advanced host configuration
Section titled “Advanced host configuration”| Field | Type | Required | Behavior |
|---|---|---|---|
:worker-processes |
positive integer | No | Overrides profile/default worker count |
:apps |
non-empty sequence | Yes | Application instances to host |
Instance fields are documented in Multiple applications.
Sources: core/src/platform.rs, core/src/package_catalog.rs, and core/src/app.rs