Skip to main content

Planix — Architecture & Data Model

1. Overview

Planix is a Kanban-based project and task management app for Nextcloud, built as a thin client on OpenRegister. It manages projects, tasks, kanban boards, and time entries for internal dev and IT teams. Tasks flow through configurable kanban columns in a continuous-flow (non-sprint) model with WIP limits, backlog management, and time tracking.

Architecture Pattern

┌─────────────────────────────────────────────────┐
│ Planix Frontend (Vue 2 + Pinia) │
│ - Dashboard (My Work, recent projects) │
│ - Project list / detail views │
│ - Kanban board view (drag-and-drop) │
│ - Backlog view (sorted task list) │
│ - Task detail view (CnDetailPage + sidebar) │
│ - Time tracking (log, timesheets) │
│ - Admin settings │
└──────────────┬──────────────────────────────────┘
│ REST API calls
┌──────────────▼──────────────────────────────────┐
│ OpenRegister API │
│ /api/objects/{register}/{schema}/{id} │
│ - CRUD operations │
│ - Search, pagination, filtering │
└──────────────┬──────────────────────────────────┘

┌──────────────▼──────────────────────────────────┐
│ OpenRegister Storage (PostgreSQL) │
│ - JSON object storage │
│ - Schema validation │
└─────────────────────────────────────────────────┘

Planix owns no database tables. All data is stored as OpenRegister objects, defined by schemas in a dedicated register.

2. Standards Research

Before defining our data model, we evaluated multiple standards across three categories.

2.1 Standards Evaluated

StandardTypeCoverageMaturityRelevance
iCalendar VTODO (RFC 5545)InternationalTask title, description, due date, start date, status, priority, percent-complete, assignee (ATTENDEE), categories, recurrence, alarmsVery mature (1998, widely implemented)HIGH — primary field reference for task data
Schema.org ActionInternationalAction, PlanAction, ScheduleAction — agent, actionStatus, startTime, endTime, scheduledTimeVery matureHIGH — semantic type annotations
Schema.org ItemList / DefinedTermInternationalItemList (ordered list), ListItem, DefinedTerm (controlled vocabulary term)Very matureHIGH — models boards and columns
OpenProject Work Packages APIIndustrysubject, description, startDate, dueDate, estimatedTime, spentTime, percentageDone, status, priority, assignee, parent, relationsMature (EU government adoption)HIGH — proven data model for work packages
Nextcloud CalDAV VTODONextcloud nativeTasks implemented via VTODO in Calendar appActive, NC-bundledHIGH — potential reuse for task sync
GitHub Issues APIIndustrytitle, body, labels, assignees, milestone, state, comments, reactionsVery matureMEDIUM — reference for dev-team task model
VNG Zaak/TaakDutch govCase-bound tasks within ZGW; InterneTaak in KlantinteractiesPart of ZGW familyMEDIUM — relevant for Procest bridge
BPMN 2.0 User TaskInternationalUserTask, assignment (assignee, candidateGroups), form fieldsISO/IEC 19510LOW — too process-oriented for kanban
W3C PROV-OInternationalActivity, Entity, Agent provenance trackingW3C RecommendationLOW — audit trail reference only

2.2 Design Principle: International First

Data storage uses international standards. Dutch government standards are an API mapping layer.

This means:

  • Objects in OpenRegister are modeled after iCalendar VTODO and Schema.org conventions
  • When exposing a ZGW-compatible API, we map our international objects to Zaak/Taak field names
  • This makes Planix usable in any organization while remaining interoperable with Dutch systems

2.3 Key Findings

  1. iCalendar VTODO (RFC 5545) provides the most comprehensive field reference for task management. Properties like SUMMARY, DESCRIPTION, DTSTART, DUE, STATUS, PRIORITY, PERCENT-COMPLETE, ATTENDEE, CATEGORIES, RELATED-TO, and VALARM map directly to our task data model. Nextcloud's own Tasks app implements VTODO natively via CalDAV.

  2. Schema.org Action family (Action, PlanAction, ScheduleAction) provides semantic type annotations. schema:Action captures the essential who (agent), what (object), when (startTime/endTime/scheduledTime), and status (actionStatus) for any task.

  3. OpenProject's Work Package model is the industry reference for server-side project management. It proves a flat JSON object approach (no separate sprint table) works at scale, with estimatedTime, spentTime, percentageDone, and derivedStartDate/derivedDueDate for hierarchy.

  4. Kanban methodology is standards-agnostic. WIP limits, column-based flow, and cumulative flow diagrams are practices, not data schema. We store wipLimit on columns and derive flow metrics from task state history.

  5. Nextcloud Tasks app (CalDAV/VTODO) is the existing native task implementation. Planix augments it with project organization, kanban boards, and time tracking — features the Tasks app explicitly excludes. We store a calendarEventUid reference to allow optional two-way sync.

  6. VNG InterneTaak (Klantinteracties) defines tasks within customer interactions. Tasks from Procest cases can be managed in Planix via the cross-app task bridge. The zaakUuid field on a Planix task links back to the originating case.

  7. GitHub Issues proves that labels, milestones, assignees, and states are sufficient for dev-team task management. Complex metadata (story points, velocity) can be derived from these primitives plus time data.

3. Data Model Decisions

3.1 Standards Hierarchy

LayerStandardPurpose
Primary (storage)iCalendar VTODO (RFC 5545)Field reference for task properties
SemanticSchema.org JSON-LD (Action, ItemList, DefinedTerm)Type annotations for linked data
API mappingVNG ZGW InterneTaakDutch government interoperability
PatternOpenProject Work Package, GitHub IssuesProven dev-team task model
Nextcloud nativeCalDAV/VTODO, Calendar, Files, ActivityReuse where possible

3.2 Entity Definitions

Task (Taak)

A task is the core unit of work in Planix. Tasks belong to a project, can be placed in a kanban column, and carry time estimates and time log entries.

AspectDecisionRationale
Schema.org typeschema:Action (subtype schema:PlanAction)"An action performed by a direct agent" — matches task concept
VTODO alignmentFields named after VTODO properties where applicableiCalendar is the most mature task standard
VNG mappingInterneTaak (Klantinteracties)Dutch API compatibility for Procest bridge
Status storageStatus stored directly on task (task.status)Simpler queries; industry consensus (GitHub Issues, OpenProject)
Column placementcolumn reference on task, optionalTasks without a column are in the backlog
Time trackingestimatedDuration on task, separate TimeEntry objects for logsSeparating estimate from actuals (OpenProject pattern)
Hierarchyparent reference for sub-tasksOptional; supports 1-level sub-task depth
CalDAV synccalendarEventUid stores VTODO UIDOptional sync to Nextcloud Tasks app

Core properties:

PropertyTypeVTODO (RFC 5545)Schema.orgVNG InterneTaakRequiredDefault
titlestringSUMMARYschema:namegevraagdeHandelingYes
descriptionstringDESCRIPTIONschema:descriptionNo
statusenumSTATUSschema:actionStatusstatusYesopen
priorityenum: low, normal, high, urgentPRIORITY (1-9)Nonormal
projectreferenceRELATED-TO (parent project)No
zaakUuidstring (UUID)Procest case UUID (cross-app bridge)Nonull
columnreferenceNonull (backlog)
columnOrderintegerschema:positionNo0
assignedTostring (user UID)ATTENDEEschema:agenttoegewezenAanGebruikersnaamNo
dueDatedateDUEschema:scheduledTimegevraagdeDatumNo
startDatedateDTSTARTschema:startTimeNo
estimatedDurationinteger (minutes)ESTIMATED-DURATION (RFC 7986)No
percentCompleteinteger (0–100)PERCENT-COMPLETENo0
labelsstring[]CATEGORIESNo[]
parentreferenceRELATED-TO (parent task)Nonull
calendarEventUidstringUIDNonull
completedAtdatetimeCOMPLETEDschema:endTimeafhandelingsdatumNonull

Status values (aligned with VTODO STATUS + Dutch lifecycle):

StatusVTODODutchDescription
openNEEDS-ACTIONOpenNot yet started
in_progressIN-PROCESSIn behandelingBeing worked on
blockedGeblokkeerdBlocked by dependency
doneCOMPLETEDGereedCompleted
cancelledCANCELLEDGeannuleerdCancelled

Project

A project is the top-level container for tasks. Each project has exactly one kanban board (columns are part of the project). Projects serve as the boundary for task organization, permissions, and reporting.

AspectDecisionRationale
Schema.org typeschema:CreativeWorkClosest match — "The most generic kind of creative work" used as project container
Board model1 project = 1 kanban boardSimplest model; columns belong to project directly
No sprintsFlow-based, continuous deliveryKanban-only per user decision
BacklogTasks in the project without a columnIndustry pattern — implicit backlog
Procest linkcaseReference optional foreign keyBridge for cases-as-projects integration

Core properties:

PropertyTypeSchema.orgRequiredDefault
titlestringschema:nameYes
descriptionstringschema:descriptionNo
statusenum: active, archived, completedschema:creativeWorkStatusYesactive
colorstring (hex)No
iconstring (emoji or MDI icon name)No
membersstring[] (user UIDs)schema:memberNo[]
defaultAssigneestring (user UID)No
caseReferencestring (Procest case UUID)Nonull
labelsstring[]No[]

Column

A column represents a stage in the project's kanban board. Columns have an explicit order and optional WIP (work-in-progress) limit.

AspectDecisionRationale
Schema.org typeschema:DefinedTermTerm within a controlled vocabulary (the board as schema:DefinedTermSet)
WIP limitwipLimit integer, null = no limitCore kanban practice; proven by Kanboard, Jira Kanban
Soft limitViolations flagged in UI, not blockedIndustry consensus — soft limits with visual warning
Column typestype enum: backlog, active, doneSemantic: determines which columns count as "completed" for metrics

Core properties:

PropertyTypeSchema.orgRequiredDefault
titlestringschema:nameYes
projectreferenceschema:inDefinedTermSetYes
orderintegerschema:positionYes0
wipLimitinteger | nullNonull
colorstring (hex)No
typeenum: active, doneNoactive

Default columns (created during project initialization):

OrderTitleWIP LimitType
0To Donullactive
1In Progress3active
2Review2active
3Donenulldone

TimeEntry

A time entry records actual time spent on a task by a specific user.

AspectDecisionRationale
Schema.org typeschema:QuantitativeValue with schema:unitCode = MIN (minutes)Quantitative measurement with unit
GranularityMinutes (integer)Matches OpenProject; avoids floating-point issues
Multiple per taskSeparate entity, many-to-one to TaskUsers can log multiple time entries across different days
No timerManual entry only in MVPSimplest implementation; automatic timer in V1

Core properties:

PropertyTypeSchema.orgRequiredDefault
taskreferenceschema:subjectOfYes
userstring (user UID)schema:agentYescurrent user
durationinteger (minutes)schema:value (unitCode: MIN)Yes
datedateschema:startDateYestoday
descriptionstringschema:descriptionNo

Label

Labels are cross-project tags that can be applied to tasks and projects. Stored as shared vocabulary objects.

AspectDecisionRationale
Schema.org typeschema:DefinedTermControlled vocabulary term
ScopeApp-wide (not per-project)Enables cross-project filtering; matches GitHub labels model
ColorRequiredVisual identification; key for kanban card scanning

Core properties:

PropertyTypeSchema.orgRequiredDefault
titlestringschema:nameYes
colorstring (hex)Yes#4376FC
descriptionstringschema:descriptionNo

3.3 Cross-App Relationships

Planix (Task Management) Procest (Case Management)
┌──────────────────────┐ ┌─────────────────────┐
│ Project │ │ │
│ Column │ │ Case │
│ Task ───────────────┼──────────────┼─► Task (InterneTaak)│
│ TimeEntry │ caseRef │ Decision │
│ Label │ │ Status │
└──────────────────────┘ └─────────────────────┘

│ (future)
┌────────▼─────────────┐
│ Pipelinq (CRM) │
│ Lead / Request │
└──────────────────────┘

Procest integration: A Procest case can create tasks in Planix via the cross-app link. The caseReference on a Planix Project (or zaakUuid on an individual Task) maintains the bridge. Task completion status is mirrored back to the case status where configured.

3.4 My Work (Werkvoorraad)

A personal workload view showing all tasks assigned to the current user, across all projects. No new entity is needed — this is a frontend aggregation pattern.

How it works:

  • Query tasks with assignedTo == currentUser and status != done AND status != cancelled
  • Group by: Overdue (dueDate < today), Due this week, No due date
  • Sort by: priority (urgent first), then dueDate ascending

Required fields already present on Task:

FieldPresent
assignedToYes
priorityYes
dueDateYes
statusYes
project (reference)Yes

3.5 @conduction/nextcloud-vue Library

All Planix UI MUST use the shared @conduction/nextcloud-vue library.

LayerWhat to UsePurpose
List viewsCnListViewLayoutProject list, backlog list layout
Data tablesCnDataTableBacklog table, timesheet view
Detail pagesCnDetailPageTask detail (card-based layout)
SidebarCnObjectSidebarTask sidebar (Files, Notes, Tags, Tasks, Audit Trail)
Status badgesCnStatusBadgeTask status indicators on cards
Empty statesCnEmptyStateEmpty project, empty backlog, no tasks
PaginationCnPaginationBacklog list pagination
SettingsCnSettingsSection, CnVersionInfoCardAdmin settings page
StoreuseObjectStore with pluginsAll OpenRegister data operations
ComposablesuseListView, useDetailView, useSubResourcePage state management

Kanban board: custom Planix component (drag-and-drop card grid), built on top of library base components. Uses useObjectStore for task data, CnStatusBadge for status indicators on cards.

"@conduction/nextcloud-vue": "^0.1.0-beta.1"

Webpack alias (conditional for monorepo/CI compatibility):

const fs = require('fs')
const localLib = path.resolve(__dirname, '../nextcloud-vue/src')
const useLocalLib = fs.existsSync(localLib)

// In resolve.alias:
...(useLocalLib ? { '@conduction/nextcloud-vue': localLib } : {}),
'vue$': path.resolve(__dirname, 'node_modules/vue'),
'pinia$': path.resolve(__dirname, 'node_modules/pinia'),
'@nextcloud/vue$': path.resolve(__dirname, 'node_modules/@nextcloud/vue'),

3.6 Vue Router (Navigation)

All navigation uses Vue Router (hash mode). Route table:

PathNameComponentProps
/DashboardDashboard
/projectsProjectsProjectList
/projects/:idProjectBoardProjectBoardroute => ({ projectId: route.params.id })
/projects/:id/backlogProjectBacklogProjectBacklogroute => ({ projectId: route.params.id })
/tasks/:idTaskDetailTaskDetailroute => ({ taskId: route.params.id })
/my-workMyWorkMyWork
*redirect → /

Key files: src/router/index.js, registered in main.js, <router-view /> in App.vue. MainMenu: use :to prop on NcAppNavigationItem (NOT @click + $router.push()).

3.7 Nextcloud Integration Strategy

Principle: reuse Nextcloud native objects where possible, reference by ID, don't duplicate.

REUSE from Nextcloud

FeatureOCP InterfaceWhat to ReuseHow
Users / AssigneesOCP\IUserManagerUser identity, display name, avatarReference by user UID; resolve display name via IUserManager::get()
FilesOCP\Files\IRootFolderTask attachments, project documentsReference by Nextcloud file ID; resolve via IRootFolder->getById()
ActivityOCP\Activity\IManagerTask created/updated/completed eventsPublish to activity stream; implement IProvider for rendering
CommentsOCP\Comments\ICommentsManagerNotes on tasks and projectsAttach using objectType planix_task + objectId
System TagsOCP\SystemTag\ISystemTagObjectMapperCross-reference labels and categoriesTag task objects with label IDs
CalendarOCP\Calendar\IManagerTask due dates synced to NC CalendarOptional VTODO export via CalDAV; calendarEventUid back-reference
NotificationsOCP\Notification\IManagerAssignment, due date, status notificationsPublish structured notifications for in-app and push delivery

BUILD in OpenRegister (Planix-specific)

WhatWhy Not Reuse
ProjectsProject-specific metadata: color, icon, members, columns, Procest link
TasksFull task lifecycle: kanban placement, time estimate, column order, sub-tasks
ColumnsKanban-specific: WIP limits, column type (active/done), ordered board
TimeEntriesTime tracking: per-user, per-task logs with date and description
LabelsApp-scoped labels with colors — different from NC system tags

Key OCP Interfaces

// Users - resolve assignee display name
$userManager = \OCP\Server::get(\OCP\IUserManager::class);
$user = $userManager->get($userUid);
$displayName = $user?->getDisplayName() ?? $userUid;

// Activity - publish task events
$activityManager = \OCP\Server::get(\OCP\Activity\IManager::class);
$event = $activityManager->generateEvent();
$event->setApp('planix')->setType('task_update')->setSubject('task_assigned', ['task' => $title]);
$activityManager->publish($event);

// Notifications - task assignment
$notificationManager = \OCP\Server::get(\OCP\Notification\IManager::class);
$notification = $notificationManager->createNotification();
$notification->setApp('planix')->setUser($assignedTo)->setSubject('task_assigned');
$notificationManager->notify($notification);

// Files - resolve attachment
$rootFolder = \OCP\Server::get(\OCP\Files\IRootFolder::class);
$files = $rootFolder->getById($fileId);

// Calendar - export task as VTODO
$calendarManager = \OCP\Server::get(\OCP\Calendar\IManager::class);
// Use CalDAV write endpoint for VTODO creation

4. OpenRegister Configuration

Register

FieldValue
Nameplanix
Slugplanix
DescriptionProject and task management register

Schema Definitions

Schemas MUST be defined in lib/Settings/planix_register.json using OpenAPI 3.0.0 format, following the pattern used by Pipelinq and Procest.

Schemas:

  • task — Work item (schema:Action / schema:PlanAction)
  • project — Task container with kanban board (schema:CreativeWork)
  • column — Kanban board column (schema:DefinedTerm)
  • timeEntry — Time log entry (schema:QuantitativeValue)
  • label — Cross-project label/tag (schema:DefinedTerm)

The configuration is imported via ConfigurationService::importFromApp() in the repair step.

5. Resolved Research Questions

All research questions below have been resolved. Decisions are recorded in the app-specific ADRs under openspec/architecture/.

  1. CalDAV VTODO syncOne-way export to Nextcloud Tasks app in V1. The calendarEventUid field on Task stores the VTODO UID. Planix writes tasks to CalDAV; changes made in the Tasks app are not synced back. Two-way sync was rejected due to data model mismatch (Tasks app has no concept of projects, columns, or WIP limits).

  2. Sub-task depthOne level only (task → sub-task), all tiers. Projects serve the "epic" grouping role. No formal Epic entity. This matches Linear and Plane (1 level + containers) and avoids Jira's 3-level complexity.

  3. Procest task bridgeConfigurable — Procest UI decides. Procest's UI presents a project picker when creating tasks for a case. It may create a new project (with caseReference) or add tasks to an existing project (with zaakUuid on each task). Planix has no routing mechanism — it reads whatever Procest wrote to OpenRegister. See ADR-003.

  4. Time tracking scopePer-task only, forever. TimeEntry.task is always required. Overhead work (meetings, planning) is tracked as tasks — not as project-level time entries. This keeps the data model simple and queryable. No special cases needed. See ADR-004.

  5. GitHub/GitLab syncVia OpenConnector in V1. Planix owns no GitHub/GitLab API code. OpenConnector handles the external API mapping (GitHub Issues ↔ Planix tasks). This avoids duplicating integration logic across Conduction apps.

  6. WIP limit enforcementSoft limits with visual warning. Column header turns orange/red when over the WIP limit; counter shows e.g. 4/3. Drag is never blocked. Industry consensus: hard limits cause friction and workarounds (Jira, Kanboard both use soft limits).

6. References

Primary Standards (International)

Schema.org Types Used

Dutch Standards (API Mapping Layer)

Industry References