Build a Nextcloud app on the Conduction stack — Part 3: Schema-driven integrations
One calendarProvider block on the booking schema. Bookings appear in NC Calendar with no controller, no event listener, no per-app glue.
This is Part 3 of the four-part DeskDesk tutorial. Part 2 wired the desk and booking schemas. Part 3 makes those bookings appear in every user's Nextcloud Calendar — without writing a controller, an event listener, or any per-app calendar glue.
The trick: OpenRegister already ships an ICalendarProvider (RegisterCalendarProvider) that exposes any schema with a calendarProvider block as a virtual calendar. You declare the block on the booking schema. NC Calendar shows the events. That's the whole story.
Why not OpenConnector or a custom listener?
When you first hear "sync DeskDesk bookings to Nextcloud Calendar" your instinct is probably one of:
- Write an OpenConnector flow that listens to
BookingCreatedEventand POSTs to the CalDAV endpoint - Subclass
IEventListener<BookingCreatedEvent>in PHP and create a CalDAV event by hand - Have the frontend
POST /remote.php/dav/...when the user clicks Save
All three work. All three are wrong for this case, because they treat NC Calendar as a destination you push to. OpenRegister already exposes every schema as a calendar — you're not pushing anywhere; you're declaring that the booking schema is a calendar source.
The same pattern applies to:
| Integration | Schema annotation | What you get |
|---|---|---|
| NC Calendar | calendarProvider block | Read-only virtual calendar per schema |
| NC Activity | (automatic, free) | CRUD events in the activity stream + dashboard widget + email digest |
| NC Mail Smart Picker | linkedTypes: ["email"] | Paste a desk URL into NC Mail and get a rich preview |
| NC Contacts | linkedTypes: ["contact"] | /api/contacts/match returns matching desk users |
Anti-pattern: hand-coding <App>CalendarSync.vue, <App>NotesService, <App>MailSidebar.vue. Use the providers.
Step 1: Add the calendarProvider block
Open lib/Settings/deskdesk_register.json and add a configuration block to the booking schema:
"booking": {
"slug": "booking",
"title": "Booking",
/* ... existing properties ... */
"configuration": {
"calendarProvider": {
"enabled": true,
"dtstart": "start",
"dtend": "end",
"titleTemplate": "Desk {desk} · {user}",
"color": "#003a8c"
}
}
}
The five fields:
enabled—trueexposes the schema as a virtual calendar.dtstart— name of the property that holds the event start. We use thestartproperty already on every booking.dtend— same, but for the end. Optional — when missing, OpenRegister falls back to a sensible default (next-hour for date-time, end-of-day for date-only).titleTemplate— string template for the event summary. Tokens in{braces}are resolved against the object's properties. We useDesk {desk} · {user}. To resolve relations (thedeskproperty is a uuid pointing at adeskobject), include_extend=relationswhen the calendar provider fetches — see the OpenRegister side.color— hex colour of the calendar. We use cobalt-blue (--c-blue-cobalt).
That's the whole change. No other file needs to be touched.
Step 2: Re-import the configuration
Schemas in OpenRegister have a version. Bumping the schema version + re-running the import is the canonical way to update them in place. Bump the file's info.version (0.2.0 → 0.3.0) and the booking schema's version:
"info": { "version": "0.3.0" },
"booking": { "version": "0.3.0", /* ... */ }
Then trigger the import the same way as Part 2:
docker exec nextcloud php occ app:disable deskdesk
docker exec nextcloud php occ app:enable deskdesk
(Or hit POST /api/settings/load if you've wired admin settings yet.)
Step 3: Verify in NC Calendar
Open /apps/calendar/. You should see a new entry in the calendar list:
- Booking — cobalt-blue colour, read-only badge, owned by your user
Click it. The seed bookings from Part 2 (the Monday focus-work session, the Tuesday sprint-planning meeting, the tentative quiet morning) appear as events. Click one. The popover shows the desk + user title + the start/end times.
If you don't see the calendar:
- Confirm OpenRegister registered its provider:
docker exec nextcloud php occ config:list | grep openregister - Make sure your
bookingschema hasconfiguration.calendarProvider.enabled: trueafter the re-import - Check
nextcloud.logforRegisterCalendarProvidermessages
How it works under the hood
OpenRegister's RegisterCalendarProvider implements Nextcloud's ICalendarProvider interface. On every Calendar API request, it:
- Lists every schema with
configuration.calendarProvider.enabled === true - For each, returns a
RegisterCalendar(a lightweightICalendarimplementation) - When the user opens that calendar, queries OpenRegister for objects where
dtstartis in the requested range - Transforms each object into an iCalendar VEVENT via
CalendarEventTransformer, applying thetitleTemplate
Per-object ACLs gate event content. The provider bypasses multi-tenancy when listing schemas (so cross-tenant calendars work), but every individual event is still gated by RBAC.
The same pattern is followed by the spec.
What you've now bought yourself
Activity (NC Activity) is already wired. Browse /apps/activity/ and you'll see your booking creates / updates. The dashboard activity widget + email digest both show DeskDesk events. Zero code on your end.
Mail Smart Picker too: in NC Mail, paste a desk URL (/apps/deskdesk/desks/desk-3-east-12) and the picker resolves it into a rich preview card with the desk label + zone + equipment.
You've effectively integrated DeskDesk into four Nextcloud apps with one JSON block. That's the schema-driven discipline paying off.
