Repository navigation
Turn the UI-extension registry into ADR-082's build-time frontend extension API #3317
Description
Goal
Core's desktop frontend exposes the edition-neutral frontend extension API that ADR-082 (proposed) §2 defines.
- nodespace-sync's Pro frontend plugs into it at build time through
virtual:nodespace-extensions. - Core never imports, names or branches on it.
- With nothing injected, every host renders nothing and no hook runs. That is the free app.
After this parent, #3327 (settings surfaces) and #3333 (sync lifecycle, collaboration UI, workspace root) move the Pro UI out by registering it through this API, without editing core's shell.
Context
The code is at core main @ af8ddb61. Locate code by symbol, because line numbers drift.
The registry is Pro-shaped. In packages/desktop-app/src/lib/plugins/:
ui-extensions.tskeys every contribution byvariant: ProSyncVariant.ui-extensions.svelte.tsfilters byresolveProSyncVariant(), importsproSyncandlabsFlags, and registers the Pro UI through a side-effectimport './pro-plugin'.extension-outlet.sveltehas no{:catch}branch and no error boundary.
Hard-coded Pro call sites with no extension point:
- Account, "Add synced database…" and the tenant badge, in
settings-sidebar.svelte,settings-pane.svelteanddatabase-settings.svelte. proSync.start()in theonMountofapp-shell.svelte.activateProSyncindatabase.svelte.ts, andmembership.invalidateForDatabaseSwitch()inevictAndReloadActiveDatabase().boundTenantCollection || ROOT_COLLECTION_IDin thecollectionsTreegetter ofcollections.svelte.ts.- The
proSyncblock incaptureStoreDump()indebug-channel.ts.
How the Pro GUI is built today.
- nodespace-sync's
scripts/build-pro-dmg.shand.github/workflows/release.ymlrunbunx tauri build --config src-tauri/tauri.pro.conf.jsoninside core'spackages/desktop-app, at core HEAD. - Tauri's
beforeBuildCommand(bun run build) inherits that environment. - So deleting the side-effect import would silently strip the Pro UI from the next Pro build. That deletion is Stop importing the Pro extension in core; register it only through the build entry #3316, and it waits for the pin gate.
- Once the Pro app builds from one pinned core revision, core merges first. nodespace-sync then sets
NODESPACE_EXTENSIONSin the pin-bump PR that crosses the change (ADR-081 migration step 4, ADR-083 §7). - Every other Turn the UI-extension registry into ADR-082's build-time frontend extension API #3317 sub-issue is additive, or preserves behaviour for the Pro build, so none of them needs a gate.
The contract
#3317, #3327, #3333 and their SYNC gates all use this one set of names, which are ADR-082's. Plans or gates that still say NODESPACE_PLUGIN_ENTRY, $lib/plugin-api, lifecycle.activeDatabaseChanged, databaseCachesCleared or hiddenCollectionIds are renamed to these before filing.
| Item | Name and shape | Added in |
|---|---|---|
| Build entry | virtual:nodespace-extensions, set by NODESPACE_EXTENSIONS=<module path>. Default-exports readonly NodespaceExtension[], which is [] in core. src/routes/+layout.svelte calls registerExtensions(extensions) |
#3311 |
| Host API | src/lib/extension-api/, aliased as @nodespace/extension-api, with /ui and /testing entries |
#3311 (alias, types), #3315 (services) |
| Version | EXTENSION_API_VERSION = { major: 1, minor: 0 }; each extension declares apiVersion: 1 |
#3310 |
| Extension | NodespaceExtension { id, apiVersion, chrome?, viewerTabs?, settingsSections?, settingsSlots?, collectionTreeRoots?, lifecycle?, debugDump? } |
#3310, #3312, #3313, #3314 |
| Contribution | { id, when?, priority?, load }. Hosts key each one by <extension id>/<contribution id> |
#3310 |
| Chrome | Slots app-shell-overlay and app-shell-modal |
#3310 |
| Viewer tab | { nodeType, label }, props { nodeId } |
#3310 |
| Settings section | { label, after? }, props { navigate } |
#3312 |
| Settings slots | database.actions (no props) and database.row (props { databaseId }) |
#3312 |
| Collection-tree roots | collectionTreeRoots(): readonly string[], unioned across extensions; a throw is treated as empty |
#3314 |
| Lifecycle | lifecycle.start(), which runs only under Tauri and may return a cleanup; lifecycle.onDatabaseActivated(databaseId), which is synchronous, so the extension serializes its own async work; a top-level debugDump() |
#3313 |
EXTENSION_API_VERSION stays at 1.0 for the whole of #3317. The compatibility policy and the surface snapshot take effect when #3315 merges. From then on, an additive change bumps the minor version and a breaking change bumps the major version.
Sub-issues, in order
| Key | Title | Size | Direct dependencies |
|---|---|---|---|
| #3309 | Move the Pro variant machine and DATABASE_SETTINGS_NODE_ID out of the generic registry files | M | NodeSpaceAI/nodespace-docs#90 (ADR-082), #3299 |
| #3310 | Adopt ADR-082's contribution contract: ids, when() predicates, keyed and isolated outlets | M | #3309 |
| #3311 | Add the virtual:nodespace-extensions build entry and the extension-api import alias | M | #3310 |
| #3312 | Add settings-section and database settings-slot extension points | M | #3310 |
| #3313 | Add start, onDatabaseActivated and debugDump extension lifecycle hooks | M | #3310 |
| #3314 | Add the collectionTreeRoots extension point to the sidebar collection tree | S | #3310 |
| #3315 | Publish the extension host API: services, ui and testing entries, version policy | M | #3311, #3312, #3313, #3314 |
| #3316 | Stop importing the Pro extension in core; register it only through the build entry | S | #3315, SYNC pin gate |
D2, C1.1 ──► C3.1a ──► C3.1b ──┬──► C3.2 ──┐
├──► C3.3 ──┤
├──► C3.4 ──┼──► C3.6 ──► C3.5 ◄── SYNC: pin gate
└──► C3.7 ──┘
Once #3310 is on main, #3311, #3312, #3313 and #3314 can run in parallel.
Why this order:
- Add the virtual:nodespace-extensions build entry and the extension-api import alias #3311 needs Adopt ADR-082's contribution contract: ids, when() predicates, keyed and isolated outlets #3310's
NodespaceExtensiontype andregisterExtensions(). - Stop importing the Pro extension in core; register it only through the build entry #3316 comes last for two reasons. Its no-extensions test then covers every extension kind. And core stops importing the Pro extension only after the host API that replaces those imports has been published.
Pin gate (#3316 only): SYNC:Pro app and Pro daemon build from one pinned core revision, not core HEAD (ADR-081 step 1).
Hand-offs to other parents
The other plans must match these before filing.
- Add the Pro-boundary ratchet check and the CLAUDE.md Pro/sync placement rule #3299 lands first, with two rules:
- a relocation-raise class. Move the Pro variant machine and DATABASE_SETTINGS_NODE_ID out of the generic registry files #3309 and Adopt ADR-082's contribution contract: ids, when() predicates, keyed and isolated outlets #3310 move existing Pro code inside core, and Move the Pro variant machine and DATABASE_SETTINGS_NODE_ID out of the generic registry files #3309 takes
proNamedFilesfrom 16 to 18 ataf8ddb61. - an absence-test convention, which Stop importing the Pro extension in core; register it only through the build entry #3316's and Publish the extension host API: services, ui and testing entries, version policy #3315's guard tests follow.
- a relocation-raise class. Move the Pro variant machine and DATABASE_SETTINGS_NODE_ID out of the generic registry files #3309 and Adopt ADR-082's contribution contract: ids, when() predicates, keyed and isolated outlets #3310 move existing Pro code inside core, and Move the Pro variant machine and DATABASE_SETTINGS_NODE_ID out of the generic registry files #3309 takes
- Delete the Pro Account section, workspace-members card and tenant-admission calls #3325:
- removes the Account entry from
CORE_SETTINGS_CATEGORIESinsrc/lib/components/settings/settings-categories.ts(created by Add settings-section and database settings-slot extension points #3312); - removes the pane's
AccountSettingsbranch; - the Pro package then registers the section with id
accountandafter: 'database'.
- removes the Account entry from
- Delete the Add synced database dialog and tenant badge from Database settings #3326:
- deletes only the hard-coded Pro markup in
database-settings.svelte, because Add settings-section and database settings-slot extension points #3312 already mounted thedatabase.actionsanddatabase.rowoutlets; - the replacement uses
databases.create,databases.switchTo,databases.error,nodes.createNodeandcollections.reload()from Publish the extension host API: services, ui and testing entries, version policy #3315.
- deletes only the hard-coded Pro markup in
- Stop driving Pro sync lifecycle from the app shell, database store and debug channel #3329 deletes the Pro calls only:
- The hooks are already in their final positions (Add start, onDatabaseActivated and debugDump extension lifecycle hooks #3313):
onDatabaseActivatedfires afterevictAndReloadActiveDatabase()inswitchTo(), and after the restore branch inload(). - There is no
databaseCachesClearedhook. The Pro extension invalidates its membership cache insideonDatabaseActivated. - Extension dumps appear under
stores.extensions[<id>]. databases.refreshDatabaseSettings(),collections.reload()andschemas.reload()come from Publish the extension host API: services, ui and testing entries, version policy #3315.
- The hooks are already in their final positions (Add start, onDatabaseActivated and debugDump extension lifecycle hooks #3313):
- Remove the tenant-binding root fallback from the collections tree #3330 deletes only core's own root:
- in
collections.svelte.ts,ROOT_COLLECTION_ID, theboundTenantCollectionread and thedatabaseStoreimport; - and it makes the default root set of
buildCollectionsTreeempty. - Its SYNC gate is renamed from
hiddenCollectionIdstocollectionTreeRoots. That gate can be met before Remove the tenant-binding root fallback from the collections tree #3330 merges, because Add the collectionTreeRoots extension point to the sidebar collection tree #3314 addscollectionTreeRoots.
- in
- Delete the Collaboration tab, membership store and service, and the Pro plugin from core #3331 deletes
pro-plugin.tstogether with the explicit registrations that Stop importing the Pro extension in core; register it only through the build entry #3316 adds topro-sync-variant.test.tsandfree-user-guardrail.test.ts. - Delete the Pro sync store, consent and relogin modals, and the Pro variant machine #3332:
- deletes
src/lib/plugins/pro-sync-variant.svelte.tsandsrc/tests/plugins/pro-sync-variant.test.ts, both created by Move the Pro variant machine and DATABASE_SETTINGS_NODE_ID out of the generic registry files #3309, instead of editingui-extensions.svelte.ts; DATABASE_SETTINGS_NODE_IDlives insrc/lib/constants/database-settings.ts.
- deletes
- nodespace-sync (a SYNC item filed later): the pin-bump PR that crosses Stop importing the Pro extension in core; register it only through the build entry #3316 sets
NODESPACE_EXTENSIONStopackages/desktop-app/src/lib/plugins/pro-plugin.tsin the pinned checkout. This holds until Move the Pro sync and collaboration UI out of core #3333 moves the Pro frontend into nodespace-sync.
Parent acceptance criteria
Core ships no Pro extension:
- No module under
packages/desktop-app/src/liborsrc/routesimportspro-plugin. -
bun run --cwd packages/desktop-app buildemits no Pro extension: the stringCollaboration is off for this databaseis absent frombuild/. - None of
proSync,labsFlags.syncEnabled,ProSyncVariantorresolveProSyncVariantappears in any of these files:ui-extensions.ts,ui-extensions.svelte.ts,extension-outlet.svelte,chrome-slot-outlet.svelte;extension-lifecycle.ts,settings-categories.ts;src/lib/extension-api/*.
- No
pro-sync-pillclass remains.
Extension points:
- Every contribution kind and hook in the contract table exists and is documented.
- Core tests exercise each of them through the fixture extension in
src/tests/fixtures/test-extension/, including every failure row of ADR-082 §2.4.
Build entry:
-
virtual:nodespace-extensionsis[]in core. -
NODESPACE_EXTENSIONS=<path>injects that module, together with Tailwind content, devfs.allow,resolve.dedupeand the Vitest include. - A missing path fails the build.
- Core's release workflow fails when the variable is set.
Host API:
-
@nodespace/extension-apipublishes the ADR-082 §2.6 host API, with its compatibility policy and a surface snapshot. - No core module imports it.
The free app:
- With nothing injected, no contribution renders and no hook runs (Stop importing the Pro extension in core; register it only through the build entry #3316's boundary test).
At every step:
- After each sub-issue merges:
- core builds;
bun run test:changedandbun run --cwd packages/desktop-app test:free-userspass;- the free app behaves as before;
bun run scripts/check-pro-boundary.tsexits 0, and its printedBASELINESare pasted.
- The Pro build keeps working. Move the Pro variant machine and DATABASE_SETTINGS_NODE_ID out of the generic registry files #3309, Adopt ADR-082's contribution contract: ids, when() predicates, keyed and isolated outlets #3310, Add the virtual:nodespace-extensions build entry and the extension-api import alias #3311, Add settings-section and database settings-slot extension points #3312, Add start, onDatabaseActivated and debugDump extension lifecycle hooks #3313, Publish the extension host API: services, ui and testing entries, version policy #3315 and Add the collectionTreeRoots extension point to the sidebar collection tree #3314 either add code or preserve behaviour, and Stop importing the Pro extension in core; register it only through the build entry #3316 merges only after the pin gate.
Dependencies
- NodeSpaceAI/nodespace-docs#90 (ADR-082): ADR-082 accepted. It defines the contract above.
- Add the Pro-boundary ratchet check and the CLAUDE.md Pro/sync placement rule #3299: must land before Move the Pro variant machine and DATABASE_SETTINGS_NODE_ID out of the generic registry files #3309. It provides the ratchet, the relocation-raise rule and the absence-test convention.
- SYNC pin gate: must land before Stop importing the Pro extension in core; register it only through the build entry #3316.
- Blocks:
- Delete the Pro Account section, workspace-members card and tenant-admission calls #3325 and Delete the Add synced database dialog and tenant badge from Database settings #3326, which need Add settings-section and database settings-slot extension points #3312 and Publish the extension host API: services, ui and testing entries, version policy #3315;
- Stop driving Pro sync lifecycle from the app shell, database store and debug channel #3329, which needs Add start, onDatabaseActivated and debugDump extension lifecycle hooks #3313 and Publish the extension host API: services, ui and testing entries, version policy #3315;
- Remove the tenant-binding root fallback from the collections tree #3330, which needs Add the collectionTreeRoots extension point to the sidebar collection tree #3314;
- Delete the Collaboration tab, membership store and service, and the Pro plugin from core #3331, which needs Stop importing the Pro extension in core; register it only through the build entry #3316.
Notes
Ratchet. Every PR runs bun run scripts/check-pro-boundary.ts, pastes the printed BASELINES and lowers every marker it pays down. The only raises allowed are relocations under #3299's rule, declared in the PR description.
Coding rules:
- Code comments cite ADRs, never GitHub issue numbers.
- Use
createLogger, never rawconsole.*. - No lint suppression.
- Run
bun run test, neverbun test. - Documentation beyond code comments belongs in nodespace-docs (the ADR-082 docs follow-up).
Filing. Core's CLAUDE.md says never to assign an issue at creation. Self-assign at pickup, with bun run gh:assign <N> @me.
Plan key: C3 (pro-boundary DAG).
🤖 Generated with Claude Code
Activity
mstomar125 commentedon Oct 1, 2026
Superseded by #3414 (umbrella against the final ADR-081–085)