Skip to main content

Architecture

The default installable solution is a metadata Marketplace App (hosting: runtime). The platform validates, stores, installs, renders UI, persists entities, and runs Business Flows on FlowRunner.

Developers do not ship a /qefro server for Restaurant, Clinic, Real Estate, Booking, or CRM apps. That SDK path is external integration.

Tenant → workspace → app

Tenant
└── Workspace
├── Channels (WhatsApp, …) # owned by workspace
└── One primary application # install of a published package
  • Catalog (global): signed packages published by platform admins only.
  • Install (per workspace): tenant admins activate a published version and configure settings. Runtime apps have no /qefro binding.
  • Channels: WhatsApp (and similar) bind to the workspace, not to package settings.

Scaffolding a new app: App scaffold. Publishing into the catalog: Publishing.

Runtime vs SDK

Marketplace AppExternal Integration
DefinitionMetadataSDK
RuntimeQefro RuntimeExternal server
Business logicQefro RuntimeCustomer system
StorageQefro managed storageExternal system
ToolsRuntime capabilitiesSDK capabilities
EventsRuntime eventsSDK events
FlowQefro FlowRunnerQefro FlowRunner + SDKAdapter
Marketplace App:
metadata → installer → Qefro Runtime → entity tools → managed storage

External ERP / POS / CRM:
customer system → SDK → /qefro → FlowRunner + SDKAdapter

SDK Connections (hosting: external) are how Focus ERP / Yaaz attach — not Marketplace Apps. See Managed apps only as a pointer to the removed /qefro Marketplace model.

The pipeline

StageComponentTenant-scopedPage
App packageMetadata directory (entities/ required for hosting: runtime)n/aManifest
RegistryGlobal signed catalogNoPublishing
InstallerTenant activationYesInstallation
Qefro RuntimeFlowRunner + entity tools + event busYesWorkflows
Managed storageDocuments for declared entitiesYes (per op)Managed storage
Connector bridgeShared pool for external SoRPool shared; calls carry tenantConnectors
SDKAdapterExternal /qefro toolsYes (per connection)Runtime vs SDK
Portal rendererNative widget registryYesPages

Registry

The registry is the global catalog of published solutions and connectors. It is not tenant-scoped: every tenant resolves against the same signed catalog.

Who can write: only platform admins (UUIDs in QEFRO_PLATFORM_ADMIN_IDS on solution-service). Tenant / workspace admins install from the catalog; they cannot publish or yank versions.

Responsibilities:

  • Accept signed packages (manifest, components, signature, signature_kid, publisher_id).
  • Verify the Ed25519 signature over id|version|checksum before storing.
  • Track version lifecycle: draft → published → deprecated → yanked.
  • Resolve dependency constraints (connector versions) at install time.

Packages are immutable once published: a new version is a new package. See Publishing.

Installer

The installer turns a published version into a tenant installation. Installing a solution registers flows and prompts with the runtime — the solution itself never executes anything.

Key properties:

  • Capability negotiation: the granted set is the intersection of the capabilities requested by the package and what the installation grants. It is computed at install time, stored with the bundle, and re-checked on every host call. See Capabilities.
  • Activation plan: the install pipeline executes explicit steps — enable tenant connectors, register flows, ensure connector pool, store secrets, register the UI bundle. The wizard shows requested vs granted capabilities before activation.

Runtime

The runtime is the single execution engine of the platform. For Marketplace Apps it provides:

  • Workflow execution — installed workflow definitions run on FlowRunner (ask, tool, condition, delay, approval, challenge, complete). Metadata flows compile into the same BusinessFlow model. See Runtime concept.
  • Entity toolsentity.<id>.create (and siblings) persist declared entities through managed storage. execution: runtime — no SDKAdapter.
  • Runtime data sourcesmetrics, executions and workflows targets (capability runtime.query).
  • Event busui.* lifecycle events and business events. See Events.
info

A solution never executes workflows itself. Installation registers definitions; the runtime owns execution.

Domain boundary (ADR-006)

Runtime is domain-agnostic. It executes, routes, persists conversation drafts, authorizes, and orchestrates. It does not know what a reservation, appointment, guest, table, or visit type is.

LayerOwns
App (entities/ + workflows/ + manifest)Business nouns, field names, choice values, when to confirm
ManifestGeneric conversation_slots, trigger reply_signals / required_slots / identity
RuntimeExtract declared slots, map {chip_prefix}:{value} chips, identity OTP, fire the pending declared capability on generic “yes”

Runtime-owned protocol vocabulary (Runtime may understand these): conversation_slots, required_slots, forbidden_slots, reply_signals, confirmation, identity_challenge, chip_prefix, chip_value.

App-owned opaque vocabulary (Runtime must never special-case, even if common): guest_name, pickup_date, room_type, visit_type, table_id, order_id, …. If it is app-defined, it is opaque.

A new app (Hotel, Salon, Real Estate, …) plugs in by declaring entities, slots, and triggers — no Runtime change. The contract is ADR-006 (qefro-plugin-platform/docs/adr-006-domain-agnostic-runtime.md).

Managed storage

Marketplace App entities persist through Qefro-managed storage. Packages never receive a Mongo connection string and never invent storage-service URLs.

entity.reservation.create (Runtime capability)
→ storage-service /v1/internal/storage/*
→ MongoDB `managed_apps` ({solution_slug}__{logical})

Isolation, reserved metadata, soft delete, and audit are enforced by storage-service. See Managed storage.

SDK Connections persist only via ctx.storage inside /qefro — never from workflow/UI YAML targeting storage/*.

Connector bridge

connector data sources that target declared external pool connectors never call a connector directly. They are forwarded through the connector bridge, which:

  • routes POST /v1/route calls to a shared, stateless connector pool (containers named qefro-connector-{name}-{version}-{id} — never per-tenant),
  • attaches the tenant context to every call,
  • enforces that the calling solution holds connector.invoke and declared the connector in its manifest.

Sources whose target is {solution}/{tool} for an SDK-hosted install skip the pool bridge and call that installation /qefro (gated on runtime.query). Metadata apps use type: entity sources instead — see Sources.

See Connectors and the connector reference.

Portal renderer

The portal renders solution UIs natively at /app/solutions/ui/:name/:pageId? using its own component registry:

EngineRole
Solution UI hostLoads the tenant bundle, scoped theme container, page tabs, lifecycle events
Theme enginetheme.yaml → CSS custom properties on the solution container only
Navigation engineBundle navigation under Managed solution in the portal sidebar
Widget registryClosed widget-kind list rendered with platform UI primitives
Layout engineResponsive grid (1 column on mobile, columns at ≥ 1024 px)
Data sourcesCapability-gated fetches from runtime, own-app /qefro, or pool bridge
CapabilitiesThe ui.* host API, implemented in-process
UI boundaryError boundary + schema coercion — broken definitions degrade to a scoped error card

Nothing from a package ever executes: no iframes, no postMessage, no injected scripts. See Security.

Data ownership

DataOwnerTenant-scoped
Published packages (manifest, UI, workflows)Registry / solution-serviceNo — global catalog
Installations, settings, granted capabilitiesInstaller / solution-serviceYes
Workflow executionsRuntimeYes
Solution application documentsstorage-service → Mongo managed_appsYes (per op)
Running connector containersConnector managerNo — shared pool
Connector credentialsSecret managerYes
UI event log (ui_events)RuntimeYes

Trust boundaries

  1. Validation rejects unknown widget kinds, unknown capabilities, icons outside the closed set, non-grid layouts and executable assets — see Validation.
  2. Signing binds every package to id|version|checksum with Ed25519.
  3. Negotiation caps capabilities at install time.
  4. Re-check gates every host call and every data-source fetch at runtime.
  5. Boundaries ensure a malformed definition degrades to a scoped error card — the portal itself never crashes.