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 CopyFrom fills 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 own AINavigationPrompt guidance.

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:

Text
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 StringResources key 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 schema Default, 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 ProtocolV2 or StoredDataV2. See the App data-type design guidelines.
  • Attributes such as Format, PhysicalUnit, DateOnly, Bitflags, ValidationRules, chart metadata, ReadOnly, Hidden, and ExcludeUI affect validation/presentation without changing the underlying wire type. ExcludeUI is 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 TypesOnly roots are intentional.
  • Every FormattedText use satisfies the static read-only placement and request-value contract.
  • Mutating endpoint operations explicitly require Write access.
  • 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.

Search Libertas