---
title: "Minimal App"
description: "A small, complete ZeyOS app with a resource, an authorized REMOTECALL service, a weblet, and settings."
canonical_url: "https://www.zeyos.com/ixml/ixml-guide/development-environment/minimal-app"
markdown_url: "https://www.zeyos.com/ai/guide/ixml/ixml-guide/development-environment/minimal-app.md"
section: "ixml-guide"
authority: "guide"
language: "en"
source_repository: "zeyos/guide"
source_path: "docs/ixml/03-development-environment/minimal-app.md"
source_revision: "cce886a8da98cd124ee3644711085bbdcf1a87ce"
---

# 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](https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts) rule that each entry point authenticates differently.

:::note[Verified: remote 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](#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`.

| 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](https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts#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](https://www.zeyos.com/ixml/ixml-guide/development-environment/troubleshooting#reported-not-independently-verified--check-in-your-target).

## 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`](https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts#bind-globals-inside-restresource).
- 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](https://www.zeyos.com/ixml/ixml-guide/development-environment/app-structure#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](https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts#calling-your-own-service-from-a-weblet).

```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](https://www.zeyos.com/ixml/ixml-guide/introduction/result-propagation).
- `$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](https://www.zeyos.com/ixml/ixml-guide/development-environment/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`.

## Related topics

- [Execution Contexts](https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts)
- [Verification Checkpoints](https://www.zeyos.com/ixml/ixml-guide/development-environment/verification-checkpoints)
- [Troubleshooting](https://www.zeyos.com/ixml/ixml-guide/development-environment/troubleshooting)
- [App Structure](https://www.zeyos.com/ixml/ixml-guide/development-environment/app-structure)
- [SDK Workflow](https://www.zeyos.com/ixml/ixml-guide/development-environment/sdk)

## 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`.
