TotalApp Docs

Unified Contact Directory Architecture

How TotalApp models customers, vendors, leads, and companies as one record — and how CRM, Sales, Purchasing, and Call Center all read and write it.

Overview

Most CRM/ERP products end up with three or four overlapping "who is this?" concepts — a CRM Account, a Sales Customer, a Purchasing Vendor, a Contact — each stored separately, each drifting out of sync with the others. TotalApp avoids that by storing every person and company in a single unified Contact record. What a contact is — a customer, a vendor, a lead, a company, a manufacturer — is not which table it lives in, it's a set of boolean role flags on that one record. This page is the architectural reference for that model: the data shape, the lifecycle, and exactly how each module — CRM Accounts, Sales Pipeline, Vendor Management, and Call Center — reads and writes it.

Where "Accounts" Went — and Why It Came Back Differently

Earlier versions of TotalApp had a CRM screen called "Accounts," which clashed with Finance's Chart of Accounts (the same word meaning two different things depending on which module you were in — a well-known ambiguity in Odoo/Salesforce/HubSpot-style products). That screen became Customers, and "Accounts" was reserved for Finance's ledger accounts. TotalApp later added a company-centric CRM Accounts screen — but rather than backing it with a separate account table (the classic source of the original ambiguity), it reads and writes the exact same unified Contact directory described on this page, filtered to isCompany. There is still only one place a company record lives.

The Contact Record

Every person or company — regardless of what module you found them in — is one row with this shape:

FieldTypePurpose
id, namestringIdentity.
parentIdstring | nullSelf-referencing link to another Contact's id. On an individual, points at the company they work at (parentId = companyContact.id); on a company, always null. Listing "everyone at company X" is contacts.filter(c => c.parentId === companyId) — no separate join table.
company, email, phonestringStandard contact details.
rolesobject of booleansWhich relationships this contact has with you — see the Role Flags table below. Several can be true at once.
lifecycleStageenumsuspectleadopportunitycustomer. The contact's own CRM funnel position — distinct from any Deal's pipeline stage.
statusenumactive / inactive / pending.
tagsstring[]Free-text category tags (industry, vendor category, source like "call-center").
industry, website, addressstring (optional)Company-profile fields. Present on every contact record, but only meaningful — and only editable through the UI — when roles.isCompany is true; individual contacts leave them blank. Shown and edited from CRM Accounts.
vendorProfileobject (optional)Present only when roles.isVendor is true — tax ID, VAT number, address/city/country, payment terms, currency, categories, and performance metrics. See "Vendor Profile Fields" below.

Role Flags

A contact's role flags can combine freely. A supplier who also buys your software is isVendor: true AND isCustomer: true on the exact same record — never two separate rows to keep in sync.

FlagMeaningRead by
isCompanyLegal entity rather than an individual person.Customers → Companies tab, CRM Accounts (entire screen)
isCustomerBuys goods/services from you.Customers → Customers filter
isVendorSells goods/services to you (a.k.a. supplier).Vendor Management, Customers → Vendors filter
isManufacturerProduces parts or goods (Manufacturing / MOM context).Reserved for manufacturing-side screens.
isTransporterCarrier / logistics provider.Reserved for Warehouse/Logistics screens.
isLeadSales prospect, not yet converted to a paying customer.Customers → Leads filter, Sales Pipeline
isEmployeeInternal staff member.Reserved for internal task/expense contexts.

Filtering By Role, Not By Table

Because there is only one table, every screen that shows "customers" or "vendors" is really the same directory with a different filter applied. The CRM Accounts screen's role filter bar (All / Customers / Vendors / Companies / Leads) is the most visible example of this — switching filters never re-queries a different dataset, it just changes which flag is checked.

Lifecycle Stage vs. Deal Stage — Don't Confuse Them

This is the single most common point of confusion when integrating a new module against the contact directory, so it gets its own section:

Suspect Lead Opportunity Customer
  • Contact.lifecycleStage belongs to the contact itself — one value, always up to date, describing where this person/company sits in your overall relationship funnel.
  • Deal.stage (Qualified / Proposal / Negotiation / Closed Won / Closed Lost, in Sales Pipeline) belongs to a specific opportunity tied to that contact. A single contact can have several open Deals at once, each in a different stage, while the contact's own lifecycleStage stays wherever it currently is.
  • The two are connected by exactly one automation: dragging a linked Deal to Closed Won advances the contact's lifecycleStage to customer (see "Closed-Won Automation" below). No other Deal stage change touches lifecycleStage.

Module Integration Map

Five modules currently read from and write to the unified directory. Each uses a different entry point appropriate to how that module works day to day:

CRM Accounts

The directory's primary UI. Lists, role-filters, creates (isCustomer), and edits contacts directly. The Companies tab is the same data filtered to isCompany.

CRM Accounts

The company-only view of the same directory (isCompany). Creates and edits industry/website/address fields, and is the source for the Account picker on Deals and Activities.

Sales Pipeline

Links Deals to a person (contactId) and/or a company (accountId) — both point into the same directory. Automatically promotes whichever is linked to Customer when the Deal reaches Closed Won.

Vendor Management

Creates and edits contacts with isCompany + isVendor, attaching a vendorProfile (tax, payment terms, performance metrics) to the shared record.

Call Center

Every outbound call resolves the dialled number against the directory — matches an existing contact, or auto-creates a new Lead (isLead, lifecycleStage: suspect) so no caller is ever lost.

Deal.accountId and CrmActivity.accountId Point Into the Same Table

A Deal or an Activity carries both a contactId (a person row's id) and an optional accountId (a company row's id) — both fields resolve against the exact same Contact directory, just different rows. There's no separate accounts table for accountId to reference.

Automation Events

Two automations keep the directory current without manual re-entry. Both are one-way and idempotent — running them twice on an already-converted contact is a no-op.

TriggerWhereEffect on the Contact
Closed-Won conversion A Deal linked to a contact and/or a company is dragged to a Closed Won column in Sales Pipeline. roles.isCustomertrue, roles.isLeadfalse, lifecycleStagecustomer — applied to contactId's row and, if different, accountId's row too. Deals with neither linked, or dropped into any non-won stage, do nothing.
Call Center auto-lead An outbound call is started in Virtual PBX → Start Call to a phone number not already in the directory. A new contact is created: roles.isLeadtrue, lifecycleStagesuspect, tags includes "call-center". Dialling the same number again reuses the existing contact instead of duplicating it.

Automations Never Downgrade a Contact

Neither automation ever clears isCustomer or reverts lifecycleStage backwards. A contact only ever moves forward through the funnel automatically; demoting a contact (e.g. marking a former customer inactive) is always a manual edit.

Vendor Profile Fields

Reserved for contacts with roles.isVendor: true — folded onto the same record rather than a separate vendor table, so a contact that's both a customer and a vendor stays one row:

FieldDescription
taxId, vatNumberTax and VAT registration numbers.
address, city, countryRegistered business location.
paymentTermsDays, currencyPayment terms and billing currency used across purchase documents.
categoriesFree-text tags describing what the vendor supplies.
metricstotalOrders, onTimeDeliveries, lateDeliveries, averageLeadTimeDays, qualityScore, reliabilityScore, totalSpent, lastOrderDate — computed from purchase order history and shown on the vendor's profile panel.

Frequently Asked Questions

Why one table instead of separate Customer/Vendor/Lead tables?
Because real-world relationships overlap constantly — a supplier that also buys from you, a lead who becomes a customer, a company that is both. Separate tables force you to either duplicate the record (and let the copies drift out of sync) or build cross-table joins everywhere. One record with role flags means every module always sees the same, current truth about who a contact is.
If I update a contact's email in Vendor Management, does it change everywhere?
Yes. Vendor Management, Customers, Sales Pipeline, and Call Center all read and write the same underlying contact record — there is no per-module copy to keep in sync. Editing a shared field (name, email, phone) from any module updates it everywhere immediately.
What's the difference between a contact's lifecycleStage and a Deal's stage?
lifecycleStage is a single property of the contact itself, describing its overall relationship with you (suspect → lead → opportunity → customer). A Deal's stage (Qualified, Proposal, Negotiation, Closed Won, Closed Lost) belongs to one specific opportunity in Sales Pipeline — a contact can have several Deals in different stages simultaneously, while its own lifecycleStage reflects the furthest any of them (or a direct sale) has actually progressed.
Does calling a number from Call Center ever create a duplicate contact?
No. Start Call normalises the dialled number and checks it against every contact's phone field before creating anything. A recognised number reuses the existing contact (and shows its name and role); only a genuinely new number gets a new Lead record.
Can a contact lose its Customer status automatically?
No. The Closed-Won and Call Center automations only ever move a contact forward (toward Customer). Marking a contact inactive, or removing a role flag, is always a manual edit from the Customers or Vendor Management screen — there is no automatic downgrade path.
Is this the same "Contacts" as the Contacts & Deals Kanban screen?
They share the same underlying directory, but serve different jobs. Contacts & Deals is the day-to-day CRM Kanban for managing active opportunities. This page documents the data model underneath — the Contact record, its role flags, and how Customers, CRM Accounts, Sales Pipeline, Vendor Management, and Call Center all integrate with it.
Is CRM Accounts backed by a separate table from Customers?
No. CRM Accounts and Customers read and write the exact same Contact directory. CRM Accounts is simply the same data filtered to isCompany, with company-specific fields (industry, website, address) surfaced in its form. A company created in either screen appears identically in the other, with no import or sync step involved.
How do I find all the people who work at a given company?
Filter the directory by parentId: every individual contact created under a company has parentId set to that company's Contact id, so contacts.filter(c => c.parentId === companyId) returns its people. There is no separate company_id foreign-key table — parentId on the same Contact record does the job.