Skip to main content
View Markdown

Minimal App

This chapter builds the smallest app that shows how the parts of a ZeyOS app fit together: one shared resource, one protected service, one weblet, and instance settings. Use it as a starting point, and as a working example of the execution contexts rule that each entry point authenticates differently.

Verifiedremote compiled on sdk-alpha, 5 November 2026

resources/Greeting.ixml, services/api.ixml and weblets/hello.ixml compile with zeysdk compile (engine iXML 2.00-00) on a sandbox instance. They have not been deployed or run on an instance. See the Evidence section before you rely on them.

The app has this shape. The same files live in the guide repository under examples/minimal-app/.

Text
minimal-app/
├── zeyos.app.json
├── README.md
├── resources/
│ └── Greeting.ixml
├── services/
│ └── api.ixml
├── settings/
│ ├── defaultsettings.json
│ ├── settingscode.ixml
│ └── usersettingscode.ixml
└── weblets/
└── hello.ixml

resources/ holds the logic, while services/ and weblets/ are thin entry points that handle identity and output. The two empty settings scripts come from the scaffold.

Create the scaffold​

zeysdk create writes the directories, an empty README.md, .zeysdk/sync.json, and three settings files. This step is local.

Bash
zeysdk create example.minimal --name "Minimal Example" --vendor "Example"

The command also creates an assets/ directory, which this app does not use. Git does not track empty directories, so it is absent from the example.

Manifest​

zeyos.app.json names the app and registers every asset.

JSON
{
"identifier": "example.minimal",
"name": "Minimal Example",
"version": 10000,
"vendor": "Example",
"resources": {
"Greeting": {
"name": "Greeting",
"mimetype": "application/ixml+xml",
"public": 0
}
},
"services": {
"api": {
"name": "API",
"type": "REMOTECALL",
"entity": "",
"schedule": 0,
"interval": 1,
"url": "",
"mimetype": "application/ixml+xml"
}
},
"weblets": {
"hello": {
"name": "Hello",
"type": "EMBEDDED_FRAMED",
"view": "contacts.details_contact.head",
"width": 0,
"height": 120,
"url": "",
"langaliases": null,
"mimetype": "application/ixml+xml"
}
}
}

Each entry's key plus its MIME type's extension gives the file path, so Greeting with application/ixml+xml is resources/Greeting.ixml.

FieldMeaning
identifierApp ID. Letters, numbers, ., _, and -, starting and ending with a letter or number
name, vendorDisplay values. The SDK sends these with a push
versionLegacy field. ZeySDK 3.3.0 does not write it in a new scaffold, and a push does not send it. It stays here because the reference's manifest example includes it
mimetypeDecides how the platform treats the asset
public1 makes a resource publicly reachable. Keep it 0 for code
typeService or weblet type. The SDK stores these in uppercase. Older apps with lowercase values still work, because the SDK normalizes them
viewWhere a weblet appears. Valid names are in ref/sdk/views.md
width, heightFrame size in pixels for popup and embedded weblets

Asset identifiers​

Each asset's full identifier is <app>.<name>. In this app they are example.minimal.Greeting, example.minimal.api, and example.minimal.hello. Inside the app, .Greeting addresses the same resource without repeating the app name. See Includes and JSON resources for the evidence level of this form.

Weblet placement​

EMBEDDED_FRAMED on contacts.details_contact.head shows a framed panel in the contact detail head. The example sets an explicit height. Automatic sizing with height 0 is not independently verified.

Settings​

settings/defaultsettings.json ships with the app and must never contain secrets.

JSON
{
"greeting": {
"defaultPrefix": "Hello",
"fallbackName": "there",
"prefix": {
"de_DE": "Hallo",
"en_US": "Hello"
}
}
}

An optional settings/settings.<instance>.json holds the values for one linked instance. A push sends only the file for the active instance.

At runtime, ZeyOS merges the two recursively into APPSETTINGS (Source-confirmed). Instance values win. Nested objects merge key by key, so an instance file can override greeting.prefix.de_DE and keep the rest. List items merge by position rather than being replaced as a whole, so an instance list shorter than the default keeps the default's extra items.

Put secrets only in instance settings, keep those files out of version control, and never send a settings value to a browser or a log.

Resource​

The resource defines a function on a global, the same controller style real apps use. It knows nothing about HTTP or sessions, so every entry point can share it.

XML
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ixml SYSTEM "https://developers.zeyos.com/schema/ixml.dtd">
<ixml xmlns="https://developers.zeyos.com/schema/ixml">

<global var="Greeting" />

<!--
Build a localized greeting.

@param string name Display name; may be empty
@param string lang Locale such as en_US; may be empty
@return string
-->
<function var="Greeting.forName">
<trim var="clean">$name</trim>

<if value1="$clean" value2="">
<set var="clean">$APPSETTINGS.greeting.fallbackName</set>
</if>

<set var="prefix">$APPSETTINGS.greeting.defaultPrefix</set>

<array:keyexists var="APPSETTINGS.greeting.prefix" var_result="known">$lang</array:keyexists>

<is var="known" type="true">
<set var="prefix">$APPSETTINGS.greeting.prefix[$lang]</set>
</is>

<set var="return">$prefix, $clean!</set>
</function>

</ixml>

Every iXML file here uses the envelope from the SDK's current default template, which references only the DTD.

warning

Do not add xsi:schemaLocation pointing to ixml.xsd. Older templates include it, but that XSD URL was reported to return 404. Only the DTD at https://developers.zeyos.com/schema/ixml.dtd is used here.

REMOTECALL service​

The service handles one route and authorizes explicitly. An unauthenticated call still runs this file, so the check is what protects the route.

XML
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ixml SYSTEM "https://developers.zeyos.com/schema/ixml.dtd">
<ixml xmlns="https://developers.zeyos.com/schema/ixml">

<include id=".Greeting" />

<rest:server>
<rest:resource route="/greeting/:name" method="GET">
<!-- Query arguments become locals here. Bind globals first so a request cannot replace them. -->
<use var="AUTH" />
<use var="Greeting" />

<!-- An unauthenticated call still reaches this code. Reject it here. -->
<is var="AUTH.user" type="empty">
<array var="problem">
<item key="error">Authentication required</item>
</array>

<set var="return">401</set>
<header>WWW-Authenticate: Bearer</header>
<header>Content-Type: application/json</header>
<output><encode:json var="problem" /></output>
<return />
</is>

<call func="Greeting.forName" var="text">
<param name="name">$name</param>
<param name="lang">$AUTH.lang</param>
</call>

<array var="payload">
<item key="greeting">$text</item>
</array>

<header>Content-Type: application/json</header>
<output><encode:json var="payload" /></output>
</rest:resource>
</rest:server>

</ixml>

Key points:

  • $name comes from the :name route segment. A route segment takes priority over a query argument of the same name.
  • <use> must come first. Inside rest:resource, query arguments become local variables, and locals hide globals. Without <use var="AUTH" />, the request ?AUTH[user]=1 would create a local AUTH and pass the check with no token. The same applies to Greeting, which a request could otherwise replace to break the call.
  • With AUTH bound, AUTH.user is set only for a valid token, so empty covers a missing token, an invalid one, and a forged query argument.
  • A check placed before rest:server is not enough on its own, because $AUTH.* inside the handler could still be forged. See Bind globals inside rest:resource.
  • Any valid token passes this check. If a route is meant for one application's tokens, also compare AUTH.application.
  • return carries the HTTP status. <return /> stops the handler.
  • This check establishes who is calling. A route that changes data must also check what that user may do, and it should re-check ownership of any record it touches.
  • rest:server buffers output, so header can follow other statements inside the handler. Do not produce output before rest:server.

This follows the recommended service-to-resource flow: the service owns transport and authorization, and the resource owns the logic.

A call looks like this. The token is a user access token, not your SDK token.

Bash
curl -i \
-H "Authorization: Bearer $ZEYOS_ACCESS_TOKEN" \
"https://cloud.zeyos.com/<instance>/remotecall/example.minimal.api/greeting/Ada"

Expected result with a valid token: status 200 and a body such as {"greeting":"Hello, Ada!"}. Without a token, or with ?AUTH[user]=1 and no token: status 401 and {"error":"Authentication required"}.

Weblet​

The weblet runs in the user's session, so it reads the identity from session and calls the resource on the server. It does not call the REMOTECALL service from the browser. The browser may send its cookie with that call, but the service's auth step ignores it.

XML
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ixml SYSTEM "https://developers.zeyos.com/schema/ixml.dtd">
<ixml xmlns="https://developers.zeyos.com/schema/ixml">

<include id=".Greeting" />

<session var="session" />

<call func="Greeting.forName" var="text">
<param name="name">$session.name</param>
<param name="lang">$session.data.lang</param>
</call>

<encode:html var="safeText">$text</encode:html>

<header>Content-Type: text/html; charset=utf-8</header>
<output><![CDATA[<p>$safeText</p>]]></output>

</ixml>

Key points:

  • The text is encoded with encode:html before it reaches the page, because a display name is data.
  • encode:html assigns to a variable first. Literal text next to an embedded statement in output would be discarded.
  • $session.data.lang is the session locale. The resource falls back to defaultPrefix if it is empty or unknown.

Expected result: a frame in the contact head showing Hello, <your user name>!.

Deploy and test​

Run the four verification checkpoints in order. In short:

Bash
zeysdk link <instance>
zeysdk compile resources/Greeting.ixml
zeysdk compile services/api.ixml
zeysdk compile weblets/hello.ixml
zeysdk push --dry-run

Review the plan, push to a sandbox, then test the service over HTTP and the weblet in the browser. zeysdk run cannot test either entry point.

Common mistakes​

  • Removing the AUTH.user check because the service "only reads data".
  • Removing <use var="AUTH" /> because the handler seems to work without it. It does, until someone adds a query argument.
  • Putting an API key in defaultsettings.json, which ships with the app.
  • Calling the service from weblet JavaScript and expecting the session to carry over.
  • Using include for a service or weblet without its type.
  • Writing <output>Hello <encode:html>$name</encode:html></output>, which loses Hello.

Evidence​

ItemRecord
Guide sourceRevision 5271871 plus the working-tree changes of this update
Referencesref/ixmldoc.xml, ref/sdk/, ref/sdk/views.md, ref/dbref.json
SDK inspectedzeysdk 3.3.0: create output in a scratch directory, offline help, and source
CheckedEvery command and attribute against ref/ixmldoc.xml, manifest-to-file consistency, chapter listings against the example files, JSON parsing, and XML well-formedness with xmllint. The service's auth check, including forged query arguments, and the weblet were also run in a local copy of the iXML engine with stand-in globals, without an instance
Remote compilationzeysdk compile with SDK 3.3.0 on the sandbox sdk-alpha, 5 November 2026: all three scripts compile (engine iXML 2.00-00). The two empty settings scripts were not submitted.
Not performedDeployment and execution on an instance. No runtime result on this page has been observed on an instance

xmllint confirms XML well-formedness only. It is not native validation. The expected outputs above are derived from the reference and the local engine run, not observed on an instance. Open runtime checks are listed in contributing/REVIEW_BACKLOG.md.