Skip to content
Article Apps and windows
☀️ Light Clean and bright
📜 Sepia Warm and vintage
🌤️ Light Gray Subtle and neutral
🔵 Light Blue Calm and serene
🌙 Dark Easy on the eyes
🌑 Dark Gray Deep and modern
🔷 Dark Blue Professional and sleek
🌿 Forest Calm and natural

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 .toolbar rule 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 window or document, 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:

ColumnPurpose
sp_owner_uidThe owning user. Drives ownership filters
sp_shared_withSharing 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:

  • DomBuilder for nodes
  • Widgets and WidgetEngine for components
  • ApiLogic for 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:

  1. The package is uploaded through the system app.
  2. The manifest is read and validated.
  3. schema.json is applied through uploadSchema, which fails closed on anything malformed.
  4. 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.