Applications
The System → Applications page installs, upgrades, removes and builds .gemapp packages — complete, versioned units of GEM configuration that can be moved between installations and installed more than once.
Where Import / Export moves individual artifacts as a one-way JSON bundle, an application is a package with an identity. GEM records everything it created, so it can later be upgraded to a new version or removed cleanly, leaving the rest of the site untouched.
Every action on this page requires an elevated role. A package can carry custom driver code and web service scripts, both of which execute on the controller — installing one is a code path into your server. See Roles.
What an application is
A .gemapp file contains:
- a manifest — the package's identity (
app_id, version, author), what it requires of the target system, and the settings it asks for at install time - its artifacts — devices, zones, UIs, macros, drivers and everything else listed under Contents, in the same form Import / Export uses
- its assets — theme fonts and images, and anything else that isn't configuration text
Packages are not encrypted. That is deliberate: you should be able to inspect a package before you install it.
Ownership is what makes it an application
When GEM installs a package it writes a ledger of every row it touched, and records each one as either:
- owned — the install created this row. An upgrade may update it; an uninstall removes it.
- referenced — the row already existed and the package merely points at it (a subsystem it hung zones from, a widget it re-used by name). Referenced rows are never modified on upgrade and never deleted on uninstall. Somebody else made them.
This is why an application can be removed cleanly and a plain import cannot.
Ownership is visible on the item itself
Every item a package created carries the package on its row, so the rest of GEM can see it without consulting the ledger. The device, zone, macro and UI editors show Provided by name v_x_ on such items, and the widget picker on a UI page groups a package's widgets under its name. Deleting an owned item from an admin page is refused and names the package: remove it through Applications, or Forget the application to release everything it created. Provided by links from UI Pages and UI Widgets, and the package icon in the row toolbox of the UIs, Subsystems, Device Types and Command Sets grids, land on this page as ?app_id=<id>[&instance=<n>] and open that package's detail panel directly; a package that is not installed is reported with a warning. Items a package merely referenced carry nothing, and neither does anything hand-built. The column is kept in step with the ledger at every boot; a disagreement is repaired toward the ledger and logged.
An application's own interface
A package may declare an entry: a UI it ships that is its frontend.
"entry": {
"ui": "ui:security_console", // the ui artifact that is the interface — required
"nav_layout": "none", // none (default) | top | rail
"theme": "none", // inherit (default) | none | the key of a ui_theme it ships
"listed": false, // keep it out of the UI selector (default true)
"home": "console_home", // the page it opens on, by its name in the package (optional)
"login": "ui_widget:sign_in" // a code widget that replaces the sign-in screen (optional)
}
Once installed, the application answers at /app/<app_id> — /app/<app_id>/<instance> when the same package is installed more than once. The route loads the entry UI exactly as ?ui= would, under the same role allow and deny and the same command scope, and keeps its address while you move between pages, so a reload or a bookmark lands back in the application. The Installed card shows the route.
- Chrome.
nav_layout: nonetakes GEM's top bar, media bar and page header off the screen entirely. They are not drawn at all, so they load nothing for that UI: no clock, no weather poll, no site modes. The application gets the whole viewport, including on wide screens where a classic layout is letterboxed to 16:9, and draws everything. The media session behind the media bar keeps running, so the application's own AV widgets still follow the room's source, volume and warmup. Reconnect, PIN prompts and toasts still render unless the application replaces them. - Theme.
inherituses the site's theme. Aui_themekey uses one the package ships.noneloads only the platform base stylesheet — layout, tokens, reset — plus the application's ownassets/app.css, linked last so its rules win. Either explicit choice locks the theme against a user's personal override; the UI'stheme_lockedattribute shows it and an admin can clear it. - Start page.
homenames the page the UI opens on, by the name it has in the package. The install writes the UI'shomepagesetting to that page's installed (prefixed) name. Without it the UI falls back to the site's global page, or the controls grid. - Sign-in screen.
loginnames a code widget in the package. Someone who opens the application's route signed out is sent to the sign-in page as usual, and that page shows the application's widget instead of GEM's keypad, dressed in the entry UI's theme andapp.css. The widget is handed asignInfunction; see the sign-in contract. Sign-in itself is unchanged: the same credential check, the same session, the same two-factor rules. If the widget fails to load, GEM's keypad renders instead. - Selector.
listed: falsesetshidden_from_selectoron the UI. It stays reachable by link and by route. An application's entry UI that is listed carries an application badge in the selector. - Assets. Everything under
assets/in the package, other than theme assets, is retained at install and served at/app/<app_id>/assets/<path>, versioned by the package digest. Rewritten on upgrade, removed on uninstall.
Forgetting an application, or an uninstall that keeps its edited entry UI, hands the UI back to the site: chrome, theme, start page, sign-in screen and selector settings return to inherited defaults.
The Installed card names each application's route, with Open to launch it in a new tab, and a line saying what its interface asks for. The Install preview shows the same line before anything is written. Under it, Interface names the UI the install resolved the entry to and opens that UI's settings over the list — that is where the application's pages, zones and theme are edited, as distinct from Open, which runs it.
What the frontend is built from
A developer builds the interface from the widget system, not from a designer: a shell widget that draws its own navigation and embeds the pages underneath it (gem.components.PageEmbed), on a page marked Edge to edge, with the other pages composed in the page editor like any UI page. What widget code may rely on from GEM is the Widget API: commands, live values, data, navigation, a shared state store and event bus for the widgets on one UI, and the four system dialogs an application may replace. A package whose widgets do not compile on this GEM is reported in the compatibility list when it is inspected, before anything is written.
A standalone application
There is no separate "custom" mode: a standalone application is these settings together.
| Setting | What it gives you |
|---|---|
nav_layout: none | No GEM bars, drawn or loaded |
theme: none plus assets/app.css | Only the base stylesheet and yours |
| A start page marked Edge to edge holding one shell widget | The whole screen for your shell |
gem.dialogs.register(...) in the shell | Your own toasts, blocker, reconnect and PIN screens |
login | Your own sign-in screen |
The application still runs inside GEM's client, so it keeps the session, the reconnect and resubscribe handling, the role and scope checks, and the platform components (ZoneMap, Keypad, PageEmbed and the rest). That is the point of building it this way rather than as a separate web page.
Starting from the starter
The Install tab's Start from the Starter App builds a complete standalone application under the package id and name you give it, signed with this system's key. It then shows it for review and installs it through the same steps as an uploaded package. The starter has:
- an entry UI with
nav_layout: noneandtheme: none - a shell widget on the start page that draws a header with the application's name and the signed-in user, lists the UI's pages as tabs, leaving out edge-to-edge pages like its own (ordered by the page's
nav_orderconfig, then title), embeds the chosen one, and draws its own toasts - two ordinary pages, Welcome and Now on, to show that pages are still composed in UI Pages
- a sign-in widget, a PIN pad, as its sign-in screen
- an
app.csswith all of its styling
Open it from the Installed card, then make it yours: edit the widgets in UI Widgets and the pages in UI Pages. Rebuild it from the Create tab with its UI as the interface in step 3. A UI that is already an installed application's interface brings that application's app.css and other assets into the build. To rework app.css itself, lay the package out as a source folder (--unpack, below), edit the file, and build from the folder.
Installing
- Go to System → Applications → Install and choose a
.gemappfile. It uploads in chunks, so package size isn't limited by the usual message ceiling. - GEM reads the package and reports back without writing anything:
- identity, version, author and content digest
- whether the package's requirements are met (GEM version, drivers, licensed features, macro step types) — every unmet requirement is listed at once
- whether it contains code that runs on the server, and exactly which artifacts
- any settings the package asks for
- an item-by-item list of what will be created and what already exists
- Answer any settings, choose how existing items should be handled, and click Install.
Cancel, choosing another file, or leaving the page discards the uploaded package — nothing from it is kept on the controller until you install it.
Handling things that already exist
Each item that collides with existing configuration can be handled three ways:
| Choice | Effect |
|---|---|
| Use existing (default) | The package points at what's already there. The existing row is recorded as referenced and is never touched. |
| Copy under new name | A parallel row is created with a suffixed name. Nothing existing is changed. |
| Replace | The existing row is overwritten with the package's version. It is recorded as referenced, so a later uninstall will not delete it. |
An Already exists badge in the preview is a link — click it to open that record in a reference modal over the wizard and see exactly what Replace would overwrite, without losing the upload, the settings you have answered, or the choices you have already made on the other rows. The same applies to the artifact lists on the install result and on an installed application's detail panel.
Handling things that are missing
A package names its dependencies by name. When it points at something this installation doesn't have — a subsystem, a device, a command set — that item appears under Missing References, and the install is held until each one has an answer:
| Choice | Effect |
|---|---|
| Use an existing … | Pick a record already on this system. The package binds to it and records it as referenced, so uninstalling leaves it alone. |
| Create it | GEM creates a minimal record with that name. Offered only for subsystems, site modes and site spaces — the types where a name is enough to build a usable row. It is recorded as owned, so uninstalling removes it again. |
Once you have picked a record for a reference, a check link appears beside the dropdown — it opens the record you chose in a reference modal over the wizard. Picking the wrong row welds the package to it silently, and confirming which amp_1 you just selected should not cost you the upload, the settings you have answered and every resolution already made.
Click Re-check after answering to confirm the references resolve. This is not a warning you can skip: the first unresolved reference stops the install, and everything already written is rolled back.
Licence terms
A package can carry an agreement you have to accept before it installs. It is shown in full, before anything is written, alongside the provenance and the item list.
- Accepting records your username and the time against the installed application. It is shown on the application's card and in its detail panel, and the agreement itself is kept on that panel so what was accepted can be read back later.
- The terms are part of the package's signed content. If the package is signed, its terms cannot be altered on the way to you without the signature failing.
- An upgrade carrying the same agreement does not ask again — you already accepted that text.
- An upgrade carrying a changed agreement says so and asks again. Accepting the old version does not carry over.
A package may also carry terms marked as not requiring acceptance. Those are shown for information and never block the install.
Packages that run code
If a package contains any of the following, GEM lists each one and requires you to confirm you trust where the package came from before it will install:
- a custom driver or a web service — both execute on the controller
- a device script — command and response scripts run every time that device is talked to
- a macro with script steps
- a custom widget carrying its own source — compiled on install and then loaded by every operator browser, including the admin session that ran the install. A widget composed in the visual designer carries a layout, not source, and does not count.
The judgement is made on what each artifact actually contains, not on its type, so a widget bundled inside a UI or a page is found the same way a standalone one is. It is also made after the package's settings have been applied, since a setting can decide what a macro step turns out to be.
Treat this the way you would treat running an installer from the internet: a package from an unknown source can do anything your controller can do.
Settings
A package can ask questions at install time instead of hard-coding answers. A well-built package for a conference room asks which amplifier drives this room rather than shipping a device ID that only means something on the machine it was authored on.
Settings render as normal form fields, including catalog-driven dropdowns that list the actual devices, zones or pages on your system. See Building a package for how they are declared.
Secrets — API keys, passwords — are handled separately. They are never stored in the package or on the application record; they are written straight to a secure attribute on this installation, encrypted with this installation's own key. A package can therefore be handed to someone else without handing them your credentials.
A secret can only be written onto a device or zone the package itself created, or onto the application record. A package that asks to put a secret on a row that was already here — an access-control panel, a camera, an alarm transceiver — is refused and the skip is logged, because the install wizard shows a setting by its label and never says which record its answer lands on. Overriding that is a deliberate, separately-granted choice, and the write is recorded when it happens.
Name prefix — installing the same package twice
Packages built by GEM declare a Name prefix setting, and everything the package creates is named under it.
That is what lets the same package be installed more than once on one controller. Install Conference Room AV with the prefix room_a, then install it again with room_b, and you get two complete, independent sets of devices, zones and UI pages that don't collide.
Each copy is tracked separately: it has its own version, its own settings and its own ledger, and can be upgraded or removed on its own.
Upgrading
Install a newer .gemapp with the same package ID. GEM matches the new version's items against what the old one installed and:
- updates items that exist in both, including the pages and widgets inside a UI it ships, so new widget code takes effect
- creates items the new version adds
- removes items the new version no longer ships
- leaves alone anything you have edited since install, and tells you it did
That last point matters. If you renamed a zone or retuned a macro after installing, an upgrade will not quietly revert your work — it reports the item as changed and skips it. Tick Let the package's version win only when you want the package to take those rows over.
An upgrade also skips anything the application does not own. A row it only referenced — or one that merely happens to share a name with something the new version ships — is reported as not owned by this application and left exactly as it is. That is the same rule uninstall follows, applied to the other direction: a package can only overwrite what it created. Force overrides it when you genuinely want the package's version to take the row over.
Choosing which copy to upgrade
A package installed more than once has one copy per name prefix. When you upload a newer version, GEM lists every copy already installed and you pick the one to upgrade — you do not have to remember the prefix it went on under. Selecting a copy re-reads the settings it was installed with, so its name prefix carries forward and the new version lands on that copy rather than beside it. Those settings are a starting point, not a lock: change one, press Re-check to see the effect on the item list, and the upgrade is sent with the changed value. Switching to a different copy clears every answer and tick made for the previous one.
If you meant to add another copy instead, choose Install as a new copy and give it a prefix of its own.
If an upgrade fails partway, it is reported precisely rather than rolled back: the previous version's rows are still in place and some of the new ones have landed. Read the result, fix the cause, and run it again.
Removing
Uninstall removes the configuration the application created:
- items it owns are deleted, children first
- items it only referenced are left in place
- items you have edited since install are kept and listed, unless you tick Also remove items I have edited
- history is never touched — attribute history, monitor history and activity records outlive the configuration that produced them
What you built on the package
A package's items rarely stay alone. You put its widget on a page of your own, schedule its macro, build a zone on its device. Nothing is deleted until that has been looked at, and the Used by column in the details panel shows it per item before you click. Each thing named there — and each item the uninstall panel reports as blocking — is a link that opens the record in a reference modal over this page, so you can move or delete it without losing the choices you have already made in the dialog. Only records with an editor are linked; the rest stay plain text.
- Placements, UI entries, command bindings and schedules that exist only to point at a package item are yours, so the uninstall stops and changes nothing unless you tick Also remove the placements, UI entries and schedules I built that use these items. Placements on a page the package itself brought are removed with that page without asking.
- Anything that is a thing in its own right — a zone on the package's device, a site space set to its site mode, a site mode scoped to its site space — is never deleted by an uninstall. While one exists the button is disabled and the panel names it. Move or delete it first, or choose Forget, keep configuration and the package's items become ordinary configuration.
If an item still refuses to go for a reason that could not be foreseen, it keeps its place on the package's ledger and the application stays installed to hold it. The result says which items went and which did not, and running the uninstall again picks up from there. An item is never left behind unowned.
The same rule applies to an upgrade that no longer ships an item: one you have placed, scheduled or bound is reported as in use and released to you rather than taken away.
Any secret the application was given that wasn't aimed at a specific device or zone is stored against the application itself, and goes when the application does — a credential is not left behind attached to nothing. A secret written onto a device or zone follows that item's fate: it is removed with an item the application created, and left in place on one it only referenced.
The retained package file
Installing keeps a copy of the .gemapp alongside the installation. That copy is what the Package button on an application's card downloads, and what lets the application be re-installed or handed on without the original file.
Uninstalling leaves it in place unless you tick Delete the retained package file. Another copy of the same package installed under a different name prefix shares that one file, so it is only removed once no copy is using it.
Forget, keep configuration removes GEM's record of the application but leaves everything it built. Use this when you want to take a package's output over as your own configuration.
GEM still remembers which items a forgotten copy left behind, and the same goes for edited items an uninstall kept. If you later install the same package again, under the same name prefix, it takes those items back:
- The install preview marks them from an earlier copy and sets them to Replace.
- Their content becomes the package's again, new widget code included.
- They are the application's again, so it can upgrade and remove them.
Choose Use existing on a row to keep your version of that item instead. A same-named item the site made itself, or one another package has since taken, is never treated this way. If the reinstall fails, those items are released back to the site rather than deleted. Any content already replaced before the failure stays replaced, as with any other Replace.
Building a package
The Create tab builds a .gemapp from configuration already on this system, in five steps.
Everything you assemble there is saved as you work, as a draft under the package id: the details, the selection, the settings you turned into questions, the licence and the icon. Give the package an id and the tab saves itself a moment after each change; the Drafts picker at the top reopens any draft after a refresh or on another day, with its last build ready to download. New starts another package without losing the current one, Delete draft removes the draft and keeps any package already built from it. A second version of a package is its draft reopened with the version bumped and built again.
1 · Package details
The package ID (reverse-DNS, e.g. com.yourcompany.conference_av), version, name and author. Optionally a minimum GEM version — installs on older systems are then refused up front with a clear message rather than failing partway — a licence label, a description of what installing the package gives you, and an icon (png, jpeg, gif or webp under 180KB) that identifies the application wherever it is installed.
The name, author and description are what the installer sees on the package before deciding to install it.
2 · Contents
Select the items to include. Each row carries an open link — inspect a record before deciding whether it belongs in the package, without losing the selections already made across every other section.
The filter narrows every section at once, and a section's Select all takes only the rows the filter is currently showing. Sections with nothing in them are not listed.
Everything a selected item depends on travels with it: a macro brings its steps, a UI its pages, widgets, controls and macro buttons, a device its attributes and command/response scripts.
3 · Interface
Optional. Pick one of the UIs selected in step 2 to be the application's own screen, and the page it opens on. Once chosen, open it shows that UI's settings over this form, so its pages and widgets can be checked without taking the half-assembled package off screen. With nothing selected in step 2 yet, the hint's step 2 link expands that step in place. Then choose:
- Chrome: none, GEM's top bar, or GEM's side rail
- Theme: the site's, none, or a theme the package ships
- Sign-in screen: GEM's keypad, or one of this site's code widgets
- whether the UI is listed in the UI selector
A theme or sign-in widget chosen here is added to the contents. This step writes the manifest entry described above. Leave the UI unset for a package that only adds configuration.
4 · Settings the package asks for
This is the step that decides whether you have built a package or a snapshot of this site.
Press Scan selection. GEM reads the artifacts you selected and reports two things:
- Points outside the package — every reference to configuration that is not included. A zone pointing at
main_ampwill look for a device calledmain_ampon the target system and fail if there isn't one. - Attribute values — the addresses, ids and credentials travelling with your devices and zones.
Tick any of them to turn it into a question. A reference becomes a dropdown listing the target system's own records of that type; an attribute becomes a text field. Each gets a setting key and a question you can edit.
Both ends of a reference row, and the artifact an attribute sits on, are links into the reference modal — the scan and everything you have already ticked stay behind it. Deciding whether an address or a credential is specific to this site is a question about the whole row it belongs to, not about the one value the scan printed. Composite references (rendered as name (in subsystem)) name no single row and stay plain text.
Tick Secret on an attribute — GEM pre-ticks it for anything that looks like a credential — and the value is removed from the package entirely. The installer is asked for it, and the answer is written straight to a secure attribute on the target, encrypted with their key. That is what lets you hand a package to someone else without handing over your credentials.
Other questions declares a setting that isn't a reference or an attribute. Reference it from an artifact as {{param.your_key}}.
Re-scan whenever you change the contents. If you build against a stale scan, any setting whose target is gone is reported as not applied rather than silently dropped — the package builds, but those values are still hard-coded.
Roles and permissions
A package can ask that a role of the installing site be allowed to read and edit a few tables from the package's own screens — the schedules it installed, its notification profiles, its alarm definitions. Each grant is a setting the installer answers with one of their roles (the package never creates roles), a list of tables, and which of read, create, edit and delete the role may do. Reference the role from the package's widgets through gem.user for presentation; the server enforces the grant itself.
A grant reaches only rows the package owns and rows that role creates through the package, never the site's own rows in the same table. Rows a granted user creates become the package's: they show its badge, an upgrade neither replaces nor removes them, a plain uninstall keeps them as the site's work and a forced one removes them. The tables that can be granted are configuration only — nothing that executes, no devices or zones, nothing a user or role is made of, none of the rows that gate access — and a granted edit can never re-home a row to another package, flip an elevation flag, or point a schedule or trigger at an elevated macro.
In the manifest:
"params": [{"key": "manager_role", "type": "string", "required": true,
"option_source": {"type": "query", "model": "auth_role", "value_field": "name", "label_field": "name"}}],
"grants": [{"role": "{{param.manager_role}}", "tables": ["macro_schedule", "notification_profile"],
"operations": ["read", "insert", "update", "delete"]}]
The installer sees every grant spelled out before installing, under the licence terms.
5 · Licence agreement
Paste an agreement to be shown to whoever installs the package. With Require the installer to accept ticked (the default when there is any text) they must tick to continue, and their username and the time are recorded against the installed application.
The text lives in the manifest, which is covered by the package signature — a signed package's terms cannot be altered in transit without the signature failing. Leave it empty for no agreement.
Then build
Click Build Package, then Download. GEM automatically:
- generates the artifact index from what's actually in the package, so the manifest can never disagree with its contents
- rewrites the names of everything included to use the Name prefix setting, and rewrites the references between them to match — references to things outside the package are left alone. Custom drivers and attribute definitions keep their names: a driver is what a device's driver column points at and every copy of the package shares it, so a second install adopts the driver already there
- writes the correct dropdown definition for every reference setting, so the installer picks a real record rather than typing a name
- records which macro step types the package uses, so an older target refuses it up front
- stamps a content digest over the whole package and signs it
Building from a script
For rebuilding a settled package from CI rather than authoring one, scripts/build_app.js takes the same specification as a JSON file:
# what in this selection is welded to this site, and the bindings to write
node scripts/build_app.js package.json --analyze
# build it
node scripts/build_app.js package.json --out conference_av-1.4.0.gemapp
The spec holds manifest, items and param_bindings — run --analyze first, paste the binding lines it prints, then build. It exits non-zero if any setting could not be applied, so a CI job cannot ship a package that quietly kept your site's values. It connects to the configured database (a package is built from configuration) but does not start a server or load drivers.
A package as a source folder
A built package can be laid out as files and rebuilt from them, which is what puts a package under source control — one file per artifact to review and diff, real image and audio files, no database on the build machine.
# lay a built package out as a folder
node scripts/build_app.js --unpack conference_av-1.4.0.gemapp ./conference_av
# edit, commit, then rebuild it (no database needed)
node scripts/build_app.js ./conference_av --out conference_av-1.5.0.gemapp
The folder is the package's own layout:
| Path | What it holds |
|---|---|
manifest.json | Identity, requirements, settings, entry, licence terms. The artifact index, digest, format version and the list of macro step types are not here — the build derives them from the folder, so they can never disagree with it. |
artifacts/0001_….json | One artifact per file, {key, type, data}, read in filename order. That order is install order, so keep the numeric prefix — a zone must follow its subsystem. A file without a key is keyed type:name. |
assets/… | app.css, audio clips, blueprints/, themes/<name>/… — every regular file under it travels; dotfiles do not. |
The artifacts in a folder are in their packaged form: names already carry the {{param.name_prefix}} token and settings are already {{param.key}} tokens, exactly as the Create tab wrote them. New artifacts are written the same way. A parameter an artifact references but the manifest does not declare still refuses to build. The unpack drops the signature — it committed to a digest the next build will not reproduce — and the rebuild signs afresh with this machine's publisher key (or --no-sign).
Definitions the Create tab cannot start from — attribute definitions for an application's own attributes, before any version of it is installed — are written here as artifacts of type attribute_registry, one per (system_target, name, context).
What can go in a package
| Group | Types |
|---|---|
| Devices | Custom Drivers, Command Sets, Device Types, Devices (with attributes and command/response scripts), Subsystems, Zones, Attribute Definitions |
| Automation | Macros (with steps), Schedules, Triggers, Variables |
| Monitoring | Alarms, Monitors, Monitor Tags, Reports, Notification Profiles |
| Interface | UIs (with pages, widgets, controls, zones and macro buttons), UI Pages, UI Widgets, UI Themes, Zone Groups |
| Other | AV Sources, AV Zones, Channel Providers, Web Services, Holiday Calendars, Site Modes, Site Spaces (with their floor plans), Sites |
Custom widget source travels with the widget and is compiled automatically after install. Theme fonts and images travel as package assets.
Attribute definitions are the Attribute Registry rows that give an application's own attributes a type, options, a description, a unit and the secure and history flags — what makes the attribute editor render them properly. They install as rows the package owns; the registry seeder, which otherwise removes every definition it does not know at boot, leaves them standing until the package is removed. A definition's name is never namespaced by the name prefix (code reads it), and an overwrite of a definition already on the target may not change its per-user override flag. The Create tab offers only definitions a package installed — a definition added by hand does not survive the next boot — so new ones are authored in a source folder.
Floor plans. A site space's plan image travels as a file under assets/blueprints/ rather than as text inside the artifact, and is written back into the space on install. It is never served from the package's public assets: a building's plan is only ever read by signed-in clients.
Clips and other files. Anything else an application needs at run time — audio clips, images, its stylesheet — goes under assets/ and is served at /app/<app_id>/assets/<path> with no session, which is what lets a speaker fetch it. An Announce step in a packaged macro plays a clip by a path relative to that folder, clips/chime.mp3.
Versions and identity
- Package ID (
app_id) is the identity used for upgrades. Keep it stable across versions; changing it makes a different application. - Version is dotted-numeric (
1.4.0). Installing a lower version than the one present is refused; when the wizard detects one it offers an explicit Install this older version option. - Item keys are derived from each item's type and its name as authored, before the name prefix is applied. Keeping names stable across versions is what lets an upgrade match old to new. If you rename something between versions, the upgrade removes the old row and creates a new one.
An installed copy is identified by both the package ID and its name prefix, so a page that links to a package passes both: /admin/applications?app_id=com.example.tiles&instance=west opens that copy's detail panel directly. Leaving the instance off opens the first copy.
Signing and publisher trust
A package can carry code that runs on your controller with full server privileges. A signature is how you tell where a package came from and whether anyone changed it on the way.
Packages built by GEM are signed automatically. The Publishers tab is where you manage the rest.
Trust levels
Every package is judged when you open it:
| Level | Meaning |
|---|---|
| Published by GEM | Signed with the key built into this release. |
| Verified publisher | Signed by a publisher GEM has vouched for. |
| Trusted publisher | Signed by a key you added on this install. |
| Signed, publisher not trusted | The signature is valid and the package is unmodified — but you have not said you trust whoever made it. |
| Unsigned | Nothing proves who made it or that it is unchanged. |
| Revoked key | Signed with a key marked compromised here. Always refused. |
| Invalid signature | The signature does not match the contents. Always refused. |
The first three are trusted: their packages install their code without asking you to vouch for it. That is the practical reason to sign and to pin.
What a signature actually protects
The signature covers a digest of the manifest, every artifact and every asset. Change one character of one artifact and it stops verifying. The package can be re-compressed or re-packed freely — that does not break it — but its contents cannot be altered without detection.
Two attacks are specifically closed:
- A signature cannot be lifted off a package you trust and attached to a different one: the package id and version are part of what gets signed.
- A package cannot carry its own key while claiming a trusted publisher's fingerprint: the fingerprint is recomputed from the key in the package and compared, never taken at face value.
Trusting a publisher
- Get the publisher's public key and its fingerprint.
- Confirm the fingerprint through a different channel — a phone call, an in-person handover, an existing trusted email thread. This comparison is what the whole thing rests on; a key and a fingerprint that arrive together prove nothing.
- Paste the key under Publishers → Trust a Publisher, record how you verified it in the notes, and save.
Their packages then install without the code prompt.
Your own signing identity
The first package you build generates a signing key for this install. Its public half is shown under Publishers → Your Signing Identity; hand it to your customers so their controllers can trust your packages.
The private key lives in .package_signing_key at the install root, readable only by the GEM user, and never in the database or in any backup or export. Treat it the way you would treat a code-signing certificate:
- Anyone with that file can sign packages in your name.
- If you lose it, you cannot sign updates to your existing packages — customers will see a new, untrusted key and must pin it again.
- If it leaks, tell your customers to revoke the fingerprint.
Revoking
Revoke marks a key compromised. Packages signed with it are refused from then on, and the refusal cannot be overridden by acknowledging them — that is the point.
Applications already installed from that key are left running and listed for you. Pulling a site's lighting or HVAC configuration because a signing key leaked would be an outage of your own making; deciding what to do about them is yours.
You do not need to have pinned a key to revoke it. Revoking a fingerprint you have only been warned about pre-empts the package before it ever arrives.
Remove is the milder action — it simply stops trusting a key, without declaring it compromised. Packages from it go back to being installable with a confirmation.
Code policy
Under Publishers → Code From Untrusted Publishers:
- Ask me (default) — a package carrying drivers, web services or script steps installs if you confirm you trust its origin.
- Trusted publishers only — code installs only when signed by a publisher on your list. Confirming is not enough.
Switch to Trusted publishers only once your publishers are set up. It is the setting that makes "someone talked an operator into ticking a box" stop being a way onto your controller.
Either way, an invalid signature or a revoked key is always refused.
A note on what signing does not tell you
A signature proves origin and integrity. It does not prove the package is good. A publisher you trust can still ship a package that misconfigures your site — trust means "I know who to call", not "this cannot hurt me". Read the install preview.
See also
- Import / Export — moving individual artifacts between systems
- Backup & Restore — full-installation snapshots for disaster recovery
- Roles — who can reach these pages