Libertas App contract
This is the common authoring contract for Rust and TypeScript Apps.
Required design principle
The goal is the smallest design that remains logically complete. All App authors, including CodeBots, must follow this principle. It is a requirement, not an optional style preference.
Implement every required behavior, condition, and outcome, with a clear owner for each responsibility. Use the platform's defined behavior and normal App lifecycle instead of duplicating them with additional mechanisms. Add App logic when the application's requirements need it; simplicity must not omit necessary validation, permissions, consistency, or user control.
Model data in its natural form. Declare each stored type as what the data is: its real fields, one owner per fact, and a Union where a value is one of several shapes. Never reshape stored data for a screen, a workflow step, or a question order. Human-facing types, the Request and Response payloads a person edits and views, are views of that data: they may wrap it as the person's path needs, for example as a wrapper member within a Union, while the stored model stays as it is.
The shortest path is the person's. Human usability is part of what the smallest design optimizes: the fewest steps, questions, and screens a person needs to reach a complete, correct answer. A design that is shorter for its author or a CodeBot never justifies a longer path for the person.
The two never conflict. The goal is human first, and the natural data model
serves it: the platform presents the model in its natural form along the
person's shortest path, so neither is traded for the other. Where they seem to
conflict, the platform is missing a presentation mechanism; raise that gap
instead of bending the data or lengthening the person's path. Presentation and interaction adapt to the model through the
platform: schema attributes such as AskOnAdd, ReadOnly, and Hidden, and
Display Template rules such as lock, flatten, promote, and layouts. When
the platform has no way to present a natural model, that is a platform gap to
raise, not a reason to bend the data.
For ordinary Apps, persistence failure belongs to the Hub runtime. After storage repair
and a third-party Enable action, the App follows ordinary startup logic; it must not introduce a separate
storage-failure recovery system. Follow the
Data API responsibility boundary.
Ask once, directly
A question exists only to collect a fact. Do not add a Boolean, Yes/No or "something else applies" switch whose only effect is to reveal a follow-up asking the same thing in detail: the person answers twice, the App needs extra navigation and state, and the stored switch can disagree with the real answer. Ask the detailed question itself: a Bitflags or Enumeration list with a "None of these" member, a List that may be empty, or a required Union whose variants carry their own data. Keep a separate question only when its answer decides something the detailed answer cannot.
AI access in every design step
An App's data and interactions serve people, other software and AI from one description, so AI access is part of the design rather than an addition to it. At every design stage (types, pages, rules, messages and tests):
- A page whose values come from a document declares document import. Every field whose value is printed on that document or its attached statements is an import target, including fields in sections that appear only conditionally. A value that only the person's own records hold is not an import target; its description says where to find it. A new field on an importing page is classified when it is added. Nothing an import can set may hold data only the person supplies: a composite on an import path is not Nullable, because a null result replaces it, and a Union whose variant the import chooses carries only document values, because a variant change discards the old payload; keep person-supplied values in a record the import does not select.
- AI Search selects among a page's
offered links and eligible action choices. Links usually differ by the
destination their Request carries: an Enumeration (or Union) field that
CopyFromfills from the tapped row or the response. The authoring obligations make each destination a bound candidate documented with the identifiers and common names a person would type or say, and ownAINavigationPromptguidance.
Application model
The Hub architecture overview explains the platform's resource management and the roles of human users, AI and applications.
A Libertas App is self-describing source code. Public functions define task entry points. Function arguments define task configuration. Reachable public types define configuration, persistent data, Endpoint protocols, and runtime presentation. Studio extracts and validates those declarations and generates the language-neutral schema and localized documents.
App is data interaction. Design the domain data, its meaning, the operations participants can request, and the behavior that handles those operations. Configuration, Requests, Responses and SubscriptionData describe that interaction as typed values. Source code implements the behavior; declaring a protocol does not execute it. Background tasks and TypesOnly packages retain their ordinary roles and need no invented user interaction.
Data is the GUI. Clients present the declared values and operations as editors, results, choices and actions. The schema defines their structure and constraints, and source documentation supplies their localized names and meaning. The Display Template arranges and styles those values without changing their meaning. Design the data interaction first, then choose its presentation.
One interaction serves humans, AI and other Apps. A person uses generated controls, AI uses schema-described tools, and another App exchanges typed Endpoint messages. They use the same declared protocol and App behavior. Implement the operation once in the task; the GUI, AI and software interfaces derive from that common description. A person can use the GUI directly without AI involvement.
For example, declare a SetLimit Request with a typed limit field and
Write access, and a Response reporting the saved
limit. A person edits the generated field, AI supplies the tool argument, and
another App constructs the typed Request. The same task handler checks the
App's conditions, applies the limit and returns the result.
Each participant remains subject to its applicable access checks. Generated
clients follow the human interaction lifecycle;
software may agree with the service on its own ordering within the declared
protocol. ExcludeUI can limit the GUI and
AI projection while retaining the underlying Endpoint contract.
A package therefore has these parts: source code; the generated schema; the
localized documents; the optional display.json at the project root, the
version-controlled source of its presentation rules; and the compiled
Display Template, which Studio builds from
the source presentation attributes and display.json. The template is
mandatory, but an App with neither compiles to the valid empty template. It is
not part of the schema, and changing only the template never changes the App
version. Edit display.json or source attributes, never the compiled template.
Do not create a second JSON schema, manually assign schema IDs, or treat generated schema JSON as source.
The schema FQN for an owned public type is:
developer-package::TypeName
The App identity contains exactly one hyphen. Both sides are snake_case made of
lowercase ASCII letters, digits, and single underscores; neither side may have
leading, trailing, or consecutive underscores. For example,
acme-temperature_monitor is valid and acme-temperature-monitor is not. The
developer and package identify ownership. Importing or re-exporting a type from
another App does not transfer ownership.
Project and discovery rules
Declare task entry functions in src/lib.rs for Rust or Studio's selected
entry module for TypeScript. Other implementation can live in ordinary modules.
Function re-exports do not replace these declarations. The
code organization practice explains module boundaries
and exact-name type exports.
An ordinary App uses every non-ignored public/exported function as a root. Its published type set is the minimum transitive closure needed by:
- function arguments;
- function persistent-data schemas;
- Endpoint protocol schemas;
- inherited and nested type references; and
- schema attributes that reference types, including Bitflags and compatible foreign types.
An unreachable public type is not published by an ordinary App.
A TypesOnly package publishes no executable functions. It automatically roots
every public package-owned named type exported from the language entry module,
unless the declaration is explicitly ignored. A public helper function in a
TypesOnly package must also be ignored. If an included type reaches an ignored
type, generation fails rather than emitting a dangling reference.
Every published named type must be publicly exported from the entry module under its exact declaration name. Renamed root exports and duplicate flattened names are invalid.
Schema compilation with Codex
Studio provides the compile_schema tool for saved Rust and TypeScript App
source. After changing public functions, types, attributes, or dependencies,
save the files, compile the affected project, fix the reported source errors,
and repeat until validation succeeds. Edit source declarations rather than
generated schemas. Continue to run language builds and tests separately.
Supply the absolute project directory; an optional entry selects the entry
file. The default result contains diagnostics, source locations when known,
the schema revision, and a dependency summary. Set includeArtifacts: true
to receive the root schema, default document, and compiled Display Template
plus every transitive App dependency's schema, default document, Display
Template, revision, and dependency edges. Compilation validates display.json
with the schema; its errors are ordinary diagnostics naming display.json and
the one-based line when known. Each
dependency appears once. Missing, ambiguous, or cyclic App dependencies fail
compilation; a partial bundle is not a successful result.
Compilation uses saved files and installed or locally resolvable dependencies. It does not read unsaved editor buffers, install missing dependencies, overwrite source or generated files, or deploy. No Hub connection is required. Success means source schema validation passed; it does not establish exact deployed dependency-version bindings or replace Studio's deployment checks.
The tool's instructions tell Codex and Claude Code to follow this edit-and-validate workflow. See Studio setup for registration and an example prompt. Deployment uses the separate MCP deployment contract.
Type model
Use the closest native type. Do not encode typed values as ad-hoc strings.
| Libertas meaning | TypeScript | Rust |
|---|---|---|
| Boolean | boolean |
bool |
| Signed/unsigned integer | number plus @LibertasNumber(type=...) |
concrete i*/u* |
| Floating point | number, optionally @LibertasNumber(type=...) |
f32/f64 |
| String | string |
String |
| Bytes | Uint8Array |
Vec<u8> |
| List | T[] |
Vec<T> |
| Nullable | optional/nullable supported type | Option<T> |
| Table | interface/class/object type | named-field struct |
| Enumeration | TypeScript enum | payload-free enum or enum attribute |
| Union | discriminated union | enum with variants |
| Date/time | LibertasDateTime, LibertasTimeOnly |
SDK equivalents |
| Platform object | Libertas global object type | SDK object type |
| Endpoint | LibertasEndpoint |
LibertasEndpoint |
DateOnly is an integer yyyyMMdd calendar tuple, not an epoch timestamp.
TimeOnly is seconds from midnight. DateTime is epoch seconds presented in
the viewer's local timezone.
User-facing and non-user-facing types
Choose a representation for the value's meaning and its consumers. A public,
schema-reachable declaration is not necessarily user-facing: it may describe
persistent records or internal calculations. Review each occurrence of a type,
including its editability and visibility, rather than classifying it by pub
or export alone.
| Use | Design question |
|---|---|
| User input, including task configuration | What must the person enter or decide? Which answers may start blank, absent, or unselected? |
| User-visible output | What does the App report? How does an unavailable result differ from an empty result or zero? |
| Non-user-facing storage, calculation, or transport | Which states must the App preserve, and which distinctions affect its logic or compatibility? |
Reuse a type when these meanings and constraints agree. Use separate input and internal types when they differ, and map between them explicitly. For example, an editor can use blank text while a stored record uses absence. This does not justify copying every type into a second hierarchy.
For user input, choose among these representations:
| Meaning | Recommended representation |
|---|---|
| Ordinary optional text, where blank means not supplied | String with Default="", SizeMin=0, and a suitable maximum. |
| Required text | String with SizeMin of at least 1; omit a Default when the person must supply the answer. |
| An optional override of a value the App computes or receives, or harmless supplementary detail | Nullable (Option<T> in Rust), with null meaning “use the computed or received value” or “not given”, stated in the description. For output and internal data, Nullable also marks an unavailable value as distinct from zero. |
| A yes-or-no decision whose “yes” carries details, or an optional override | Nullable labelled with [NullText] and [SetText]; no Default when the person must answer, $null when skipping means null. See Nullable choices. |
| Optional text that must meet a minimum length when supplied | Nullable String with the appropriate size bounds; null remains valid. |
| A decision between named alternatives, including “none” and “provide details” | A labeled Enumeration for choices without payloads, or a labeled Union when a choice carries additional input. |
| Details that only need to expand or collapse | A Table with folding. Folding changes presentation without changing the answer. |
Rows a person adds
A member of an editable List is a row a person can add, starting from type
initialization with no service values. Give every Hidden field in such a row a
Default, Fixed or CopyFrom, since nobody can answer it; an undefined one
blocks submission of the new row. When the service creates the rows and the
person only edits them, mark the List ReadOnly, which fixes its membership
while its members stay editable (ReadOnly Lists).
Studio warns about a Hidden required field in a row a person can add.
Nullable choices
A Nullable (optional) value is a two-state Union: one null state and one
state holding a value of its type, each with default label text unless the
field document names it ([NullText], default “None”; [SetText], default
“Specify”).
An editor presents an editable Nullable as one question with two named
answers (Nullable choices), each client
using its best-practice control for the value type: a null answer labelled by
the field's [NullText] and a value answer labelled by its [SetText], which
opens the field's ordinary editor. An editable Nullable must declare
[NullText], and [SetText] too wherever the control draws a value answer
(standard properties)
(a Number, String, Bytes, date or time, Table or List; a Boolean's and a picker's value answers are their
own members), written as the two answers the person chooses between.
NullText is a complete answer; SetText introduces the value entered right
after it, so it reads as a lead-in to that value. For example: “Alive at the
end of 2026” and “Died in 2026 on” followed by the date; “Use the payer's
basis” and “My own basis is” followed by the amount; “Not a real estate
professional” and “Real estate professional” followed by the qualifying
details. The defaults “None” and “Specify” are only a client fallback; they
never explain the decision.
Decide whether skipping the question is a lawful answer:
- If the person must answer, give no Default. The choice then starts with no branch selected and blocks Submit until the person chooses, exactly like a Union without a Default.
- If skipping means null, such as an optional override of a value the App
computes or receives, or optional detail whose omission decides nothing,
give the reserved Default
$null(Rust#[libertas_default($null)]). The choice starts on the null branch, and the description says what null means. - If one value is the usual answer, give that value as the Default; the choice starts on the value branch with it.
Use a labelled Enumeration or Union instead when there are three or more
answers, or when the negative answer carries data of its own. For output and
internal data, which no person answers, Nullable remains the normal way to
state that a value is absent. Native None/Some or Nothing/Something
names never explain a decision; this is not the gate
Ask once, directly forbids, because the value branch
carries the details itself and the person answers once.
An Option<String> with SizeMin=1 is valid: null is allowed, but a present
empty String is rejected. It therefore differs from a non-Nullable String with
SizeMin=0. Use the latter for ordinary optional text when that distinction
serves no purpose. Size bounds do not supply initial values. Under the
initialization contract, when no
Fixed, Schema Default, or localized default supplies a value, a String starts
unanswered even if it permits empty text, an editable Nullable starts with no
branch selected, and a Union starts unselected. A Default selecting “none” does not establish
that the person confirmed that choice.
Reviewing a representation change
When changing Nullable text to blank text, review presence checks, validation
rules, label fallbacks, calculations, and grouping keys. Translate "" to
absence only where the destination uses that meaning, and translate absence
back to "" for that editor. Do not accidentally group unrelated unnamed
records together. Keep unknown amounts distinct from zero.
Update source comments and AI Import declarations and instructions to match the new blank semantics: making manual entry optional does not enable blank import results, which follow the optional-text import rule. Inspect the generated schema and document, and validate the affected entry and import behavior.
Review published protocol and persistent-data compatibility before changing any representation. Replacing Nullable with a String or custom Union is not automatically compatible. Follow the data-type evolution contract; changing an editor does not require changing an existing stored representation.
External types and App dependencies
Resolve an external type through the native language import/module system. The parser converts the resolved declaration to the owning App FQN. It never guesses an unbound plain name by scanning dependencies.
The external package must itself be a Libertas App and must export the exact type from its entry module. Ordinary npm/Cargo library types cannot become Libertas schema references.
Use ForeignType only when this App publishes its own wire-compatible named
type as a direct representation of one official external type. The compatible
type retains its own FQN; it does not become the official type. The official
type supplies inherited schema/document metadata when consumers materialize
the graph, while explicit compatible attributes override same-name official
attributes.
Do not write versioned AppDependencies in source or edit generated locks.
Studio discovers and verifies dependencies, deploys them first, and records the
actual exact versions. The deployment dependency contract
defines discovery, including which Endpoint constraints create a dependency,
Plaza preflight, installation, failure handling, and the exact
Version pin.
Functions, permissions, and singleton tasks
Public functions are task templates. Their arguments are configuration values. A function may declare:
- a persistent-data Union;
- required process permissions and same-key default-locale reasons;
- required concrete network hostnames or reserved local/external grants and same-key default-locale access reasons;
- semantic validation expressions, each paired with a quoted package
StringResourceskey whose localized value is the user-facing error; - package-owned string-resource keys used by the function; and
Singleton, limiting the Hub to at most one task for the App and native function name across versions.
Before creating a task, Android and iOS first fail fast on a Singleton
conflict, then queue the requested creation while statically checking required
task-process dependencies. They traverse the complete effective input schema.
A client Endpoint carrying Default or Fixed is absolutely required when
neither it nor an ancestor is Nullable and no ancestor List permits zero
items. Every such Endpoint needs a currently compatible server; Fixed also
requires the exact named task-process identity. Missing servers open the
required-task installation UI, and the queued creation proceeds only after a
fresh complete check finds every dependency satisfied. Cancelling any required
dependency installation cancels the originating task creation. See the
task pre-creation dependency check.
Permissions grant the task process access to platform data/resources.
The client approval view always shows the raw permission key as its current
summary and the requesting App's same-key localized StringResources value as
a separate explanation. Different Apps can request the same permission for
different reasons, so explanations are package-owned rather than global.
AccessHost declares the task process's network access: concrete DNS
hostnames or the reserved {network_access_local} /
{network_access_external} broad grants. The public
App network access contract owns its default denial,
syntax, reasons, approval view and runtime enforcement.
AccessPrivilege is different: it limits which endpoint users may invoke one
runtime Request or SubscriptionRequest.
Persistence responsibility
By default, Data API storage failures require the Hub runtime to stop the task rather than returning a recoverable App status. See the persistence responsibility and restart rationale.
A function may opt into checked Data API results using DataErrorStatus; see the
Data API owner. This exception transfers
consistency handling to that App without adding Hub-automatic restart.
Task version selection
A task runs the App version selected for it. Installing or upgrading an App never changes any task's selection. A person with configuration permission on the task changes it through the task's settings; selecting a version of another App retargets the task and also requires configuration permission on that App, as task creation does. The Hub then replaces the task's runtime with the selected version, keeping its arguments and enabled state. When the arguments cannot be initialized under the selected version, the change fails and the task keeps its version. Which version reads the data another wrote follows the directional evolution rules.
Task removal
On Hubs supporting removal preflight, Android and iOS ask the Hub to identify tasks affected by removing a task's virtual devices, Endpoint servers, or on/off device. This includes indirect effects when configuration cleanup removes another task's outputs. Disabled tasks are included.
The user needs removal permission on the selected task and effective configuration permission on every affected dependent task. Dependencies alone produce a localized warning listing the affected tasks, with a choice to cancel or continue. Missing configuration permission produces a localized failure identifying the tasks that need permission; removal cannot continue. The Hub's object-removal handling performs the actual configuration cleanup.
Removing a task removes its persistent data. The confirmation lists every affected task together with the effect on it. An argument of another task that bound an Endpoint of the removed task is set to null. The Hub then validates that task's arguments: when they are still valid, the task is restarted automatically with the new arguments; otherwise it is disabled with the new arguments.
The Hub repeats the same analysis immediately before executing removal. An earlier successful probe cannot bypass a later permission change. When the repeated check fails, including when the Hub cannot inspect the complete task configuration, the Hub reports the failure and does not begin removal.
Removal is atomic. The Hub applies it in the order of the dependency map: the removed task and its data, then each dependent task's nulled arguments and its restart or disabling. Either the whole removal takes effect or none of it does; no person or task observes a partial state, and a removal interrupted by a power loss is completed before the Hub resumes ordinary operation. Older Hubs retain their supported removal operation; the clients warn that dependency and permission preflight requires a Hub update.
Documentation and localization
Source documentation creates the default-locale package document:
- the first documentation line is the display name;
- remaining ordinary lines are the description;
- custom properties use
[PropertyName]followed by their value; [DefaultText]supplies a non-empty, one-line localized initial String value, Endpoint server name, or function task name; unlike schemaDefault, it may differ by locale; omit it when it exactly matches the ordinary fallback;[NullText]and[SetText]supply non-empty, one-line localized labels for the null and value branches of a nullable field's Nullable choice; an editable Nullable declares them as Nullable choices requires, even when “None” is the intended text; a client falls back to localized None and Specify only when a label is missing;- persisted or user-assigned text, including an empty string, is immutable to programmatic defaulting; these defaults apply only while creating a value for which no persisted value exists;
- list documentation may contain a line of at least four hyphens; content above the divider describes the list and content below describes each item; and
- the array divider must appear before custom document properties belonging to the list item.
Plain code comments are not localized schema documentation.
Every authored package, function, named type, parameter, field, List container and item, Enumeration member, Union variant, and inline value must have a non-empty natural label and description that make sense without source names or implementation vocabulary. Custom document values and package string resources follow the same user-language and accessibility rules. The complete normative contract is the schema documentation invariants.
Every public function comment must additionally satisfy the developer function-comment invariants. Its description is a complete, actor-explicit account of the function's purpose, user choices and actions, automatic behavior, outputs, and material limits; it must not rely on generated type text to fill semantic gaps.
String-resource keys are stable identifiers and are never translated. Values are translated as LMF1, preserving the structure the message-format contract defines.
Persistent records use the function's declared binary key union. Human-readable names may use localized formatted values without changing record identity. Follow the transactional Data API and its append-only key and value schema rules.
Wire and compatibility discipline
- Protocol and persistent-data schema versions must remain backward compatible.
Keep each such Union's native name versionless and invariant for the App's
lifetime; a new App version appends new payload variants to that same Union
instead of declaring
ProtocolV2orStoredDataV2. See the App data-type design guidelines. - Attributes such as
Format,PhysicalUnit,DateOnly,Bitflags,ValidationRules, chart metadata,ReadOnly,Hidden, andExcludeUIaffect validation/presentation without changing the underlying wire type.ExcludeUIis specifically a Control-plane projection: the Hub omits a marked server Endpoint from its Control UI-device list and generated clients omit marked request operations. It never removes nodes from admin creation, editing, approval, persistence, routing, or authorization. See the Endpoint declaration contract. - Union declaration order is significant.
variant_index!uses declaration order and append-only protocol/data Union evolution depends on it. - Append new variants for compatible Union evolution; do not insert, remove, reorder, or change existing payload shapes in a published append-only line.
- A protocol may declare conformance separately from its optional variants. Configuration checks mandatory client/server compatibility. A client uses runtime variant indices to decide whether the selected server supports an optional request before sending it (capabilities).
- The common schema/dependency lock and each existing target's exact App version, binary, language, TypesOnly state, and distribution dependencies are immutable within one Plaza release. Changing any of them creates a new content-derived release; adding or removing targets and changing documents or allowed manifest metadata synchronizes the current release. App source never chooses the release tag or public version. See the Plaza publication contract.
Completion checklist
- The project is a valid language library/module and typechecks.
- All published types are exact-name root exports.
- Every external plain name resolves through native imports.
- Function roots and
TypesOnlyroots are intentional. - Every
FormattedTextuse satisfies the static read-only placement and request-value contract. - Mutating endpoint operations explicitly require
Writeaccess. - Permission reasons and string resources are complete.
- Defaults, fixed values, bounds, bit masks, and dates satisfy their host types.
- Schema-visible Union order and wire-shape changes are intentional.
- No generated schema/document JSON was edited manually.