Apps and windows
An app in SepoDesk is a self-contained unit: its own manifest, its own database schema, its own controllers, its own front-end entry point. The system discovers apps rather than being compiled against them, which is what makes installing one into a running instance possible.
The window model
The desktop shell hosts apps as windows. A window can be moved, stacked over or under its neighbours, and closed. There is no limit on how many are open at once.
The important consequence for app authors is that your app is not alone on the page:
- Scope every selector, class name and CSS custom property to your app. A bare
.toolbarrule will style someone else's toolbar. - Never touch
document.body, global styles, or another app's DOM. - Bind listeners to your own root node, not to
windowordocument, unless you also remove them on unmount. - Keep state inside your app instance. Two windows of the same app may be open simultaneously and must not share mutable state.
- Clean up on unmount: timers, intervals, observers, in-flight requests.
The shell keeps an app's state alive for as long as its window exists. Closing the window is the teardown signal.
Anatomy of an app
apps/<AppName>/
├── manifest.json identity, version, entry points, permissions
├── schema.json database tables this app owns
├── Controllers/ server-side handlers
└── assets/ the app's JS, CSS and templates
manifest.json
The manifest is how the system finds the app. It declares the app's identity and
version, its controller entry points, and what it is allowed to do. Discovery
walks the apps directory reading manifests; AppsConfig.php holds the resolved
registry.
Version it properly. The updater and the app installer both compare versions, and an app that never bumps its version will never be seen as updated.
schema.json
An app declares the tables it owns rather than shipping migrations. Each column
entry carries FieldName, DataType, IsPrimaryKey and the rest of its
definition.
The engine's uploadSchema diffs the declaration against the live database and
issues the necessary CREATE or ALTER. It works against both MySQL and
SQLite. Before any DDL runs, assertSafeSchema validates every identifier,
type, index and foreign key, and fails closed on anything it does not
recognise — a malformed schema stops the install rather than half-applying it.
Two columns are conventional on any table holding user data:
| Column | Purpose |
|---|---|
sp_owner_uid | The owning user. Drives ownership filters |
sp_shared_with | Sharing list. Drives shared-record access |
The query and CRUD layers apply ownership and sharing filters automatically when these are present. Omitting them means opting out of that protection, so omit them only for genuinely global reference tables.
Controllers
Requests reach an app through /api/v2/data. CoreDataApiController resolves
the target app from the registry, and ControllerFactory instantiates the
controller inside an AppShell.
Shared behaviour comes from traits rather than a base class — the auth-user, sharing, record-injection, formatter, CRUD-security and response traits compose into whatever a given controller needs. Use them; the security-relevant ones in particular are not optional decoration.
The front end
An app's client code is a module that the shell mounts into a window. Build against the shipped primitives:
DomBuilderfor nodesWidgetsandWidgetEnginefor componentsApiLogicfor every server call
Do not reach for fetch directly. ApiLogic wraps EcmaScriptController,
which handles caching, de-duplication, batching, the circuit breaker, and token
refresh. Bypassing it means reimplementing all of that, badly.
There are no external dependencies in this project. If a helper you want does not exist, write the smallest version that does the job and put it in the shared utilities.
Bundled apps
Around twenty apps ship with the system. They cover:
- System administration — package upload, backup management, update history, installer control
- Data tools — the multi-table CRUD surfaces and record editors
- Query building — saved queries, grouping, aggregates and calculations over the formula engine
They are ordinary apps. Read one when you are unsure how something should be
done; SysBackupManager and SysPackageUpload are good short examples of an
app that talks to a system endpoint and updates its own UI in place.
Installing an app
An app is installed into a running system, not deployed alongside it:
- The package is uploaded through the system app.
- The manifest is read and validated.
schema.jsonis applied throughuploadSchema, which fails closed on anything malformed.- The app is registered and becomes available in the launcher.
Front-end assets are picked up by the bundler, so rebuild the bundles after installing an app whose code belongs in the shared bundle.
Events
The shell and the system apps communicate over DOM events rather than direct
calls. sp:system-updated is emitted after a successful system update so that
open windows can refresh their own state without a browser reload — the update
UI listens for it and redraws client-side.
Follow the same pattern for cross-app signalling: emit a namespaced sp: event,
and let interested windows subscribe. Do not reach into another app to call a
method on it.