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.
Geprüftremote 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/.
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.
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.
{
"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.
| Field | Meaning |
|---|---|
identifier | App ID. Letters, numbers, ., _, and -, starting and ending with a letter or number |
name, vendor | Display values. The SDK sends these with a push |
version | Legacy 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 |
mimetype | Decides how the platform treats the asset |
public | 1 makes a resource publicly reachable. Keep it 0 for code |
type | Service or weblet type. The SDK stores these in uppercase. Older apps with lowercase values still work, because the SDK normalizes them |
view | Where a weblet appears. Valid names are in ref/sdk/views.md |
width, height | Frame 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.
{
"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 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.
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 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:
$namecomes from the:nameroute segment. A route segment takes priority over a query argument of the same name.<use>must come first. Insiderest:resource, query arguments become local variables, and locals hide globals. Without<use var="AUTH" />, the request?AUTH[user]=1would create a localAUTHand pass the check with no token. The same applies toGreeting, which a request could otherwise replace to break the call.- With
AUTHbound,AUTH.useris set only for a valid token, soemptycovers a missing token, an invalid one, and a forged query argument. - A check placed before
rest:serveris not enough on its own, because$AUTH.*inside the handler could still be forged. See Bind globals insiderest:resource. - Any valid token passes this check. If a route is meant for one application's tokens, also compare
AUTH.application. returncarries 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:serverbuffers output, soheadercan follow other statements inside the handler. Do not produce output beforerest: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.
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 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:htmlbefore it reaches the page, because a display name is data. encode:htmlassigns to a variable first. Literal text next to an embedded statement inoutputwould be discarded.$session.data.langis the session locale. The resource falls back todefaultPrefixif 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:
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.usercheck 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
includefor a service or weblet without itstype. - Writing
<output>Hello <encode:html>$name</encode:html></output>, which losesHello.
Related topics
Evidence
| Item | Record |
|---|---|
| Guide source | Revision 5271871 plus the working-tree changes of this update |
| References | ref/ixmldoc.xml, ref/sdk/, ref/sdk/views.md, ref/dbref.json |
| SDK inspected | zeysdk 3.3.0: create output in a scratch directory, offline help, and source |
| Checked | Every 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 compilation | zeysdk 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 performed | Deployment 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.