Build a Nextcloud app on the Conduction stack — Part 1: Scaffold
Clone the Conduction app template, rename it to DeskDesk, build, enable, and see the canonical app chassis. The first of four parts that build a working desk-booking app on the full Conduction stack.
This is Part 1 of a four-part tutorial that builds DeskDesk, a flexible desk-booking app for an open-office environment, on the full Conduction Nextcloud stack. The end product: pick a desk on a floor, book a slot, see your booking in your Nextcloud Calendar, and surface zone-specific knowledge articles from xWiki right next to the booking. Every piece reuses what @conduction/nextcloud-vue and OpenRegister already give you for free.
In Part 1 you scaffold the app, rename it, build it, and see the canonical app chassis on screen. No data, no integrations yet. We get the bones right first.
What we're building, across all four parts
| Part | Title | Adds |
|---|---|---|
| 1 | Scaffold (you're here) | Empty app shell, chassis visible, navigable |
| 2 | Schemas + manifest | Desk and booking schemas, full CRUD, dashboard |
| 3 | Schema-driven integrations | Bookings appear in NC Calendar via the OpenRegister calendar provider |
| 4 | External knowledge + ship | xWiki source via OpenConnector, sidebar tab, package, publish |
Same shape, two repos:
- App source:
ConductionNL/deskdesk— the actual Nextcloud app you're forking from the template. - Tutorial source:
ConductionNL/conduction-website— the post you're reading right now.
Step 1: Use the template
ConductionNL/nextcloud-app-template is a GitHub repository template. The "Use this template" button gives you a fresh repo with the full starter kit: Vue 2 + Pinia frontend, PHP backend, OpenRegister wiring, quality pipeline, OpenSpec scaffolding, GitHub Actions.
gh repo create ConductionNL/deskdesk \
--template ConductionNL/nextcloud-app-template \
--public \
--description "Flexible desk booking for open-office environments"
Then clone it next to your other Nextcloud apps. Most workspaces keep them in a single apps-extra/ directory the dev container mounts.
cd /path/to/your/nextcloud/workspace/apps-extra
git clone https://github.com/ConductionNL/deskdesk.git
cd deskdesk
You now have a directory with the full template content under the new name. Nothing inside has been renamed yet — the directory is deskdesk/, but every file still says app-template, AppTemplate, OCA\AppTemplate. Step 2 fixes that.
Step 2: Rename the app
Nextcloud requires three identifiers to line up:
- The directory name (
deskdesk/) ✅ already - The
<id>inappinfo/info.xml - The PHP namespace (
OCA\AppTemplate→OCA\DeskDesk)
Plus a handful of supporting files reference the old id. The full list is small enough to do by hand, and doing it by hand is the right move. Project memory: never use sed or scripted edits on code files — use a real editor with project-aware refactoring, or just read each file once before you change it.
2a. The boot-critical files
These are the ones that prevent Nextcloud from booting the app at all. Edit each with Find & replace all in your editor — AppTemplate → DeskDesk and app-template → deskdesk:
| File | Replace |
|---|---|
appinfo/info.xml | <id>, <name>, <namespace>, <navigation>, <settings> paths |
composer.json | name, description, the psr-4 autoload prefix |
package.json | name |
webpack.config.js | the appId constant |
templates/index.php and templates/settings/admin.php | OCA\AppTemplate → OCA\DeskDesk, the data-* element id |
lib/AppInfo/Application.php | namespace, APP_ID constant, docblock |
Every other PHP file in lib/ | namespace OCA\AppTemplate\… → namespace OCA\DeskDesk\…, every use OCA\AppTemplate\… import, every docblock |
appinfo/routes.php | docblock only |
Every Vue file in src/ | the t('app-template', '…') translation namespace becomes t('deskdesk', '…') |
src/router/index.js | generateUrl('/apps/app-template') → generateUrl('/apps/deskdesk') |
src/store/store.js, src/store/modules/settings.js | '/apps/app-template/api/settings' → '/apps/deskdesk/api/settings', fallback register slug |
src/settings.js | loadTranslations('app-template', …) and the #app-template-settings mount selector |
lib/Settings/app_template_register.json | rename to lib/Settings/deskdesk_register.json (and the app field inside it) |
2b. The "you can do later" files
Tests (tests/Unit/AppTemplateTest.php, the integration Postman collection), phpcs.xml / phpmd.xml / REUSE.toml headers, and the .github/ workflow inputs. They reference the template id in metadata only; the app boots fine without them touched. Fix them when you set up CI.
2c. One Nextcloud-version compatibility nip
The template's lib/AppInfo/Application.php registers a repair step at runtime:
$context->registerRepairStep(InitializeSettings::class);
registerRepairStep() is missing from IRegistrationContext on a few Nextcloud builds (you'll see Call to undefined method in nextcloud.log). Move it to appinfo/info.xml instead — same effect, more portable:
<repair-steps>
<install>
<step>OCA\DeskDesk\Repair\InitializeSettings</step>
</install>
<post-migration>
<step>OCA\DeskDesk\Repair\InitializeSettings</step>
</post-migration>
</repair-steps>
Then drop the registerRepairStep() line and the use OCA\DeskDesk\Repair\InitializeSettings; import from lib/AppInfo/Application.php.
Step 3: Build and enable
The template ships its build output uncommitted. So the first thing to do in a fresh clone is install dependencies and build the JS bundles, otherwise the app UI is just a blank <div id="content">.
composer install --no-dev
composer dump-autoload
npm install --legacy-peer-deps
npm run build
Then make sure your Nextcloud container can see the directory. In a typical Docker setup the apps-extra/ host directory is mounted at /var/www/html/custom_apps/ in the container; if your compose file uses one bind mount per app, add a line for deskdesk and restart. Otherwise:
docker cp ./deskdesk nextcloud:/var/www/html/custom_apps/
docker exec -u root nextcloud chown -R www-data:www-data /var/www/html/custom_apps/deskdesk
Now enable:
docker exec nextcloud php occ app:enable deskdesk
You should see:
deskdesk 0.1.0 enabled
Open http://localhost:8080/apps/deskdesk/ and log in. You see a placeholder dashboard with sample KPI cards, two empty panels, three nav items (Dashboard / Items / Documentation) and a Settings entry pinned to the bottom of the rail. That's the chassis.
The chassis: the whole point of Part 1
Every Conduction app — DeskDesk, OpenRegister, OpenCatalogi, Procest, MyDash, the dozen others — looks the same way on first sight. Same five structural pieces, same place, same behaviour. That recognisability is what @conduction/nextcloud-vue enforces: a user who learnt one app navigates the next one without docs.
The chassis is one shape, five atoms. Each card below focuses on one atom: the focused zone is at full opacity with a KNVB-orange outline, the rest fades to 25% so you see where the atom sits.
.topbarTopbar
App · Desk · alwaysThe Nextcloud chrome row. Sits across every page unconditionally because every Conduction app lives inside Nextcloud's workspace. The shelf icons are the cross-app navigation; per-app links never go here.
.navLeft navigation
App · required · Desk: neverThe per-app sidebar. Carries this app's own primary navigation, plus a footer pinned to the bottom for global access (Settings, Feedback). The active item is the only place cobalt-100 backgrounds a row.
.colMain column
App · Desk · alwaysThe work surface. In an App pattern, the column opens with a .pageHeader (title + actions), then KPI strip and panels. In a Desk pattern, .col nests inside .grid to become a full-bleed widget canvas with no page header.
.pageHeaderPage header
Index · Detail · Desk: neverThe first row of .col on every Index and Detail template. Carries the page title (left) and exactly two action buttons (right): one ghost (secondary), one primary (filled cobalt). The header tracks .col's width: full-spread when the sidebar is closed, constrained when it is open.
.detailSidebar
App · optional · Desk: neverThe right-hand sidebar. Optional, dismissible, anchored to the active record or the active view. Carries an icon + title + description in a header, then a tabbed body (Search / Columns by default). Class stays .detail for code; we call it the Sidebar in copy.
In Part 2, you'll learn how manifest.json plus a JSON schema fills these atoms with real data. The placeholder Items nav entry, the empty dashboard, and the "article" schema you saw on screen will all become desks, bookings, and a real occupancy dashboard — without you laying out a single atom by hand.
