Design decisions
The SDK’s architecture is recorded as decision records: short documents that
state a problem, the choice made and what it rules out. They are public
because the constraints they impose show up in the API - why a plugin never
sees iface, why a job is a plain function, why the two packages version
independently - and a user who hits one of those constraints deserves the
reasoning, not just the error.
Each row links the full record on GitHub from its number. The status is the record’s own: an accepted decision is in force, a proposed one is still under discussion and not yet what a release does, and a superseded one has been replaced by the later record its summary names. The date is when the record was written.
| Number | Title | Decision | Status | Date |
|---|---|---|---|---|
| decision-1 | Python declarative API is the plugin authoring source | Plugins are written as Python declarations - decorators and typed functions - that the SDK inspects; authors never maintain a separate JSON description. | Accepted | 2026-10-15 |
| decision-2 | The plugin description is the shared schema contract | One plugin description derived from the declarations is the contract every tool and renderer shares, with a versioned JSON form for the TypeScript side. | Accepted | 2026-10-15 |
| decision-3 | Development and distribution artifacts are separate | qgis-sdk is developer tooling and qgis-sdk build produces a self-contained plugin ZIP, so end users install nothing but the plugin. |
Accepted | 2026-10-15 |
| decision-4 | QGIS-aware networking uses an async-first transport | Networking goes through one injectable, QGIS-aware transport that is asynchronous by default; a blocking call is always explicit. | Accepted | 2026-10-15 |
| decision-5 | Rust algorithm implementations are post-v1 and backend-neutral | Algorithms stay in Python for 1.0; the SDK imposes no Rust toolchain on authors or users. | Accepted | 2026-10-15 |
| decision-6 | Separate models services core and runtime adapters | The Python package is split into QGIS-free models and service contracts, a core, and native adapters that alone import QGIS. | Accepted | 2026-10-16 |
| decision-7 | Use layered pytest fixtures properties and behavioural contracts | Testing is layered pytest fixtures over fakes with a deterministic scheduler, plus behavioural contract tests that fakes and native adapters share. | Accepted | 2026-10-16 |
| decision-8 | Separate check test dev and build workflows | check, test, dev and build are separate commands with separate responsibilities; check never installs, builds or rewrites. |
Accepted | 2026-10-16 |
| decision-9 | Declare allowlisted typed bridge endpoints with runtime owned routers | Bridge endpoints are declared explicitly with typed request and result models and stable ids, and the runtime owns the router. | Accepted | 2026-10-16 |
| decision-10 | Use dataclass authoring and schema driven TypeScript generation | DTOs are standard-library dataclasses in a documented typing subset; the TypeScript types and clients are generated from the derived schema. | Accepted | 2026-10-16 |
| decision-11 | Use one context injected class per Processing algorithm | Processing algorithms were one context-injected class each; decision-42 reshaped them into plain functions on a provider. | Proposed | 2026-10-16 |
| decision-12 | Use one context injected controller per UI surface | Superseded by decision-55: UI surfaces are plain bound classes composed from curated components, not controller classes with a build() factory. |
Superseded | 2026-10-16 |
| decision-13 | Use runtime bound Celery inspired local jobs | Local jobs are runtime-bound and owned by the scope that submitted them, with typed handles for cancellation and completion. | Accepted | 2026-10-16 |
| decision-14 | Enforce QGIS free import boundaries during check | check enforces the import boundaries: a QGIS import outside a native module, or a pure module depending on a native one, is a check error. |
Accepted | 2026-10-16 |
| decision-15 | Refresh development through owned runtime generations | qgis-sdk dev reloads through owned runtime generations over a development-only control channel that never ships in a ZIP. |
Accepted | 2026-10-16 |
| decision-16 | Separate reusable layer styles from UI themes | Layer styles are reusable resources applied through the layer service, separate from UI themes; the renderer API is not mirrored. | Accepted | 2026-10-16 |
| decision-17 | Deliver a qualified Python first vertical slice for v1 | 1.0 is one qualified Python-first vertical slice - a reference plugin through new, check, test, dev and build - not a broad wrapper set. | Accepted | 2026-10-16 |
| decision-18 | Ship a framework neutral bridge and explicit web scaffolding choices | The TypeScript bridge is framework-neutral with optional adapters; the root import never loads a framework, test utilities or a live connection. | Accepted | 2026-10-16 |
| decision-19 | Prove a non destructive local GIS reference workflow | The reference workflow is a non-destructive local GIS task on a fixture dataset; it never waits on an external service and never edits the source layer. | Accepted | 2026-10-16 |
| decision-20 | Enforce scoped host authority and stale reference failures | Web views receive opaque session-scoped handles; a stale, foreign or removed handle fails explicitly instead of resolving by name. | Accepted | 2026-10-16 |
| decision-21 | Bound job ownership and refuse unsafe runtime replacement | Jobs submitted from a view belong to the view, disposing the view cancels them, and a runtime replacement with live work is refused. | Accepted | 2026-10-16 |
| decision-22 | Use strict inputs extensible results and experimental contracts | Inputs are strict - unknown fields and implicit coercion are rejected - results are extensible, and experimental contracts are marked as such. | Accepted | 2026-10-16 |
| decision-23 | Adapt Tauri invocation resource and testing patterns to QWebChannel | The bridge follows Tauri’s shape over QWebChannel: a small invocation kernel plus generated typed clients that validate responses. | Accepted | 2026-10-16 |
| decision-24 | Release the Python SDK and TypeScript bridge at one shared version | Superseded by decision-25 before any release shipped under it: the Python SDK and the TypeScript bridge were to share one version number. | Superseded | 2026-10-24 |
| decision-25 | Version each package independently behind a shared contract version | Each package versions independently from its own conventional commits behind a shared contract version; a release stamps only the package it publishes. | Accepted | 2026-10-24 |
| decision-26 | Derive the bridge allowlist and the generated client from one declared manifest | One declared manifest drives the bridge allowlist, the handshake and the generated client; nothing is registered twice. | Accepted | 2026-10-24 |
| decision-27 | Expose curated host services to web views as generated namespaces | Curated host services - layers, notifications, styles, settings - reach web views as generated namespaces on the host surface. | Accepted | 2026-10-24 |
| decision-28 | Declare streams as operations and generate their events not the frame envelope | A stream is an operation in the same id namespace as actions; its events are generated types, the frame envelope is not. | Accepted | 2026-10-24 |
| decision-29 | Keep READMEs minimal and hold reference prose in backlog guides until the docs site | READMEs are landing pages; the reference prose lives in the guides the documentation site renders. | Accepted | 2026-10-24 |
| decision-30 | Derive declaration ids and titles from names deterministically, never randomly | A missing declaration id or title is derived deterministically from the decorated name, never generated randomly. | Accepted | 2026-10-25 |
| decision-31 | Bind the plugin context as a class attribute, not a constructor argument | The runtime binds the plugin context as a class attribute, so self.ctx is readable everywhere including __init__ and the class stays plain outside a runtime. |
Accepted | 2026-10-25 |
| decision-32 | Offer the curated services on PluginContext and build the web-view surface from it | PluginContext carries the curated services, and the web-view host surface is built from that same context. |
Accepted | 2026-10-25 |
| decision-33 | Rename the npm package to @archont561/qgis-sdk and suffix the package directories | The npm package is @archont561/qgis-sdk, the counterpart of qgis-sdk on PyPI, with capabilities as subpath entries rather than package names. |
Accepted | 2026-10-25 |
| decision-34 | Hand CLI commands a runtime through ctx.obj and library functions the same runtime as a parameter | CLI commands receive one CliRuntime through ctx.obj and library functions take the same runtime as a parameter; there are no globals. |
Accepted | 2026-10-25 |
| decision-35 | Split the Python SDK into a runtime, a tools package and a meta kit behind one contract | Python ships as a stdlib-only runtime (qgis_sdk), a tools package (qgis_sdk_tools) and a meta kit (qgis-sdk), around one contract. |
Accepted | 2026-10-25 |
| decision-36 | Cut the three Python distributions before the contract freeze | The three Python distributions were cut in the 0.5.0 line; the contract freeze is a separate decision and means the runtime’s 1.0.0. | Accepted | 2026-10-25 |
| decision-37 | State support as a matrix of demonstrated targets | Support is a matrix of demonstrated targets held once as data, each row qualified with evidence or unqualified with a named blocker. | Accepted | 2026-10-25 |
| decision-38 | Declare jobs as plain functions with the JobContext injected | A job is a plain function whose first parameter is the injected JobContext and whose other parameters are typed keyword inputs. |
Accepted | 2026-10-25 |
| decision-39 | Expose optional services as capability attributes that refuse by name | Optional services are capability attributes that return the host’s instance or refuse by name; author code never checks for None. |
Accepted | 2026-10-25 |
| decision-40 | Make the root entry the page import and keep generated plugin clients import-free | The root of @archont561/qgis-sdk is the page import - a barrel over errors, declarations and the bridge - and generated clients import nothing. |
Accepted | 2026-10-26 |
| decision-41 | Pin the scaffolded page to the released bridge and switch it to the workspace bridge only by explicit flag | The scaffolded page pins the released bridge exactly; the release pipeline moves the pin and only an explicit flag switches to the workspace bridge. | Accepted | 2026-10-26 |
| decision-42 | Declare algorithms as plain functions on providers the plugin owns | Algorithms are plain functions decorated with @provider.algorithm on a Provider the plugin lists, with the AlgorithmContext injected. |
Accepted | 2026-10-26 |
| decision-43 | Generate the QGIS host artifacts in build and dev instead of scaffolding them | build and dev generate the QGIS host artifacts - metadata.txt, the classFactory - so the source tree holds only the plugin. |
Accepted | 2026-10-26 |
| decision-44 | Generate a typed bridge schema module and build the client from it at runtime | qgis-sdk bridge generate writes a typed schema module, and QgisSdk builds the client from it at runtime. |
Accepted | 2026-10-26 |
| decision-45 | Bind actions to the Desktop with stackable toolbar and menu decorators and plain surface classes | Desktop bindings are stackable @toolbar, @menu and @shortcut decorators over @action, and surfaces are plain classes. |
Accepted | 2026-10-26 |
| decision-46 | Ship UI helpers as declaration kinds, standard surfaces, headless web bindings and themed defaults | UI helpers ship as parameter kinds complete across hosts, standard surfaces, headless web bindings and themed defaults. | Accepted | 2026-10-26 |
| decision-47 | Build web UI on a framework-free store core with per-framework adapters and ship components as qgis-sdk-ui | Web UI is a framework-free store core in the bridge root with per-framework adapters, and the components ship as qgis-sdk-ui. |
Accepted | 2026-10-26 |
| decision-48 | Give the QGIS Python console the curated services and plugin handles, never iface | The QGIS Python console gets the curated services and plugin handles through qgis_sdk.console.connect, never iface. |
Accepted | 2026-10-26 |
| decision-49 | Expose the runtime to agents over a loopback transport with the MCP server in the tools for developers and users alike | Agents reach the runtime over a loopback transport, with the MCP server in the tools and separate developer and user modes. | Superseded | 2026-10-26 |
| decision-50 | Declare third-party Python packages as externals with a startup probe and vendor only pure-Python ones | Third-party Python packages are declared as externals with a startup probe; only pure-Python ones are vendored into the ZIP. | Accepted | 2026-10-26 |
| decision-51 | Snapshot the plugin’s own surfaces as a structured tree plus screenshot for agents and tests | A UI snapshot of the plugin’s own surfaces is a structured tree plus a screenshot, for agents and tests alike. | Accepted | 2026-10-26 |
| decision-52 | Run one plugin class on three hosts - QGIS Desktop, QGIS Server and standalone PyQGIS apps | One plugin class runs on QGIS Desktop, QGIS Server and standalone PyQGIS apps from a declared host set; a capability a host lacks refuses by name. | Accepted | 2026-10-26 |
| decision-53 | Run offloaded job steps as QgsTasks under QgsTaskManager and keep the step scheduler on the driving thread | Offloaded job steps run as QgsTasks under QgsTaskManager, and the step scheduler stays on the driving thread. |
Accepted | 2026-10-26 |
| decision-54 | Cover every QGIS service and layer kind through the contract-fake-native-host-grant pattern under one grant vocabulary | A QGIS service exists only when its contract, models, fake, native adapter, host operations and grants land together. | Accepted | 2026-10-26 |
| decision-55 | Compose plugin UI from curated components and bound surface classes, and generate nothing from an action | Plugin UI is composed from curated components and bound surface classes; nothing is generated from an action. | Accepted | 2026-10-26 |
| decision-56 | Define 1.0 API stability as tiers with a generated public API inventory and a one-minor deprecation window | 1.0 stability is three named tiers, a generated public API inventory and a one-minor deprecation window. | Accepted | 2026-10-26 |
| decision-57 | Support QGIS 4 LTR and latest on Linux and Windows with a native CI leg per row, and keep QGIS 3 out of scope | The SDK supports QGIS 4 LTR and latest on Linux and Windows with a native CI leg per row; QGIS 3 is out of scope. | Accepted | 2026-10-26 |
| decision-58 | Publish plugins to the official QGIS repository from the tools, and ship standalone apps as conda packages | qgis-sdk publish uploads plugins to the official QGIS repository, and standalone apps ship as conda packages. |
Accepted | 2026-10-26 |
| decision-59 | Carry large and binary payloads across the bridge and agent boundaries by reference, with bounded pages and backpressure | Large and binary payloads cross the bridge and agent boundaries by reference, with bounded pages and backpressure. | Accepted | 2026-10-26 |
| decision-60 | Translate declared strings through Qt catalogues shared with the page, and make accessible names a check rule | Declared strings are translated through Qt catalogues shared with the page, and accessible names are a check rule. | Accepted | 2026-10-26 |
| decision-61 | Version plugin settings with migrations, add first-run and reset hooks, and keep runtimes isolated per plugin | Plugin settings carry a schema version with migrations, first-run and reset hooks, and runtimes stay isolated per plugin. | Accepted | 2026-10-26 |
| decision-62 | Write one authority and threat model document and make every check and grant refer to it | One authority and threat model document names principals, assets and boundaries, and every check and grant refers to it. | Accepted | 2026-10-26 |
| decision-63 | Add a structured plugin log service, crash capture into it, and a diagnostics bundle, with no telemetry | A structured plugin log service with crash capture and a diagnostics bundle, and no telemetry. | Accepted | 2026-10-26 |
| decision-64 | Generate the Processing catalogue from the host registry and run any registered algorithm through one typed surface | The Processing catalogue is generated from the host registry, and any registered algorithm runs through one typed surface. | Accepted | 2026-10-27 |
| decision-65 | Run registered jobs on workers with owner-scoped named submission | In the 0.9.0 design, whole registered jobs run on workers and are submitted by name through owner-scoped handles. | Accepted | 2026-10-28 |
| decision-66 | Unify declaration introspection with describe and declaration methods | A single describe helper and declaration methods expose kind-specific metadata, including local UI bindings, without changing the bridge contract. | Accepted | 2026-10-28 |
| decision-67 | Retire the optional Pydantic controlled deployment adapter | The optional Pydantic controlled-deployment adapter and extras are retired in a future Python minor release; third-party integrations remain plugin-owned. | Accepted | 2026-10-28 |
| decision-68 | Explore additive plugin applications with explicit action groups | Proposed: explore additive plugin application objects and explicit action groups while preserving class-based plugins and the bridge contract. | Proposed | 2026-10-28 |
| decision-69 | Order host hooks, bound the app-host shutdown and guard GUI-bound capabilities at runtime | Order host hooks, bound the app-host shutdown and guard GUI-bound capabilities at runtime. | Accepted | 2026-10-30 |
| decision-70 | Support QGIS 3.44 LTS on both hosts while QGIS 4 x is not on apt, with Qt imported through qgis PyQt | Support QGIS 3.44 LTS on both hosts while QGIS 4 x is not on apt, with Qt imported through qgis PyQt. | Accepted | 2026-10-30 |