[comm]livev0.1.5

Communications

Generic, counterparty-agnostic communication log: address book (lists, contacts, endpoints) and an outbound/inbound message store across channels. Depends on nothing — business modules (crm, fin, wms) reach it through the parties-comm bridge or a raw source pointer.

Communications Module

A generic, counterparty-agnostic communication layer for dForge. comm depends on nothing — it is a peer spine to parties, not a dependent. Business modules (crm, fin, wms) reach it through a bridge (parties-comm) or via a raw source pointer on message.

Design principle

A contact owns its contact points (email, phone, messenger handle) the way an address book does — points are a directly-editable 1:N child of contact, typed straight into the contact card. No global endpoint registry, no link tables, no lookup-to-add-a-phone: address-book UX first.

Inbound resolution works by value: a message from anna@acme.example matches every contact that has that (channel, value) point. A sender that matches nothing gets an orphan point row (contact_id empty) so the message is still stored and can be linked to a contact later.

Case representation
Unknown inbound sender orphan contact_point, no contact
Same info@ used by two contacts two point rows, one per contact
Named buyer at a customer point → contact → party (via bridge)

Entities

Entity Role
contact_list Personal / shared / synced lists of contacts
contact A person or org you communicate with — deliberately thin (identity + list membership only)
contact_point 1:N child of contact: channel + value + label + primary; contact_id nullable for unlinked inbound senders. Per-channel format CHECKs (email regex, phone shape)
channel_account Mailbox / sender config; credentials referenced via admin.secret code, never stored inline
message The communication log — delivery/retry shape modeled on admin.webhook_delivery
message_attachment 1:N files on a message

Keep comm.contact thin

comm.contact carries only identity and list membership. Business attributes belong to crm.contact (sales) and parties.party (master data). The three concepts:

  • parties.partywho we do business with (master data, tax_id, is_customer/is_vendor)
  • crm.contactwho we're selling to (sales record: owner, activities)
  • comm.contactwho we can reach and how (address book)

Record linkage

message links to a source record via the nullable, indexed pair source_entity_cd + source_record_id (e.g. fin.invoice + pk). This needs no dependency on the consuming module. A reply inherits its parent's source pointer via in_reply_to → so a customer's reply to an invoice email lands back on the invoice.

The party timeline ("everything we've said to Acme") is the parties-comm bridge's job: it extends contact/contact_point with party_id and materializes a party_id breadcrumb on message for a single-index timeline query.

Seed data

Two lists (shared + synced), four contacts with directly-owned points, two orphan points (a shared info@ inbox and an unknown newsletter sender), one email channel account, and four messages exercising the demo-relevant states: sent, received-reply (threaded via in_reply_to), unmatched inbound, and failed-with-retries.

Sending ("comm.send()")

Sending is an insert, not an API: any module queues an outbound email by inserting a message row — cross-module from any action DSL:

insert('comm.message', {
	channel: 'email',
	direction: 'out',
	to_addr: 'ap@acme.example',        // status defaults to 'queued'
	subject: 'Invoice ' + [invoice_no],
	body_html: html,
	source_entity_cd: 'fin.invoice',
	source_record_id: [invoice_id]
})

The dispatch_messages scheduled job (every minute, 10 per tick, oldest first) hands queued email to the platform mailer via the sendEmail() built-in and marks rows sent. A Send Now action on message skips the wait. Composing a message by hand in the Messages grid queues it the same way (status defaults to queued).

v1 transport semantics, stated honestly:

  • sent means handed to the platform mailersendEmail() delivers post-execution, so SMTP failures are not observable from DSL (server logs only).
  • Transport uses the platform SMTP config; channel_account is declared sender identity, not yet a per-account transport (needs a platform email-sender that reads channel_account + admin.secret).
  • Attachments are not carried (raw sendEmail() has no attachment support).
  • Non-email channels stay queued until an adapter exists.

Deliberate non-goals (for now)

  • Global per-endpoint opt-out — points are per-contact, so a single global suppression flag has no home. When bulk mail arrives, add a suppression table keyed by (channel, value); do not re-introduce a global endpoint registry for it.
  • Message templates — print-template envelopes are reused instead.

Roadmap

  • v0.1.0 (this) — address book, message store, management grids, and the outbound email pipeline: queued-insert sending, the dispatch_messages scheduled job, and the Send Now action — hand-off to the platform mailer as described under Sending above. Still missing within outbound: per-account transport (channel_account + admin.secret driving real SMTP), delivery / failure observability (currently sent = handed off), attachments, and non-email channel adapters.
  • v0.2.0 — inbound ingest (mailbox polling), by-value sender matching, and a link-orphan-to-contact flow.
  • v0.3.0parties-comm bridge: contact/point↔party resolution + party timeline.
  • The timeline view type ships in @dforge/data-ui, not in this module.
/ explore more

Browse the full
module catalog.

Every module is metadata. Install in one click, customize freely, or fork it into Studio and reshape it for your business.