Ga naar hoofdinhoud
AcademytutorialBuild a Nextcloud app on the Conduction stack — Part 1: Scaffold

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.

TutorialApp developmentNextcloudOpenRegisternextcloud-vueTutorial series
8 min read

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

PartTitleAdds
1Scaffold (you're here)Empty app shell, chassis visible, navigable
2Schemas + manifestDesk and booking schemas, full CRUD, dashboard
3Schema-driven integrationsBookings appear in NC Calendar via the OpenRegister calendar provider
4External knowledge + shipxWiki source via OpenConnector, sidebar tab, package, publish

Same shape, two repos:

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> in appinfo/info.xml
  • The PHP namespace (OCA\AppTemplateOCA\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 — AppTemplateDeskDesk and app-templatedeskdesk:

FileReplace
appinfo/info.xml<id>, <name>, <namespace>, <navigation>, <settings> paths
composer.jsonname, description, the psr-4 autoload prefix
package.jsonname
webpack.config.jsthe appId constant
templates/index.php and templates/settings/admin.phpOCA\AppTemplateOCA\DeskDesk, the data-* element id
lib/AppInfo/Application.phpnamespace, 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.phpdocblock only
Every Vue file in src/the t('app-template', '…') translation namespace becomes t('deskdesk', '…')
src/router/index.jsgenerateUrl('/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.jsloadTranslations('app-template', …) and the #app-template-settings mount selector
lib/Settings/app_template_register.jsonrename 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.

.topbar

Topbar

App · Desk · always

The 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.

.nav

Left navigation

App · required · Desk: never

The 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.

.col

Main column

App · Desk · always

The 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.

.pageHeader

Page header

Index · Detail · Desk: never

The 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.

.detail

Sidebar

App · optional · Desk: never

The 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.

Troubleshooting

What's next

Next steps