Concepts & Architecture
This section explains what Gridmason is, how its pieces fit together, and the vocabulary the rest of the documentation assumes. Read it once before you scaffold a widget or stand up a host — everything else builds on the model described here. You should never need to read the source code to follow along; where a format matters, the shape is shown inline.
What Gridmason is
Gridmason is an open-source platform for building widgetized pages: application screens composed of independently authored, draggable, resizable tiles instead of hand-coded layouts. It is framework-agnostic (a widget can be written in vanilla JavaScript, React, or Vue and still drop into the same grid), and it ships with a supply chain that lets you load third-party widgets at runtime with the same kind of integrity guarantees you expect from a package registry.
It exists to solve three problems that usually get solved badly, one at a time, inside every dashboard-style product:
- Composition — letting pages be assembled from reusable tiles, with per-user and per-organization customization, without every tile knowing about every other tile.
- Extensibility — letting people who are not on your team ship widgets your application can load, without you shipping a new release and without giving their code the run of your app.
- Trust — making "the code we reviewed is exactly the code that runs" a verifiable fact rather than a hope, so loading someone else's widget is a bounded risk.
Reach for Gridmason when you are building a dashboard, an admin console, a record-detail view, or any page where the layout should be data-driven and the widgets should be pluggable — especially if you want a widget ecosystem you don't have to author entirely yourself. It is deliberately product-neutral: it has no opinion about your domain, your auth, or your database. You supply those through small adapter interfaces; Gridmason supplies the engine, the contracts, the author tooling, and the distribution-and-trust layer.
A note on maturity: every Gridmason package is currently 0.x. The concepts and the wire formats described here are stable in shape, but APIs may still change between minor versions. See What's stable, what's 0.x at the end.
The six pieces
Gridmason is not one library. It is six pieces, each released on its own cadence, each with a single job. You will not use all six directly — a widget author mostly touches the CLI and the SDK; a host builder touches core, the SDK, and a registry; the protocol sits underneath all of them.
| Piece | Package / deliverable | Its one job |
|---|---|---|
| protocol | @gridmason/protocol |
The contracts everything agrees on, plus the verification library that decides whether a remote is safe to run. |
| core | @gridmason/core |
The engine: the grid canvas, the layout model, and the rules that govern what renders where. |
| sdk | @gridmason/sdk |
The boundary between a widget and its host — the single, capability-enforcing channel a widget uses to reach data and events. |
| cli | @gridmason/cli (the gridmason binary) |
The widget author's devkit: scaffold, develop, lint, verify, publish. |
| registry | a self-hostable service | Distribution and trust: it reviews, countersigns, logs, and serves widgets, and publishes the signals hosts use to keep them safe. |
| dashboard | a deployable app | The reference host — a real, deployable dashboard that proves the whole platform end to end and shows how to embed the engine. |
The dependency direction is strict and one-way. protocol depends on nothing else in Gridmason; everything depends on protocol. core and sdk each depend only on protocol (and, importantly, not on each other — core carries the SDK handle to widgets without ever inspecting it). The cli builds on protocol and the sdk; the registry and the dashboard build on top. Nothing depends on the dashboard. This is why the pieces can be versioned and shipped separately: a change is a new published version of one package, and consumers upgrade on their own schedule.
How the pieces fit at runtime
The clearest way to see the architecture is to follow one widget from an author's laptop to a running page.
author registry host (e.g. the dashboard)
────── ──────── ─────────────────────────
gridmason lint automated + human review resolve enabled widgets
gridmason publish ─────► countersign + log + CDN ─────► verify signature + hash + log
(keyless sign) (the reviewed hash is │ (protocol verify lib,
the runnable artifact) │ inside a Service Worker)
▼
core mounts the custom element
on the grid canvas, and hands it
an SDK handle
│
▼
widget talks to the host ONLY
through the SDK (data, events,
settings, navigation)
-
Author. Using the CLI, you scaffold a widget, develop it against fixture data with no backend, and run
gridmason lint— the exact automated checks a registry runs, so "green locally" predicts "passes review." When youpublish, the CLI signs the artifact with a short-lived, identity-bound key (no long-lived private key on your machine). -
Registry. The registry runs its automated checks and a human review, then applies its countersignature (its stamp of approval), records the release in a public transparency log, and serves the immutable, content-hashed files from a CDN. Because the artifact is addressed by the hash of its bytes, the publisher cannot swap the code after review.
-
Host. When a page needs a widget, the host asks a registry which widgets are enabled and where their files live, then verifies every remote — publisher signature, registry countersignature, content hash, and transparency-log inclusion — using the verification library from protocol, before a single line of widget code runs. In the reference dashboard this happens inside a Service Worker that intercepts each fetch and refuses anything whose bytes don't match the signed release.
-
Mount. Core mounts the verified widget as a native custom element on the grid canvas, in the position the layout specifies, and hands it a HostSDK handle.
-
Talk. The widget reaches data, events, settings, and navigation only through that SDK handle, which enforces the widget's declared capabilities against the current user's permissions on every call. There is no other sanctioned path from a widget to your data.
Each of the next sections zooms into one of these ideas.
The trust and supply-chain model
Gridmason's central security claim is one sentence: the reviewed hash is the runnable artifact. Everything in the trust model exists to make that sentence literally true and independently checkable. Here is how, in plain language.
Content hashing — identity is the bytes. Every file a widget ships (its entry module, any code chunks, its schemas and assets) is identified by the SHA-256 hash of its exact served bytes, written as a tagged string like sha2-256:<hex> so a future algorithm change is a clean version bump rather than an ambiguity. A signed release lists the expected hash for every file the runtime may load. A host binds trust per file URL, never per origin — so two registries that happen to share a CDN host can't impersonate each other's files.
Signing — two signatures, two meanings. Every published version carries a dual signature:
- The publisher signature attests authorship. By default it is keyless (Sigstore-style): the CLI obtains a short-lived certificate bound to your OIDC identity (your Google or GitHub login, for example) at publish time. The OIDC issuer is the real trust anchor — each registry keeps an explicit allowlist of issuers it will accept, and the signature records exactly which identity vouched for the artifact. There is no long-lived private key to steal.
- The registry countersignature attests approval, and is applied only after review passes. Its key is held separately from the review staff.
A host requires both signatures plus the content hash plus log inclusion to check out before it will load a widget. A stolen publisher key alone therefore ships code to no one.
Review and countersigning. Between publish and countersign sits review. The mechanism is part of the platform; the policy — what a reviewer will accept — belongs to each registry operator, and is meant to be published rather than secret. Review runs in tiers by how much can go wrong: declarative artifacts that carry no executable code (layouts, page types) get a lighter path than frontend widget remotes, which get static analysis for things like network calls made outside the SDK, reachability of tokens, and obfuscation. The automated half of this is the same code the CLI's lint runs, which is what lets local-green predict review-pass. When review passes, the registry countersigns, writes a transparency-log entry, and publishes to the CDN.
The transparency log. Approvals are written to a public, append-only, Sigstore-style log, so anyone — not just the registry — can audit what a registry has shipped. Hosts verify a release's inclusion proof (this release really is in the log) against a pinned log key. The flagship registry anchors to public Sigstore infrastructure rather than running its own log; self-hosted registries can treat log-inclusion as a policy-configurable check.
Revocation and the kill switch — fail closed, but scoped. A registry can withdraw an artifact after the fact by publishing a signed revocation and kill feed. An entry is one of two states:
revoked— block new loads; instances already running are left alone.killed— the kill switch: block new loads and force-unload any running instance.
A severity field rides along for triage, but it never changes the load decision — any listed artifact is blocked regardless of severity. Hosts keep one feed cursor and one freshness clock per registry. Cached data carries a maximum time-to-live (24 hours in the reference model). Past a registry's TTL, a host must re-check that registry's feed before loading its remotes. This is the "fail closed" rule, and its scope is the important part: it is scoped to the stale registry only. Remotes from still-fresh registries keep working, and a registry being unreachable while its TTL is valid does not break anything. A registry is supply chain, not a control plane — hosts reach it over ordinary outbound HTTPS (or signed offline bundles) and keep functioning when it is down.
Trust roots — where trust is anchored. A host doesn't trust a registry by name; it trusts a pinned trust-root document for that registry, which names the registry's countersign root(s), any publisher-CA roots, the accepted OIDC issuers, the log public key(s), and a validity window. Roots arrive through one of two channels, and neither fetches a root blind at runtime:
- Build-time — shipped inside the host application build (the default for registries known when the app is built, such as the flagship).
- Deploy-time — an operator-supplied config or secret, so a deployer can trust a fresh self-hosted registry without rebuilding the app. Whoever can write this config can graft a registry into the deployment's supply chain, so it is protected like any credential and is never writable from the application UI.
Roots rotate with an overlap window: a new root is cross-signed by the outgoing one, hosts accept either during the overlap, and drop the old one on their next release. Air-gapped hosts pin the same documents, delivered inside signed offline bundles.
Federation — what self-hosting a registry means. The registry is open source and self-hostable. Any organization can run its own private registry for its own widget ecosystem, with its own trust roots, its own publisher records, and its own review policy; the project also operates a public flagship instance. A host can trust several registries at once (say, the flagship plus your private one). Because a publisher's namespace prefix is unique only within a registry — there is no global naming authority — a host pins each prefix to one registry and resolves every widget by its source-qualified identity. A configuration in which two registries claim the same prefix is rejected rather than guessed.
Key vocabulary
These are the nouns the rest of the docs use precisely. Learn them once.
Widget. A self-contained, reusable UI tile, delivered as a plain ES module that registers a native custom element. Its identity is its custom-element tag, which must be publisher-prefixed (for example acme-sales-chart). "Widget" refers to the type — the published, versioned artifact — not a particular tile on a page.
Widget instance. One placement of a widget on one page. The type is identified by its tag; an instance is identified by a per-placement id and carries its own saved settings. Two clocks on the same page are one widget and two instances, each with its own configuration and its own SDK handle.
Manifest. The declaration that describes a widget to the rest of the platform: its tag, kind, publisher, version, the page context it requires, the pages it supports, its grid size constraints, its declared capabilities, its settings schema, and its dependencies. The registry, the CLI, and the host all read the same manifest shape. A capability increase between versions re-triggers review, because the manifest is the contract the review signs off on.
{
"formatVersion": "1.0",
"tag": "acme-sales-chart", // custom-element tag; must be publisher-prefixed
"kind": "widget", // widget | plugin | page-type | layout
"name": "Sales Chart",
"publisher": "acme",
"version": "2.3.1",
"requiresContext": { "record": { "recordType": "customer" } },
"supportsPages": ["crm.customer-detail", "dashboards.*"],
"size": { "default": [4, 3], "min": [2, 2], "max": [12, 8] },
"capabilities": [{ "api": "records.read", "scope": "recordType:customer" }],
"entry": "widget.js" // the content-hashed ES module that registers the element
}
Page. A single rendered screen. In Gridmason every page is a widgetized canvas — with no exceptions. A "fixed" page is just a locked layout, and a full-canvas tool (a map, a flow editor) is a default layout of one maximized, locked widget. There is no special-cased non-widget page, which keeps the model uniform.
Page type. The template a page is an instance of. A page type declares the typed context it provides (see below), a default layout, any locks (mandatory or immovable slots), and whether users may customize it. crm.customer-detail and dashboards.home are page types; the customer page you are looking at is a page.
Page context. The typed data a page hands to the widgets on it. A customer-detail page provides context.record (a reference to a customer); a team dashboard provides context.team. Contexts are built from a small set of typed primitives (a record reference, string, number, boolean, id) and composites (lists, objects). A widget declares the context it needs via requiresContext, and a subset check — does the page provide everything the widget requires? — decides whether the widget may appear on that page at all. Gridmason owns the shape of context; your host defines the domain vocabulary (what a customer is).
Layout (LayoutDoc). The versioned JSON that describes what is placed where on a page: pages, optional tabs, a grid, and the widgets in it with their positions, sizes, and per-instance settings.
LayoutPage { schemaVersion, page, name, default, grid, hasTabs, tabs[] }
→ LayoutTab { name, grid }
→ LayoutGrid { items[] }
→ LayoutWidget { widgetID: {source, tag}, i, x, y, w, h, props, slot? }
Two details matter. A widget's identity in a layout is source-qualified — { source, tag }, where source names the registry the instance was saved with (or a sideload origin, or local) — so a layout survives a host that trusts several registries, and no other source can silently impersonate a saved tag. And layouts migrate on read: an older document is upgraded in memory by a chain of per-version migrators, while an unknown newer version renders read-only with a warning rather than being destructively rewritten.
Layouts resolve through three governance levels — a plugin/host default, then an organization's published layout, then the user's personal layout — where the most specific wins and a slot locked at one level cannot be overridden below it.
Import map. The browser-native mechanism Gridmason uses to load widgets and share dependencies, deliberately in place of any custom module-loading runtime. The host assembles an import map from its local widgets, the enabled registry remotes, and any acknowledged sideload origins — and nothing else. When two widgets need different major versions of a shared dependency, the registry's resolution emits scoped entries so both are satisfied without global collisions. A widget's sharedScope declares the dependency ranges it expects the host to provide; omit it and the widget is fully self-contained.
HostSDK. The interface a host implements and a widget calls — the single, capability-enforcing chokepoint between the two. Through it a widget reads and writes records, makes scoped network requests, emits and subscribes to typed events, reads its context and settings, navigates, and reports telemetry. There is deliberately no raw fetch and no untyped event bus on the handle: all widget network I/O flows through the SDK, because only that path carries the per-instance identity the host's API trusts. A widget that bypasses the SDK reaches your API as an anonymous page script, not as itself.
interface HostSDK {
records: {
read(ref: RecordRef, opts?): Promise<Record> // capability: records.read:<scope>
query(spec: QuerySpec): Promise<Record[]>
write(ref: RecordRef, patch: Patch): Promise<Record> // capability: records.write:<scope>
}
net: { fetch(req: ScopedRequest): Promise<Response> } // capability: net:<host>; no raw fetch
events: { emit; on } // typed, namespaced topics
context: PageContext
settings: { get; update; onSchema }
nav: { open; toast }
telemetry: { error; mark }
readonly identity: { instanceId: string; widgetId /* {source, tag} */ }
}
Capability. A declared permission a widget needs, written as <api>[:<scope>]. The four APIs are records.read, records.write, net, and events; a scope narrows them (records.read:recordType:customer, net:api.acme.com, events:acme.sales). Capabilities are declared in the manifest and enforced by the SDK as min(user permissions, declared capabilities) — the widget can never do more than both it declared and the current user is allowed. A denied call returns a typed error, never silent empty data (which would leak whether the capability exists). The same rule gates which widgets even appear in the add-widget picker.
Trust root. The pinned document that anchors a host's trust in one registry — its countersign roots, publisher-CA roots, accepted OIDC issuers, log public keys, and validity window. Covered in full under the trust model; the one-line version is that a host trusts pinned roots, never a registry's say-so fetched at runtime.
Release. The unit a registry publishes and a host verifies: an artifact identity plus the { url → content-hash } map of every file the runtime may load, wrapped by the signature envelope that carries the dual signature and the log-inclusion evidence. The release is what the verification library checks in one shot before any widget code runs. Its integrity comes from hashing, not schema validation — its canonical bytes must hash to the value the signature covers, so a tampered release breaks the signature rather than merely failing a format check.
What's stable, what's 0.x
Two different things are versioned in Gridmason, and it is worth keeping them apart.
The packages are 0.x. Every @gridmason/* package publishes from 0.x and follows semantic versioning within that. Pre-1.0, minor releases can carry breaking API changes. Install exact or narrowly-ranged versions and read the changelog before you bump. Consume the packages from npm — never from a file or git dependency.
The wire formats are versioned independently, by the protocol. The manifest, signature envelope, transparency-log entry, revocation feed, trust-root document, and offline bundle each carry an explicit formatVersion of the form major.minor. A minor bump is additive and backward-compatible; a major bump is breaking. The verification library declares which majors it speaks and refuses an unknown major rather than guessing — so a host never silently mis-reads a format from the future. When a breaking format ships, the old major runs alongside it for at least one host release cycle before anyone retires it, and serving retirement of an old major is a per-registry decision, not a platform-wide flag day.
The practical takeaway: treat the concepts and the format shapes in this document as stable ground to build on, and treat the package APIs as still-settling surfaces that deserve a pinned version and a glance at the changelog on every upgrade.