Ga naar hoofdinhoud

Pipelinq — Architecture & Data Model

1. Overview

Pipelinq is a CRM (Client Relationship Management) app for Nextcloud, built as a thin client on OpenRegister. It manages clients (persons and organizations), contact persons, leads (sales opportunities), and requests (service intake — the pre-state of a case). Both leads and requests can flow through configurable pipelines with kanban-style boards.

Architecture Pattern

┌─────────────────────────────────────────────────┐
│ Pipelinq Frontend (Vue 2 + Pinia) │
│ - Client list/detail views │
│ - Contact person views │
│ - Lead views + pipeline kanban │
│ - Request (verzoek) views + pipeline kanban │
│ - My Work (werkvoorraad) dashboard │
│ - 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 │
└─────────────────────────────────────────────────┘

Pipelinq 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
VNG Klantinteracties APIDutch govPartij, Klantcontact, Betrokkene, InterneTaak, DigitaalAdresPre-1.0 (half-product)HIGH — exact domain match
VNG Verzoeken APIDutch govVerzoek, KlantVerzoek, VerzoekProductPart of ZGW familyHIGH — models pre-case intake
Schema.orgInternationalPerson, Organization, ContactPoint, Demand, Role, ItemListVery matureHIGH — primary vocabulary
vCard / jCard (RFC 6350/7095)InternationalContact data fieldsVery matureMEDIUM — field reference for contacts
OASIS CIQ v3.0InternationalNames, addresses, party relationshipsMature (XML-based)LOW — dated format
Industry CRM consensusIndustryAccount, Contact, Lead/Deal, Pipeline, StageDe facto standardHIGH — proven patterns
W3C Organization OntologyInternationalOrganizational structure, membership, rolesW3C RecommendationLOW — too abstract

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 schema.org and vCard conventions
  • When exposing a VNG-compatible API, we map our international objects to Klantinteracties/Verzoeken field names
  • This makes Pipelinq usable outside the Netherlands while remaining interoperable with Dutch systems

2.3 Key Findings

  1. Schema.org provides the primary vocabulary: Person, Organization, ContactPoint, Demand, Role, ItemList, DefinedTerm. Every entity carries a schema: type annotation for linked data compatibility.

  2. vCard (RFC 6350) represents decades of real-world contact management. We use vCard property conventions as the field reference for contact data (but in flat JSON, not jCard array format).

  3. Industry CRM models (Salesforce, HubSpot, EspoCRM, Twenty) provide proven patterns for pipeline/kanban management. All major CRMs store the current stage directly on the entity (not in a junction table). HubSpot and Twenty prove that a unified Lead entity (without a separate Opportunity split) works at scale.

  4. VNG Klantinteracties is the Dutch government standard for this domain (Partij, Klantcontact, Betrokkene). It is immature (pre-1.0, deprioritized since mid-2024) but we map to it for Dutch API compatibility.

  5. VNG Verzoeken API defines the "verzoek" (request) as the pre-case intake. We map to it for the verzoek-to-zaak flow connecting Pipelinq to Procest.

  6. Nextcloud provides built-in Contacts (CardDAV/vCard), Calendar (CalDAV), and user management that we reuse where possible.

3. Data Model Decisions

3.1 Chosen Standards

We adopt a layered standards approach:

LayerStandardPurpose
Primary (storage)Schema.org + vCard (RFC 6350)International data model
SemanticSchema.org JSON-LDType annotations for linked data
API mappingVNG Klantinteracties + VerzoekenDutch government interoperability
PatternIndustry CRM consensusProven UX patterns (pipeline, stages, kanban)
Nextcloud nativeContacts, Calendar, UsersReuse where possible

3.2 Entity Definitions

Client (Klant/Partij)

A client can be either a person or an organization.

AspectDecisionRationale
Schema.org typeschema:Person or schema:OrganizationPrimary international standard
vCard alignmentFN, EMAIL, TEL, ADR, URLField naming from RFC 6350
VNG mappingPartij (soort: Persoon | Organisatie)Dutch API compatibility
Dual-natureSupport both person and organization as client typesRequired by schema.org; matches industry CRM Account/Contact split
Nextcloud reuseInvestigate Contacts app (CardDAV)May reuse native vCard contacts

Core properties (schema.org primary, VNG mapping):

PropertyTypeSchema.orgvCard (RFC 6350)VNG MappingRequired
namestringschema:nameFNPartij.naamYes
typeenum: person, organization@typeKINDsoortPartijYes
emailstringschema:emailEMAILDigitaalAdresNo
telephonestringschema:telephoneTELDigitaalAdresNo
addressobjectschema:addressADRbezoekadresNo
taxIDstringschema:taxIDpartijIdentificatorNo (orgs)
websitestringschema:urlURLDigitaalAdresNo
notesstringschema:descriptionNOTENo

Contact (Contactpersoon)

A contact person linked to a client organization.

AspectDecisionRationale
Schema.org typeschema:Person with schema:worksFor → ClientInternational standard
vCard alignmentContact as vCard with RELATED to organizationRFC 6350 relationship
VNG mappingPartij (soort: Contactpersoon) linked via BetrokkeneDutch API compatibility
Role qualificationUse schema:RoleSchema.org role-qualified relationships

Core properties:

PropertyTypeSchema.orgvCardVNG MappingRequired
namestringschema:nameFNPartij.naamYes
emailstringschema:emailEMAILDigitaalAdresNo
telephonestringschema:telephoneTELDigitaalAdresNo
rolestringschema:roleNameROLEBetrokkene.rolNo
clientreferenceschema:worksForRELATEDBetrokkene → PartijYes
jobTitlestringschema:jobTitleTITLENo

Lead

A lead represents a sales opportunity — from first contact through to won or lost. Leads flow through configurable pipeline stages. Unlike Salesforce's Lead→Opportunity split, we use a unified entity (proven by HubSpot and Twenty) where pipeline stages encode qualification level.

AspectDecisionRationale
Schema.org typeschema:Demand"Announcement to seek a certain type of goods or services" — matches lead concept
Industry patternUnified Lead (no Opportunity split)HubSpot/Twenty prove single entity works; stages encode qualification
PipelineStage stored on entity (lead.stage)Industry consensus (Salesforce, HubSpot, EspoCRM) — simpler queries, atomic updates

Core properties:

PropertyTypeSchema.orgRequiredDefault
titlestringschema:nameYes
descriptionstringschema:descriptionNo
clientreferenceschema:customerNo
contactreferenceschema:buyerNo
sourceenumNo
valuenumberschema:priceNo
currencystring (ISO 4217)schema:priceCurrencyNoEUR
probabilityinteger (0–100)No
expectedCloseDatedateschema:validThroughNo
pipelinereferenceNo
stagereferenceNo
stageOrderintegerNo0
assignedTostring (user UID)schema:agentNo
priorityenum: low, normal, high, urgentNonormal
categorystringschema:categoryNo

Lead source values (industry consensus from Salesforce/EspoCRM):

SourceDescription
websiteInbound from website
emailEmail inquiry
phonePhone call
referralReferred by existing client
partnerPartner introduction
campaignMarketing campaign
social_mediaSocial media
eventEvent/conference
otherOther source

Request (Verzoek)

A request is a service intake/inquiry before something becomes a case. This is the bridge between Pipelinq (CRM) and Procest (case management). Requests can optionally flow through a pipeline alongside leads.

AspectDecisionRationale
Schema.org typeschema:Demand"Announcement to seek a certain type of goods or services" — closest match
VNG mappingVerzoek from Verzoeken APIDutch API compatibility
Lifecyclenew → in_progress → completed / rejected / convertedInternational, maps to VNG lifecycle
PipelineOptional — requests can be placed on a pipelineEnables mixed kanban boards with leads and requests
Case linkRequest can convert to a Procest caseStandard request-to-case flow

Core properties:

PropertyTypeSchema.orgVNG MappingRequired
titlestringschema:nameVerzoek.tekstYes
descriptionstringschema:descriptionVerzoek.tekstNo
clientreferenceschema:customerKlantVerzoek → KlantNo
statusenumschema:actionStatusVerzoek.statusYes (default: new)
priorityenum: low, normal, high, urgentNo (default: normal)
categorystringschema:categoryVerzoekProductNo
requestedAtdatetimeschema:dateCreatedregistratiedatumAuto
channelstringschema:availableChannelNo
pipelinereferenceNo
stagereferenceNo
stageOrderintegerNo (default: 0)
assignedTostring (user UID)schema:agentNo

Pipeline

A pipeline is a configurable kanban board with ordered stages. Multiple entity types (leads and requests) can appear as cards on the same pipeline — the frontend merges them for a combined view.

AspectDecisionRationale
Schema.org typeschema:ItemListOrdered list of items (stages)
Industry patternTrello Board / HubSpot Pipeline / Deck BoardThree-level: Pipeline → Stages → Cards
Polymorphic cardsShared Pipeline/Stage entities, referenced from both Lead and RequestNo junction table — frontend merges two API calls. Industry-standard pattern.

Core properties:

PropertyTypeSchema.orgRequiredDefault
titlestringschema:nameYes
descriptionstringschema:descriptionNo
entityTypesstring[]Yes["lead"]
isDefaultbooleanNofalse
colorstring (hex)No

Stage

A stage is a column within a pipeline. Stages have an explicit order and optional probability (for sales forecasting).

AspectDecisionRationale
Schema.org typeschema:DefinedTermTerm within a controlled vocabulary
Industry patternTrello List / HubSpot Stage / Deck StackColumn in a kanban board
Probability mappingStage probability auto-populates lead probability on stage changeProven by EspoCRM's stage→probability mapping

Core properties:

PropertyTypeSchema.orgRequiredDefault
titlestringschema:nameYes
descriptionstringschema:descriptionNo
pipelinereferenceschema:inDefinedTermSetYes
orderintegerschema:positionYes0
colorstring (hex)No
probabilityinteger (0–100)No
isClosedbooleanNofalse
isWonbooleanNofalse

Default Sales Pipeline (created during app initialization):

OrderStageProbabilityisClosedisWon
0New10falsefalse
1Contacted20falsefalse
2Qualified40falsefalse
3Proposal60falsefalse
4Negotiation80falsefalse
5Won100truetrue
6Lost0truefalse

Default Service Requests Pipeline:

OrderStageProbabilityisClosedisWon
0Newfalsefalse
1In Progressfalsefalse
2Completedtruetrue
3Rejectedtruefalse
4Converted to Casetruefalse

3.3 Status Values

Request statuses (aligned with VNG Verzoeken lifecycle):

StatusDutchDescription
newNieuwJust received, not yet triaged
in_progressIn behandelingBeing processed
completedAfgehandeldSuccessfully completed
rejectedAfgewezenRejected/declined
convertedOmgezet naar zaakConverted to a case in Procest

3.4 My Work (Werkvoorraad)

A cross-entity workload view showing all items assigned to the current user. No new entity is needed — this is a frontend aggregation pattern.

How it works:

  • Query leads with assignedTo == currentUser and open stages
  • Query requests with assignedTo == currentUser and open statuses
  • Optionally include tasks from Procest (assignee == currentUser)
  • Merge, sort by priority then due date, display as unified card list

Required fields for My Work (already present on Lead, Request, and Procest Task):

FieldLeadRequestProcest Task
assignedTo / assigneeYesYesYes
priorityYesYesYes
dueDate / expectedCloseDateYesYes
status / stageYesYesYes
Entity type label"Lead""Request""Task"

3.5 Relationship to Procest

Pipelinq and Procest share the request-to-case (verzoek-to-zaak) flow from Dutch government standards:

Pipelinq (CRM)                    Procest (Case Management)
┌──────────────┐ ┌──────────────┐
│ Client │ │ │
│ Contact │──── Request ────>│ Case │
│ Lead │ (verzoek) │ Task │
│ Pipeline │ │ Decision │
└──────────────┘ └──────────────┘

A Request in Pipelinq can be converted to a Case in Procest. The client reference is preserved so the case knows which client initiated the request.

3.6 Admin Settings

Pipelinq exposes a Nextcloud admin settings panel for app configuration. Settings are stored in OpenRegister as configuration objects and/or via Nextcloud's IAppConfig.

Configurable by admin:

SettingTypeDescription
Pipeline managementCRUDCreate, edit, delete pipelines and their stages
Stage managementCRUDCreate, edit, reorder, delete stages per pipeline
Default pipelineSelectionWhich pipeline is used by default for new leads/requests
Lead sourcesListConfigurable lead source values
Request channelsListConfigurable request channel values
Priority levelsDisplayCustomize priority labels and colors

3.7 Nextcloud Integration Strategy

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

OpenRegister objects store CRM-specific fields plus foreign keys (vCard UID, calendar event UID, file ID, user UID) pointing to Nextcloud native entities. The PHP service layer uses OCP interfaces to read/write native data.

REUSE from Nextcloud

FeatureOCP InterfaceWhat to ReuseHow
ContactsOCP\Contacts\IManagerPerson/org master data (name, email, phone, address)Reference by vCard UID. Search via IManager::search(). Create/update via IManager::createOrUpdate().
CalendarOCP\Calendar\IManagerFollow-up dates, deadlines, appointmentsCreate events via ICalendarEventBuilder (NC 31+). Reference by event UID. Expose CRM deadlines as virtual calendar via ICalendarProvider.
UsersOCP\IUserManagerAuthentication identity, assignees, handlersReference by user UID. Use IAccountManager for profile fields (org, role, phone).
FilesOCP\Files\IRootFolderDocument attachments on clients/requestsReference by Nextcloud file ID. Resolve via IRootFolder->getById().
ActivityOCP\Activity\IManagerUnified interaction timelinePublish CRM events ("Request created", "Client updated") to activity stream. Implement IProvider for rendering.
TalkOCP\Talk\IBrokerReal-time conversations per client/requestCreate conversation via IBroker::createConversation(). Store token in OpenRegister object.
CommentsOCP\Comments\ICommentsManagerNotes/comments on any CRM objectAttach comments using objectType + objectId. Supports threads, mentions, reactions.
System TagsOCP\SystemTag\ISystemTagObjectMapperCross-reference and categorize objectsTag files and objects with CRM categories.

BUILD in OpenRegister (CRM-specific)

WhatWhy Not Reuse
Client metadataCRM-specific: type (person/org), status, source, account manager, linked requests
Contact relationshipsRole-qualified links between contacts and client organizations
LeadsSales-specific: value, probability, expected close date, pipeline stage
Requests (verzoeken)Domain-specific lifecycle, pipeline stages, priority, category
Pipelines & stagesCRM-specific configurable workflow boards
Interaction logsStructured records (call log, email summary, meeting notes) with type, date, duration, outcome

Key OCP Interfaces

// Contacts - search and create
$contactsManager = \OCP\Server::get(\OCP\Contacts\IManager::class);
$results = $contactsManager->search('John', ['FN', 'EMAIL'], ['limit' => 10]);
$contactsManager->createOrUpdate($properties, $addressBookKey);

// Calendar - create events
$calendarManager = \OCP\Server::get(\OCP\Calendar\IManager::class);
$builder = $calendarManager->createEventBuilder(); // NC 31+
$builder->setSummary('Follow-up: Request #123')->setStartDate($date);

// Activity - publish CRM events
$activityManager = \OCP\Server::get(\OCP\Activity\IManager::class);
$event = $activityManager->generateEvent();
$event->setApp('pipelinq')->setType('request_update')->setSubject('...');
$activityManager->publish($event);

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

// Talk - create per-client conversation
$broker = \OCP\Server::get(\OCP\Talk\IBroker::class);
$conversation = $broker->createConversation('Client: Acme Corp', [$userId]);

4. OpenRegister Configuration

Register

FieldValue
Namepipelinq
Slugpipelinq
DescriptionClient relationship management register

Schema Definitions

Schemas MUST be defined in lib/Settings/pipelinq_register.json using OpenAPI 3.0.0 format (not inline PHP), following the pattern used by opencatalogi and softwarecatalog.

Schemas:

  • client — Person or organization (schema:Person / schema:Organization)
  • contact — Contact person linked to client (schema:Person + worksFor)
  • lead — Sales opportunity (schema:Demand)
  • request — Service intake/inquiry (schema:Demand)
  • pipeline — Kanban board configuration (schema:ItemList)
  • stage — Pipeline column (schema:DefinedTerm)

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

5. Open Research Questions

The following questions need further investigation as the app matures:

  1. VNG Klantinteracties stability — The API is pre-1.0 and deprioritized. Should we track its evolution or diverge? Current decision: align conceptually, don't depend on API stability.

  2. DigitaalAdres as separate entity — VNG models digital addresses (email, phone) as separate objects linked to a Partij. Should Pipelinq follow this pattern or keep contact fields inline on Client/Contact? Current decision: inline for simplicity, refactor if interoperability requires it.

  3. Pipeline/stages for requestsRESOLVED: Configurable pipelines with stages are now a core feature. Both leads and requests can flow through pipelines. Stages are stored directly on entities (industry consensus).

  4. BSN/KVK integration — VNG Partij supports partijIdentificator for BSN (citizens) and KVK numbers (organizations). When should Pipelinq support government ID lookups?

  5. Multi-channel support — VNG Klantinteracties models omnichannel interactions (mail, phone, web, counter). Should Pipelinq track which channel a request came from? Current decision: channel field on Request, configurable values in admin settings.

  6. Lead-to-order flow — Leads can be won, but order/product/finance management is out of scope for now. When should Pipelinq support post-sale workflows?

6. References

Primary Standards (International)

Schema.org Types Used

Dutch Standards (API Mapping Layer)

Industry References