diff --git a/frontend/DeveloperGuide.md b/frontend/DeveloperGuide.md
index f0ec58cbec..ab4d04a617 100644
--- a/frontend/DeveloperGuide.md
+++ b/frontend/DeveloperGuide.md
@@ -45,6 +45,84 @@ export function f2() { /* ... */ } // Custom desktop implementation
Building with this pattern minimises the duplicated code in the system and greatly reduces the chances that changing the core app will break the desktop app.
+### Naming extension modules
+
+Extension modules and the functions/hooks they export should be named after **what they do**, not **which build overrides them**.
+Core code must never reference build targets (desktop, saas, etc.) by name — it should simply call a generic extension point and remain unaware of which layer is providing the implementation.
+
+```ts
+// ✅ CORRECT - named after the behaviour, not the build
+// core/useFrontendVersionInfo.ts
+export function useFrontendVersionInfo() { /* stub */ }
+
+// desktop/useFrontendVersionInfo.ts
+export function useFrontendVersionInfo() { /* real Tauri implementation */ }
+```
+
+```ts
+// ❌ WRONG - core code reveals knowledge of the desktop layer
+// core/useDesktopVersionInfo.ts
+export function useDesktopVersionInfo() { /* stub */ }
+```
+
+Similarly, core code should never contain conditionals that check which build is active (e.g. `if (isDesktop)`).
+If behaviour needs to vary, that variation belongs in an extension module - the core simply calls it.
+
+The same principle applies in reverse: code inside `desktop/` is guaranteed to be running in the Tauri environment, so `isTauri()` checks are never needed there either.
+If you find yourself writing `if (isDesktop())` or `if (isTauri())` anywhere, that is a sign the extension point has not been modelled correctly - the build system is already doing that separation for you.
+
+### List extensions
+
+When a build needs to _add_ behaviour rather than _replace_ it, the extension module can return a list of items and let core manage the rendering.
+Core defines the function to return an empty list; the extension build overrides it to return a populated one.
+
+```ts
+// core/toolbarExtensions.ts
+export interface ToolbarButton {
+ label: string;
+ onClick: () => void;
+}
+
+export function getToolbarButtons(): ToolbarButton[] {
+ return [];
+}
+```
+
+```ts
+// desktop/toolbarExtensions.ts
+import { type ToolbarButton } from '@core/toolbarExtensions';
+export { type ToolbarButton };
+
+export function getToolbarButtons(): ToolbarButton[] {
+ return [
+ { label: 'Open folder', onClick: () => { /* ... */ } },
+ ];
+}
+```
+
+```tsx
+// core/Toolbar.tsx
+import { getToolbarButtons } from '@app/toolbarExtensions';
+
+export function Toolbar() {
+ return (
+