ConnectIQ Internal · architecture note
Document
Core object model
Status
For review
Scope
Entities & lifecycle
Owner
Platform

Proposal

The thing we install is not the job that installs it.

ConnectIQ currently puts the job at the centre of the model. A job is episodic — it opens, gets worked, and closes. Everything valuable around it is not.

This note proposes a persistent, location-anchored record that jobs act on rather than hang off: the ConnectIQ object.

ConnectIQ object Active
Object
CIQ-00418
Location
LOC-0092
Created by
QT-1180
Subscription
SUB-0771
Hardware
2 placed

01 · The principle

Separate what persists from what happens

Every entity in an operations portal falls into one of two groups, and the split is the whole argument.

Persistent things exist independently of any activity. A customer exists whether or not anyone is working for them. A site exists before the first install and after the last removal. A subscription runs on its own clock — it renews, lapses, and cancels without reference to whether a job is open.

Episodic things start, run, and end. A job is a unit of work with a completion date. A ticket is a reported problem that gets resolved and closed.

Putting an episodic entity at the centre forces every persistent entity to depend on something designed to finish. That is why a job-centred model strains the moment a subscription needs to outlive its install, or a customer reports a fault before any job exists.

Read left to right, the model is also the project pipeline. A lead converts into a customer and a site; a lead that closes becomes a quote; a signed quote becomes the object. Each arrow is a commitment gate, and only the last one brings the operational core into existence — everything to the right of it is what then happens to a real deployment.

How a project flows through the ConnectIQ model A lead converts into the Accounts domain (Customer and Location). A closed deal becomes a Quote. A signed Quote creates the ConnectIQ object, which Subscription, Ticket and Job then reference. Lead PIPELINE ACCOUNTS DOMAIN Customer PERSISTENT Location PERSISTENT has Quote COMMERCIAL ConnectIQ object The installation at a site DEPLOYMENT ANCHOR convert close-won signed Inventory ONGOING Subscription ONGOING Ticket REACTIVE Job EPISODIC references Three commitment gates — convert, close, sign — and only the last creates the object. The four below only reference it.
Fig 1 — How a project flows: a lead converts into the Accounts domain, a closed deal becomes a quote, and a signed quote creates the object that service and billing hang off.
Anchor Persistent Episodic

The front of the funnel is its own domain

The dashed box in Fig 1 is deliberate. The lead pipeline, the customer, and the location form one bounded context — the Accounts domain, the “who and where.” The object and everything hanging off it — inventory, subscriptions, tickets, jobs — are a different context, the Deployment domain, the “what is out there and what happens to it.” They change for different reasons and on different cadences: account details shift for CRM reasons, deployments shift for operational ones.

Drawing the boundary buys a clean ownership line for a small team, but it only holds if the two sides stay loosely coupled. The test is how many references cross the boundary — and in this model there is essentially just one.

Two bounded contexts and the single reference that crosses between them The Accounts domain holds Lead, Customer and Location. The Deployment domain holds the ConnectIQ object and its inventory, subscription, ticket and job. Only one reference crosses the boundary: the object's location_id pointing back to Location. ACCOUNTS DOMAIN Lead Customer Location DEPLOYMENT DOMAIN ConnectIQ object ANCHOR Subscription Ticket / Job Inventory + placements location_id the one reference that crosses — Deployment points at Accounts, never the reverse
Fig 2 — Two bounded contexts, one crossing reference. Keeping it single and one-directional is what lets each side evolve on its own.

Practically, the object stores a location_id and treats the Accounts domain as the owner of that record — it reads location data through the Accounts boundary rather than reaching into its tables. For a two-developer team this is a bounded context inside a modular monolith, not a literal microservice: you get the same separation and ownership clarity without standing up separate services and the network, deploy, and data-consistency overhead that comes with them.

02 · Prior art

This is the shape mature systems already use

The proposal isn’t a bespoke invention. When we checked it against the established field-service and installed-base platforms, the same separation showed up at every tier of the market — a persistent, location-bound record that work acts on, distinct from the work itself. The ConnectIQ object is our name for that record.

Salesforce Field ServiceEnterprise · public schema

The clearest reference to mirror. A first-class Asset object, separate from the Work Order. Work Orders reference an Asset and a Location; entitlements and contracts — their subscription equivalent — attach to the Asset, not the work. Assets also nest parent-to-child, which is exactly our “one object, many hardware components” case.

Core data model — developer.salesforce.com
ServiceTitanMid-market · closest to our scale

Customer, Location, Equipment and Job are separate records, not nested inside one another. Jobs get equipment attached to them; the equipment record is the thing that accumulates a full service history over time — the same reason we keep placements against the object rather than the job.

Installed equipment — help.servicetitan.com
ServiceMaxEnterprise · asset-heavy

The most doctrinaire version: it markets itself as asset-centric field service, built around the full asset lifecycle, and treats accurate installed-base data as the thing the whole service strategy depends on. Overkill for us operationally, but it confirms the direction at the far end of the spectrum.

Installed base management — ptc.com
JobberSMB · the counter-example

The useful counterpoint. Jobber deliberately has no strong equipment object — reviewers note it tracks materials used on a job and stops there. It stays simple precisely by not solving the installed-base problem. That’s the honest trade: the object model is worth its weight only because we have durable equipment with subscriptions and recurring service on top.

Product features — getjobber.com
Where we land

Subscriptions and recurring service put us in ServiceTitan / Salesforce territory, not Jobber’s. That’s the tell that the object is worth the extra table rather than a premature abstraction — the businesses that skip it don’t carry an installed base the way we do.

03 · Object vs location

Why this can’t just be the location

The object is anchored to exactly one location and never moves, which makes it feel like it could simply be the location. It can’t. Two ordinary business states become unrepresentable the moment you merge them.

Two cases that break if object and location are merged Location A holds two separate ConnectIQ objects. Location B holds none because nothing is installed yet. Location A One site, two systems ConnectIQ object Monitoring system ConnectIQ object Access control Location B Quoted, not yet installed No ConnectIQ object yet Merged: this site needs two duplicate address records. Merged: this state cannot be recorded at all.
Fig 3 — A location can hold many objects, or none. Both are normal.

A third reason matters for the ticket queue. A customer can raise a ticket about a site before anything is installed there — a survey request, a quote chase, an access problem. That ticket needs a location to attach to, and no object exists yet. Keeping the two separate gives it somewhere to live.

The line worth repeating in review: location answers where, the object answers what is deployed there.

04 · Creation

The object is born when the quote is confirmed

Not when the quote is drafted, and not when the installer finishes. Confirmation is the first moment the deployment is both real and fully specified — someone has committed to it, and the quote already says what goes where.

no object

Quote drafted. Speculative — most quotes never convert.

Created here pending_install

Quote confirmed. Object exists with zero hardware attached.

active

Install job complete. Hardware placed, subscription running.

suspended

Subscription lapsed or service paused. Hardware still on site.

decommissioned

Removed. Hardware unlinked, record kept for history.

Why not earlier

Creating an object at quote drafting fills the installed base with things that will never exist. Quote conversion is well under 100%, and every unconverted quote leaves a ghost object that pollutes counts, dashboards, and subscription reporting. The quote line is already a fine record of a proposed deployment; it doesn’t need a second one.

Why not later

This is the more important half. If the object isn’t created until the install completes, then the install job has nothing to reference. You would have to let jobs float unattached and retro-link them, or point the job at the quote — which rebuilds exactly the job-centred coupling this model is trying to remove.

Creating at confirmation means the install job is born with a target. It is a job against object CIQ-00418 from the moment it is scheduled, which is what makes the job history on that object complete rather than starting one entry late.

Born with its location — the quote is origin, not structure

At the moment of creation, the object takes a direct, permanent location_id copied from its quote line. From that instant the object knows its site on its own: one object, one location, set once. No query ever walks through the quote to find where an object lives, so the quote’s later lifecycle — completed, archived, superseded — breaks nothing. quote_line_id stays on the object purely as origin and audit.

This is what keeps the model honest years later. A maintenance quote or a maintenance job doesn’t create or redefine anything — it simply references the existing object, the same as any other work against it. The customer, likewise, is reached through the location (object → location → customer) rather than a second direct link, so there is exactly one answer to “whose site is this” — while the paying party is carried by the active subscription, which traces to whoever signed its quote. Those are usually the same customer, and when a site changes hands they legitimately differ until the new owner signs.

Consequence worth accepting

An object in pending_install has no inventory placements at all. That is a valid state, not a data error — validation and UI both need to allow it. An object with no hardware is a commitment that hasn’t been fulfilled yet.

It also needs a terminal state for deals that die after signature. A confirmed quote cancelled before install should move the object to cancelled rather than delete it, so the commercial history survives.

05 · Change of owner

The site persists, the owners are dated

When a location changes hands, the wrong move is creating a new location record. Our objects carry an immutable location_id, so a fresh record would force migrating every object at the site, split the address’s job and ticket history across two rows, and duplicate the address itself. The location is the physical truth; owners come and go on top of it.

This is also the established industry shape. Utilities separate the premise — the permanent physical service point — from the account, the commercial party currently attached to it; a building being sold never changes the meter point’s identity. Salesforce models the same idea as an association between Account and Location carrying active-from and active-to dates. Both are the same answer: the site keeps a dated history of owners.

Which makes this the third appearance of a pattern we’ve already adopted twice: hardware occupies an object for a dated period, and now a customer occupies a location for a dated period. Same table shape, same rules — one open period at a time, close-then-open in a single transaction, history never deleted.

Location ownership periods over time The location stays fixed while two different customers own it across separate dated ownership periods. The ConnectIQ object and its location link never change. Location The physical site Objects stay linked to it OldCo Sdn Bhd 2019 – Mar 2026 · ended NewCo Sdn Bhd Mar 2026 – now · current Each period is an ownership row. The object, its location link, and its hardware never move during the handover.
Fig 4 — Ownership periods against a fixed location: the same dated-period shape as hardware placements.

The handover sequence

A change of owner touches the model in a strict order. First, close the old ownership period and open the new one, effective-dated, in one transaction. Nothing on the object changes — location_id is immutable and placements are untouched. The old owner’s subscription then runs to its own termination or transfer, and the new owner signs a new quote, which produces a new subscription against the same object. That is the “follows a new quote” flow: the quote carries the new commercial relationship to an object that never moved.

The window in between — site owned by the new party, active subscription still the old one’s — is a legitimate, representable state, and the portal should display it as a deliberate handover rather than a discrepancy. One query nuance follows: object → location → owner now means the current owner, while historical questions (“who owned this site when that job ran”) resolve against the dated rows. Warranty disputes and billing audits will want the second form.

Guardrail

Keep locations.customer_id as a denormalised pointer to the current owner if reads want it, but location_ownerships is the source of truth — the pointer is written in the same transaction as the ownership rows, never on its own. Same rule as placements.

06 · Fan-out

One quote, many objects

A quotation is a commercial document scoped to a customer. A ConnectIQ object is an operational record scoped to a site. These two things don’t sit at the same grain, and confirmation is where the translation happens.

The unit that carries the mapping is the quote line. Each line names a location and a system; on confirmation, each line that represents a deployable system instantiates one object.

Correction — a line names many locations, not one

The sentence above assumes one line, one location. The CRM does not work that way. A quote line carries a quantity and is assigned across any number of locations, each with its own quantity — fifty players spread over thirty sites is one line, thirty assignments. So “each line instantiates one object” gives the wrong answer as soon as a rollout is larger than a single site.

The three cases below still hold; what changes is the thing objects are counted from. It is the line-to-location assignment, not the line. How many objects each assignment produces is deferred to S1 — see the delivery plan’s register.

Three quote fan-out scenarios A single-site single-system quote creates one object. A single-site two-system quote creates two objects at one location. A three-site quote creates three objects across three locations. CASE A One site, one system Quotation Site 1 1 object CASE B One site, two systems Quotation Site 1 Object Object CASE C Three sites Quotation Site 1 Site 2 Site 3 Object Object Object The quote never links to an object directly. Quote lines do — one each, at confirmation.
Fig 5 — Fan-out happens per quote line: one object, several at one site, or several across sites, without changing the rule.

What this means for jobs

Case C spawns at least three install jobs, because a job is a site visit and someone travels to each address. Case B is the interesting one: a single visit commissions two objects.

That forces a decision. If one visit routinely commissions more than one system, a plain job→object link won’t hold it, and you need either one job per object (grouped under a shared appointment) or job lines that each target an object — the same shape as quote lines. Salesforce Field Service takes the second route: the work order sits at the location, and its line items target individual assets.

Open decision

Don’t resolve this until we know how often case B actually occurs. If multi-system sites are rare, one job per object is simpler and costs nothing. If they’re common, job lines mirroring quote lines are worth the extra table — and the symmetry makes the model easier to explain, not harder.

The same deferral now covers the object grain itself. Whether an assignment yields one object per site, one per assignment, or one per system per site depends on a concept the CRM does not yet carry — nothing in a quote line says which product anchors a system. Both questions want the same evidence, and both are answered at S1.

07 · Hardware

The hardware moves, the object doesn’t

Because the object is fixed to its site and carries the subscription, hardware underneath it can be swapped freely without disturbing anything commercial. A failed unit is pulled, a replacement goes in, and the subscription never notices.

This is the same pattern aviation and telecom asset systems use — a fixed position holds a swappable serialised part, and each occupies the position for a dated period. The position’s history stays continuous; the part carries its own separate history from site to site.

Inventory placements over time The ConnectIQ object stays fixed while two hardware units occupy it across separate dated placement periods. ConnectIQ object Fixed at location Holds the subscription UNIT-A100 Jan – Jun · removed UNIT-B220 Jun – now · active Each period is a placement row, opened and closed by the job that caused it.
Fig 6 — Placements are dated periods, not a current-location field, which is what makes serial history queryable.

The swap is two writes

Replacing a unit closes the outgoing placement by setting unlinked_at, and opens a new one for the incoming unit — both stamped with the same job_id. That job becomes the single answer to why the change happened and who performed it.

Two rules keep this honest. An item may hold only one open placement at a time, so it can never appear installed in two places; and retired units keep their placement rows, because that history is exactly what a warranty claim needs.

Ours or theirs

Not every device in an object is ours. A subscription can run on hardware the customer provided, so every inventory item carries an owned_by flag — company or customer. The placement mechanics are identical either way, but the flag changes the rules around them: a customer-owned unit is never pulled back to our stock or redeployed to another site, warranty responsibility sits with them, and at decommission it stays behind while our units return to inventory. If a subscription is ever bound to a specific serial — a licence or SIM tied to the hardware — that binding lives on the placement, so a swap-job moves it in the same transaction and the subscription’s anchor to the object never wavers.

08 · Schema

Tables, in short

Grouped by domain. Identifiers and foreign keys are shown in mono.

The one rule

Nothing episodic owns anything persistent. Jobs, tickets, and placements may reference the object, the location, or an item — never the reverse. A persistent record must never carry a foreign key to an episodic one, because that would tie something built to last to something built to end. The Deployment tables also hold at most one reference back across the Accounts boundary.

leadsaccounts
  • id
  • contact, company
  • status new, qualified, won, lost
  • converted_customer_id nullable
customersaccounts
  • id
  • name
  • billing_id
locationsaccounts
  • id
  • customer_id → current owner, denormalised
  • address
  • site_name
location_ownershipsaccounts · dated
  • id
  • location_id → locations
  • customer_id → customers
  • started_at, ended_at one open row max
quotesaccounts
  • id
  • customer_id → customers
  • status draft, sent, signed
  • signed_at nullable
  • … quote_lines one per system
connectiq_objectsdeployment
  • id
  • location_id → locations · set at creation, immutable (cross-domain)
  • quote_line_id → origin, audit only
  • status pending_install…
  • commissioned_at nullable
subscriptionsdeployment
  • id
  • connectiq_object_id
  • plan / term
  • starts_at, ends_at
inventory_itemsdeployment
  • id
  • serial_number
  • sku
  • owned_by company | customer
  • status in_stock, installed…
inventory_placementsdeployment · episodic
  • id
  • inventory_item_id
  • connectiq_object_id
  • job_id the cause
  • linked_at, unlinked_at
jobsservice · episodic
  • id
  • location_id
  • connectiq_object_id or lines
  • type, status
Guardrail

Keep a denormalised current_placement pointer if reads need it, but treat inventory_placements as the only source of truth. The pointer is written in the same transaction as the placement row, never on its own.