Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions docs/architecture/configuration-over-code.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
---
title: Configuration over code
sidebar_position: 3
---

# Configuration over code

The point of `@conduction/nextcloud-vue` + OpenRegister isn't "less Vue to write". It's **less code, full stop**. Every page, every form, every list, every dashboard tile that a Conduction app ships is meant to be a row in a JSON file, not a `.vue` file in a `src/` tree.

That distinction matters because **configuration is safe in a way code is not**. Configuration runs inside a sandbox the library controls. Code runs inside the host app with the full power of the runtime — to crash it, to leak from it, to render arbitrary HTML, to call arbitrary backends. If we want non-engineers — and AI — to compose apps without paging the on-call, the contract has to be: you can change the JSON, you cannot change the runtime.

## The shift

A traditional Nextcloud app is a Vue project. Routes are defined in `router/index.js`. Navigation is rendered in `MainMenu.vue`. Each route is a hand-written view. CRUD is hand-written forms. Permissions are scattered through templates. The "shape" of the app lives across thirty files and is enforced by code review.

A Conduction app declares all of that in a single `manifest.json`:

```json
{
"$schema": "https://nextcloud-vue.conduction.nl/schemas/app-manifest.schema.json",
"menu": [
{ "id": "decisions", "label": "Decisions", "icon": "ScaleBalance", "order": 1 }
],
"pages": [
{ "id": "decisions", "type": "index", "config": { "register": "decidesk", "schema": "decision" } },
{ "id": "decision", "type": "detail", "config": { "register": "decidesk", "schema": "decision" } }
]
}
```

The same JSON Schema that defines the `decision` record drives the table columns, the filter bar, the create/edit dialog, the detail panel, and the validation. The same `manifest.json` decides what's in the left nav, who can see it, and which page mounts where. There is **no Vue to write** for either of those screens.

The library does the rendering. The schema does the typing. The manifest does the composition. The app author does none of it twice.

## Why this is a safer surface for AI (and citizen developers)

If "building an app" reduces to **writing two JSON files** — a schema and a manifest — then the failure modes collapse to a small, enumerable set:

- **A typo is a validation error.** Both files are JSON Schema-validated at build time and at runtime by [`useAppManifest`](/docs/utilities/composables/use-app-manifest). The user sees a clear "this field is wrong" — never a white screen, never a runtime exception, never a console stack trace.
- **There is no JavaScript to execute.** The user cannot ship a piece of code that an attacker can later use to compromise the host. The widest a manifest can reach is to reference a `customComponents` entry already shipped by the host bundle. Anything new the user adds is **data**, not behaviour.
- **The blast radius is bounded.** A bad schema breaks one page. A bad manifest entry hides one menu item. The chassis still renders. The user is never offered an "edit `App.vue`" affordance from which they can take the workspace down.
- **The change is reversible.** Both files are version-controlled, diffable, and round-trip through every editor — an admin UI, the OpenBuilt visual editor, an AI agent, a `git revert`. Anything reversible is testable; anything testable is safe to let non-engineers and AI touch.

That last point is the one that turns this from "nice architecture" into a deployable AI strategy. The reason it is hard to let an AI agent write a Nextcloud app today is the same reason it is hard to let a customer write one: the surface is too wide, and the failure modes are unbounded. Restrict the surface to a typed JSON contract and the work the AI has to do becomes generation **inside** a sandbox the platform owns. The output is constrained by the schema. The runtime stays the property of the library. The customer — or the model — never touches code that runs.

## Two concepts that make this concrete

### Spec-driven development

You don't describe the app in code — you describe it in **specs** and let an AI agent write the code that satisfies them. This is **OpenSpec**, the method Conduction's pipeline (Hydra) runs in production. A human writes the context: a Markdown spec per feature (RFC 2119 `MUST`/`SHOULD`/`MAY` + GIVEN/WHEN/THEN scenarios) and a set of Architecture Decision Records. The AI reads both and implements to them. The human develops context; the AI develops code.

Two tiers of ADRs govern how features hang together (*samenhang*): **organisation-wide ADRs** bind every app (data layer, security, i18n, the `src/manifest.json` convention itself), while **per-app ADRs** capture local choices. One feature is one spec; the spec is the source of truth, and the implementation is checked against it before it merges.

The workflow is a chain of skills. **`/opsx-explore`** is a thinking stance — you bring a vague problem, the agent investigates the codebase and the ADRs, challenges assumptions, and (when asked) captures the result as a proposal. **`/opsx-apply`** is the only skill that writes code: it walks the spec's task list and implements it, leaning on declarative `x-openregister-*` schema-register patches for business logic (ADR-031) and falling back to hand-written code only where the declarative path genuinely can't reach. Where the logic is a workflow, schema hooks fire it into **n8n** or **Windmill** via OpenRegister's `WorkflowEngineInterface` — no PHP. A sequential **quality + gatekeeping harness** (13 mechanical gates + `team-reviewer` + `team-security`) validates every change before `main`.

→ [Spec-driven development with OpenSpec](https://conduction.nl/academy/spec-driven-development) (tutorial) — the full workflow: ADRs, the explore and apply skills, the n8n/Windmill codeless path, and the validation harness.

### App builder

The end state of the configuration-over-code shift is that you don't author either file by hand. **OpenBuilt** is the visual app builder we're building on top of the manifest contract: drag schemas in, point and click the navigation, preview the chassis, publish — no JSON editor, no terminal, no Vue, no review.

→ [OpenBuilt — visual app builder](https://conduction.nl/apps/openbuilt)

OpenBuilt's output is a `manifest.json` and a set of OpenRegister schemas. The same files this docs site teaches you to hand-write. **Anything you build by hand here, you can later open in OpenBuilt and keep editing — and vice versa.** The artifact is identical because the contract is identical.

The same artifact is what we hand to an AI agent: today, an LLM that's been given the manifest schema and a list of available `register` + `schema` slugs can generate a working Conduction app in one shot. We're moving towards making that the default authoring loop, not the exception.

## What this means for you

You are reading the docs of the **library that holds the line**. Everything in this site — the chassis, the atoms, the stacked views, the manifest, the schemas — exists so that the configuration above it has somewhere to land. If we keep the runtime small and opinionated, the configuration on top can be wide and unprivileged. That is the trade: a tighter library, a freer authoring surface.

When you next add a feature to a Conduction app, the question to ask is not *"what Vue component do I write?"* — it's *"what would I have to add to the manifest, or the schema, for the existing components to render this?"*. If the answer is "nothing, the contract already covers it" — ship it as config. If the answer is "the contract doesn't have a primitive for this" — propose adding the primitive to the library, not the app. Every primitive added at the library tier is one fewer thing every citizen developer, every AI agent, and every future app author has to know.

## Where to next

- **[App manifest](./manifest.md)** — the contract. JSON schema, page types, slot system.
- **[Schemas and registers](./schemas-and-registers.md)** — the data side. How a single JSON Schema drives forms, columns, filters, and validation.
- **[App design principles](./app-design-principles.md)** — the chassis and atoms the configuration composes.
- **[Spec-driven development with OpenSpec](https://conduction.nl/academy/spec-driven-development)** — the tutorial. ADRs, the explore and apply skills, the n8n/Windmill codeless path, and the validation harness.
- **[OpenBuilt](https://conduction.nl/apps/openbuilt)** — visual app builder. Same contract, no JSON editor.
2 changes: 1 addition & 1 deletion docs/architecture/customization.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 4
sidebar_position: 5
---

# Customising Default Pages
Expand Down
97 changes: 85 additions & 12 deletions docs/architecture/manifest.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,91 @@ JSON also means the manifest survives lib upgrades. A breaking change to `CnAppN

## Page types

Each entry in `pages[]` declares its `type` — that drives which stacked view mounts:

| `type` | Stacked view | Use it for |
|---|---|---|
| `index` | [CnIndexPage](/docs/components/cn-index-page) | Schema-driven list pages with filters, search, CRUD, mass actions |
| `detail` | [CnDetailPage](/docs/components/cn-detail-page) | Single-object views with stats, cards, audit trail, files |
| `dashboard` | [CnDashboardPage](/docs/components/cn-dashboard-page) | KPIs, charts, drag-and-drop widget grids |
| `settings` | [CnSettingsPage](/docs/components/cn-settings-page) | Admin / config forms wired to `IAppConfig` |
| `logs` | [CnLogsPage](/docs/components/cn-logs-page) | Audit-trail / activity-log views |
| `chat` | [CnChatPage](/docs/components/cn-chat-page) | NC Talk-backed conversation surfaces |
| `files` | [CnFilesPage](/docs/components/cn-files-page) | Folder browser surfaces |
| `custom` | (consumer-supplied component) | Anything that doesn't fit the above |
Each entry in `pages[]` declares its `type` — that drives which stacked view mounts. Every type composes the same [five atoms](./app-design-principles.md#five-atoms-one-chassis) — **Topbar**, **Left navigation**, **Page header**, **Main column**, **Sidebar** — but each type fills the Main column differently and decides whether the Sidebar appears at all. The atom row on each card below shows the composition: bold atoms are present by default, muted atoms are off (and can be flipped on via `sidebar` config).

<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(320px, 1fr))', gap: '1.25rem', margin: '1.5rem 0' }}>

<div style={{ border: '1px solid var(--ifm-color-emphasis-300)', borderRadius: '8px', padding: '1rem 1.25rem' }}>

#### `index` → [CnIndexPage](/docs/components/cn-index-page)

**Atoms:** **Topbar** · **Left nav** · **Page header** · **Main** · **Sidebar**

Schema-driven list surfaces: sortable/filterable table or card grid, pagination, mass-actions, CRUD dialogs. The Sidebar carries search + facets. Config takes `register` + `schema`; columns and filters are generated from the JSON Schema unless overridden. Use for the most common surface in any app — "show me all decisions / contacts / requests".

</div>

<div style={{ border: '1px solid var(--ifm-color-emphasis-300)', borderRadius: '8px', padding: '1rem 1.25rem' }}>

#### `detail` → [CnDetailPage](/docs/components/cn-detail-page)

**Atoms:** **Topbar** · **Left nav** · **Page header** · **Main** · **Sidebar**

Single-object views: stats panel, cards, charts, audit trail. The Sidebar carries object metadata, attached files, notes, and the activity log. Config takes `register` + `schema`; the `:id` route param identifies the object. Use for "show me *one* thing in depth".

</div>

<div style={{ border: '1px solid var(--ifm-color-emphasis-300)', borderRadius: '8px', padding: '1rem 1.25rem' }}>

#### `dashboard` → [CnDashboardPage](/docs/components/cn-dashboard-page)

**Atoms:** **Topbar** · **Left nav** · **Page header** · **Main** · _Sidebar (off)_

Drag-and-drop widget grids on a 12-column GridStack canvas: KPI tiles, charts, NC Dashboard API widgets, integration widgets. Config takes a `widgets[]` array (or v2's per-page `widgets[]` with `slot: "body"`). Use for high-level overviews and landing pages.

</div>

<div style={{ border: '1px solid var(--ifm-color-emphasis-300)', borderRadius: '8px', padding: '1rem 1.25rem' }}>

#### `settings` → [CnSettingsPage](/docs/components/cn-settings-page)

**Atoms:** **Topbar** · **Left nav** · **Page header** · **Main** · _Sidebar (off)_

Sectioned admin / config forms wired to `IAppConfig`. Config takes a `sections[]` array of cards; in v2 these flatten into `slot: "section:<id>"` widget entries. Use for app-level configuration surfaces — connection settings, defaults, feature toggles.

</div>

<div style={{ border: '1px solid var(--ifm-color-emphasis-300)', borderRadius: '8px', padding: '1rem 1.25rem' }}>

#### `logs` → [CnLogsPage](/docs/components/cn-logs-page)

**Atoms:** **Topbar** · **Left nav** · **Page header** · **Main** · **Sidebar**

Audit-trail / activity-log views: streaming timeline in Main, filter facets (actor, action, date range) in the Sidebar. Config takes `source` (register, schema, or external endpoint) and an optional `columns[]` override. Use for compliance and observability surfaces.

</div>

<div style={{ border: '1px solid var(--ifm-color-emphasis-300)', borderRadius: '8px', padding: '1rem 1.25rem' }}>

#### `chat` → [CnChatPage](/docs/components/cn-chat-page)

**Atoms:** **Topbar** · **Left nav** · **Page header** · **Main** · **Sidebar**

NC Talk-backed conversation surfaces: message thread in Main, room list + participants in the Sidebar. Config takes a Talk `token` (room id) or a route-derived selector. Use for in-app chat features that ride on the workspace's existing Talk install.

</div>

<div style={{ border: '1px solid var(--ifm-color-emphasis-300)', borderRadius: '8px', padding: '1rem 1.25rem' }}>

#### `files` → [CnFilesPage](/docs/components/cn-files-page)

**Atoms:** **Topbar** · **Left nav** · **Page header** · **Main** · **Sidebar**

Folder-browser surfaces: file list in Main, folder tree + preview pane in the Sidebar. Config takes a `root` path inside the user's Nextcloud files. Use for app-scoped document surfaces ("attachments for this register") rather than a full Files replacement.

</div>

<div style={{ border: '1px solid var(--ifm-color-emphasis-300)', borderRadius: '8px', padding: '1rem 1.25rem' }}>

#### `custom` → consumer-supplied component

**Atoms:** **Topbar** · **Left nav** · _Page header (optional)_ · **Main** · _Sidebar (optional)_

Escape hatch for anything the typed views don't cover. Config carries a `component` key resolved against `customComponents`; the component owns its Main column. Page header and Sidebar are opt-in via `headerComponent` / `sidebar` overrides. Use sparingly — every bespoke page is one the chassis can't enforce consistency on.

</div>

</div>

Each type has a known `config` shape — the `index` config takes `register` + `schema`, the `dashboard` config takes a widget array, the `settings` config takes a sections array. The manifest's `$schema` validates these at build time, so a typo surfaces with a clear error path before runtime.

Expand Down
41 changes: 35 additions & 6 deletions docs/architecture/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,47 @@
sidebar_position: 1
---

import BrowserOnly from '@docusaurus/BrowserOnly'

# Architecture Overview

`@conduction/nextcloud-vue` is a **Layer 2** component library that sits between Nextcloud's official Vue components and individual Nextcloud apps.

## The Three Layers

```mermaid
graph TD
A["Layer 3: Your App<br/>(LarpingApp, Pipelinq, OpenCatalogi, Procest, MyDash...)"] --> B
B["Layer 2: @conduction/nextcloud-vue<br/>(Cn* components, createObjectStore, composables)"] --> C
C["Layer 1: @nextcloud/vue<br/>(NcAppNavigation, NcAppContent, NcAppSidebar, NcButton, NcDialog...)"]
```
The stack reads top-to-bottom: your app composes Conduction's Layer 2 primitives, which in turn compose Nextcloud's Layer 1 primitives. The orange trunk is the layer this docs site is about.

<BrowserOnly>
{() => {
require('@conduction/docusaurus-preset/diagrams')
return (
<div style={{ background: 'var(--c-cobalt-50)', borderRadius: '12px', padding: '0.5rem 0', margin: '1.5rem 0' }}>
<cn-domain-tree>
<cn-hex slot="apex" size="xl">Your app</cn-hex>
<cn-hex size="md">LarpingApp</cn-hex>
<cn-hex size="md">Pipelinq</cn-hex>
<cn-hex size="md">OpenCatalogi</cn-hex>
<cn-hex size="md">Procest</cn-hex>
<cn-hex size="md">MyDash</cn-hex>
</cn-domain-tree>
<cn-domain-tree compact>
<cn-hex slot="apex" size="xl" color="orange">@conduction/nextcloud-vue</cn-hex>
<cn-hex size="md" color="orange" variant="outline">Cn* components</cn-hex>
<cn-hex size="md" color="orange" variant="outline">createObjectStore</cn-hex>
<cn-hex size="md" color="orange" variant="outline">composables</cn-hex>
</cn-domain-tree>
<cn-domain-tree compact>
<cn-hex slot="apex" size="xl" color="nextcloud">@nextcloud/vue</cn-hex>
<cn-hex size="md" color="nextcloud" variant="outline">NcAppNavigation</cn-hex>
<cn-hex size="md" color="nextcloud" variant="outline">NcAppContent</cn-hex>
<cn-hex size="md" color="nextcloud" variant="outline">NcAppSidebar</cn-hex>
<cn-hex size="md" color="nextcloud" variant="outline">NcButton</cn-hex>
<cn-hex size="md" color="nextcloud" variant="outline">NcDialog</cn-hex>
</cn-domain-tree>
</div>
)
}}
</BrowserOnly>

### Layer 1: Nextcloud Vue

Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/schemas-and-registers.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Schemas and registers
sidebar_position: 3
sidebar_position: 4
---

# Schemas and registers
Expand Down
21 changes: 17 additions & 4 deletions docusaurus/src/pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
* 2. App manifest → /docs/architecture/manifest
* 3. Schemas and registers → /docs/architecture/schemas-and-registers
* 4. Components → /docs/components
* 5. Configuration over code → /docs/architecture/configuration-over-code
* (the "why JSON, why no Vue" page — also the AI-and-citizen-
* developer story, since the same JSON contract is what we hand
* to LLMs and to OpenBuilt's visual editor)
*/

import React from 'react'
Expand Down Expand Up @@ -63,6 +67,14 @@ const CARDS = [
href: '/docs/components/',
cta: 'Browse components',
},
{
eyebrow: '05 · For AI',
title: 'Configuration over code',
body:
'Every page, form, list, and dashboard is a row in a JSON file — not a .vue file. That makes the same contract safe for AI agents and citizen developers to author, because the runtime stays the library’s and the sandbox stays the platform’s.',
href: '/docs/architecture/configuration-over-code/',
cta: 'Read why JSON, not Vue',
},
]

/* Inline styles instead of CSS modules — keeps the homepage self-
Expand Down Expand Up @@ -200,13 +212,14 @@ export default function Home() {

<Section>
<SectionHead
eyebrow="Four entry points"
eyebrow="Five entry points"
title="Pick where you want to start."
lede={
<>
The library has four conceptual layers. Start at the chassis if
you're new to the design system; jump straight to the components
if you already know the pattern and just need the API.
The library has four conceptual layers — chassis, contract, data,
components — plus one cross-cutting story: why the whole thing is
configuration, not code, and what that unlocks for AI agents and
citizen developers. Start anywhere.
</>
}
/>
Expand Down
Loading