Domain Access Architecture
Status
- document type: current architecture plus extension rules
- source of truth: code for current behavior, this document for the intended mental model around access and native entity reuse
- target architecture sections are explicitly marked below
Account directly or indirectly. Access is then enforced through a combination of:
- account membership
- role or custom role permissions
- inbox membership
- team membership
- Pundit policies
- list-level filtering services
Core Principles
Accountis the primary isolation boundary.Useris global, but access inside an account is defined byAccountUser.Inboxmembership drives most agent visibility.Conversationvisibility is stricter than simple account membership.- The inherited
enterprise/layer extends OSS access rules withCustomRole, and in Onelink these capabilities are part of the active product surface rather than a separate paid tier. - Labels and custom attributes are account-scoped metadata layers.
High-Level Domain Model
Account Boundary
Account is the main aggregate root for operational data. In practice it owns:
- inboxes
- contacts
- conversations
- labels
- hooks and integrations
- custom attribute definitions
- teams
- portals
Current.account and then resolve records inside that scope. A user may belong to several accounts, but each request is resolved against exactly one current account.
Authentication And Account Resolution
The typical request flow for account-scoped APIs is: The important implication is that account membership is checked before feature-level authorization. If a user is not linked throughAccountUser, nothing else matters.
Users, Roles, And Membership
User
User is a global identity record. It can belong to multiple accounts through AccountUser. It also has direct links to:
- assigned conversations
- inbox memberships
- team memberships
- notification settings
- personal macros
AccountUser
AccountUser is the real per-account identity. It stores:
role:agentoradministratoravailability- inviter
custom_role_idimplemented in the inheritedenterprise/layeragent_capacity_policy_idimplemented in the inheritedenterprise/layer
permissions are coarse:
- administrator =>
administrator - agent =>
agent
enterprise/ code path, an agent may also carry a CustomRole, which replaces coarse access with a more granular permission set while still remaining an agent at the account level. In Onelink this should be treated as an active capability, not as a separate product tier.
Custom Roles
The inheritedenterprise/ layer adds CustomRole with the following permission set:
conversation_manageconversation_unassigned_manageconversation_participating_managecontact_managereport_manageknowledge_base_manage
custom_role plus the selected permission keys.
Access Model
OSS Access
The OSS rules are intentionally simple:- administrators can access all inboxes in the current account
- agents can access only inboxes where they are members
- conversation visibility is allowed if the user has inbox access or team access
Custom Role Access
The inheritedenterprise/ layer narrows agent visibility further when a custom_role_id is present:
conversation_manage=> all conversations from accessible inboxesconversation_unassigned_manage=> unassigned conversations plus conversations assigned to selfconversation_participating_manage=> only conversations assigned to self or where the user is a participant
- list filtering
- record-level
show?checks
Visibility Matrix
Inboxes And Channel Integrations
Inbox is the operational container for conversations. It belongs to an account and wraps a polymorphic channel.
Common channel families include:
- website widget
- API inbox
- SMS and Twilio
- Telegram
- Line
- Facebook and Instagram
- TikTok
inbox_members- conversations
- contact_inboxes
- inbox-level hooks
- assignment-related configuration
Why Inbox Membership Matters
Inbox membership is the main visibility gate for agents. A user can be a valid member of the account and still be unable to see most conversations if they are not a member of the relevant inboxes.Contacts, Contact Inboxes, And Conversations
Contact
Contact belongs to an account and stores:
- identity fields such as
email,phone_number,identifier additional_attributescustom_attributes- labels
- conversations
contact_inboxes.
Company
Company is the account-scoped organization entity used to group business contacts under a shared customer or client company.
In the current codebase it already exists and should be treated as a valid shared CRM building block, not as an accidental one-off enterprise artifact.
It currently provides:
- one
Accountto manyCompany - one
Companyto manyContact - company fields such as
name,domain,description,avatar, andcontacts_count - auto-association from a contact’s business email domain
- company search, sorting, and a dedicated dashboard list view
Company should be reused whenever Onelink needs a B2B grouping axis such as:
- multiple contacts belonging to the same client organization
- future deals attached to an organization, not just a single contact
- domain workflows where the customer is structurally a company, clinic, contractor, developer, or buyer organization
- keep
Contactas the person-level entity - keep
Companyas the organization-level entity - prefer linking future CRM entities such as
Dealtocompany_idwhen the workflow is organization-centric - use custom attributes to extend
Companyfor domain needs instead of cloning separate organization models too early
Company should therefore be considered part of the shared platform CRM vocabulary for generic, healthcare, and construction domain profiles.
ContactInbox
ContactInbox is the per-channel identity bridge:
- one contact can have many contact inboxes
- one inbox can have many contact inboxes
source_idrepresents the external identity in that inbox
Conversation
Conversation ties together:
- account
- inbox
- contact
- contact_inbox
- optional assignee
- optional team
- status
- priority
- additional attributes
- custom attributes
- labels
Company Association Flow
Company association can be auto-inferred from a contact email domain when the email is first set.Labels
Label is an account-scoped taxonomy record. The actual attachment mechanism uses acts_as_taggable_on :labels through the shared Labelable concern.
That means labels are not limited to a single entity type. In this area they are used primarily on:
- contacts
- conversations
- titles are normalized to lowercase
- label names are unique inside an account
- renaming a label triggers an async update for associated records
Custom Attributes
Custom attributes are split into two layers:- schema:
CustomAttributeDefinition - values: JSONB
custom_attributesonContactorConversation
conversation_attributecontact_attribute
- text
- number
- currency
- percent
- link
- date
- list
- checkbox
status, priority, email, phone_number, and similar standard keys.
Integrations
Integrations use two related concepts:Integrations::App
This is the app catalog and capability descriptor. It defines:
- app id
- action URL
- fields
- feature flag requirements
- hook type
- schema for settings
Integrations::Hook
This is the installed integration instance. It belongs to an account and optionally to an inbox.
Hook characteristics:
hook_type:accountorinboxstatus:enabledordisabled- optional encrypted
access_token settingsvalidated by JSON schema when applicable- creation can trigger integration-specific setup jobs
Native Platform Building Blocks
The current codebase already contains a strong set of reusable building blocks. Domain architecture should grow from these primitives instead of replacing them with parallel models too early.Workspace / Tenant
Accountis the actual workspace or tenant boundary- use
Accountfor domain profile selection, feature rollout, settings, limits, and data isolation - do not introduce a second internal
Workspaceaggregate whenAccountalready owns the platform surface
Person And Organization Layer
Contactis the person-level entityCompanyis the organization-level entityNoteis already the native account-scoped way to store structured internal notes on contacts
- healthcare: patient or related person =>
Contact - construction: buyer or project stakeholder =>
Contact - B2B customer, clinic, contractor, developer, or buyer organization =>
Company - operator notes, qualification notes, domain observations =>
Note
Communication And Ownership Layer
Conversationis the communication threadInboxis the channel boundaryTeamis the native group ownership and routing primitiveAccountUserremains the per-account access identity
- shared assignment and routing logic should continue to use inboxes and teams
- domain zones should not invent their own parallel ownership model unless the current team/assignee model becomes insufficient
Classification And Variability Layer
Labelis the lightweight tagging and segmentation layerCustomAttributeDefinitionplus JSONB values is the current variability layer forContactandConversationadditional_attributesremains useful for imported/external/raw metadata
- labels for fast operational classification and segmentation
- custom attributes for domain-specific and tenant-specific fields
- core columns for stable business identity, ownership, status, and relationships
Automation And Repeatability Layer
Macroalready exists for reusable operational actionsAutomationRulealready exists for account-scoped workflow automationIntegrations::Hookalready exists for external system connections and app-level setup
- macros for agent/operator shortcuts
- automation rules for shared event-driven flows
- hooks/custom tools/integrations for external system side effects instead of embedding integration logic into core entities
AI And Knowledge Layer
Captain::Assistantis the account-scoped assistantCaptain::Documentis the knowledge source layerCaptain::CustomToolis the account-scoped action/tool layerCopilotThreadis the operator-facing AI collaboration thread
- treat Captain as the shared AI layer for all domain zones
- make Captain domain-aware through documents, prompts, tools, and scenarios
- do not use Captain as a substitute for core domain entities or business workflows
Native-First Extension Strategy
When adding a new feature, prefer this order:- reuse an existing native entity
- extend with labels, notes, custom attributes, settings, or Captain configuration
- add domain service, policy, workflow, or UI composition
- add a new shared platform entity only if the behavior is stable across domains
- add a new domain entity only if the lifecycle and business semantics are truly distinct
- do not create a new internal workspace model when
Accountalready fits - do not create parallel organization models before exhausting
Company - do not create vertical-specific person models before exhausting
Contact - do not encode critical workflow state only in labels
- do not force custom attributes to replace core columns or relations
Target Direction Appendix
Everything in this appendix is planning guidance, not proof that the runtime architecture already exists.Domain-Zone Fit
The current recommended fit for domain zones is:Generic
- use shared
Account,Company,Contact,Conversation,Label,Note,Macro,Captain - allow flexible custom attributes and broad tenant configuration
Healthcare
- reuse
Accountas workspace - reuse
Companyfor clinic, partner, insurer, employer, or referring organization - reuse
Contactfor patient or person-level actor - store healthcare-specific fields first through contact/conversation custom attributes
- use labels and notes for intake/operational segmentation
- use Captain documents/scenarios/tools for medical-domain guidance and handoff support
Construction
- reuse
Accountas workspace - reuse
Companyfor developer, contractor, supplier, or buyer organization - reuse
Contactfor buyer, manager, estimator, or stakeholder - store construction-specific fields first through contact/conversation custom attributes
- use labels and notes for qualification and delivery segmentation
- use Captain documents/scenarios/tools for estimate/project knowledge and operator support
Near-Term Plan
The near-term implementation plan should stay native to the repo:- keep
Accountas the workspace boundary and attach domain profile/config at account level - treat
CompanyandContactas mandatory shared CRM primitives - use notes, labels, and current custom attributes to cover early domain variance
- use teams, macros, automation rules, and integrations as the native operational layer
- use Captain as the shared AI layer with domain-specific documents, scenarios, and tools
- only after that introduce shared CRM entities such as
Deal,Task, andActivity - add domain-specific entities only when generic shared primitives stop matching the real lifecycle
Policy Summary
The current policy behavior for the requested domains is roughly:| Domain | Agent | Administrator | Custom role |
|---|---|---|---|
| Contacts | Can view/create/update/search/filter | Full, plus import/export/destroy | Same as agent unless extra enterprise policy added |
| Inboxes | Can view assigned inboxes | Full manage | Same inbox membership base |
| Conversations | Can view by inbox or team access | Full view, can destroy | Narrowed by custom role permissions |
| Labels | Can list | Can create/update/show/destroy | No special label permission layer by default |
| Hooks / Integrations | process_event only | Create/update/destroy | No custom hook permission layer by default |
| Companies | Implemented under inherited enterprise/, broad access except destroy | Full | No dedicated custom permission in current model |
Architectural Notes
What Drives Visibility
The practical order of visibility checks is:- account membership
- inbox membership
- team membership where relevant
- role or custom role permissions
- endpoint policy
One Important Inconsistency
CustomAttributeDefinitionsController is account-scoped but does not currently perform an explicit policy check in the same way as contacts, labels, and hooks. That means access there is effectively gated by account membership rather than a dedicated authorization rule.
Recommended Mental Model
When adding or changing features in this area, use this model:- start from
Account - determine whether the actor is a
User,AgentBot, or platform actor - resolve
AccountUser - determine inbox visibility
- apply conversation or entity-specific policy checks
- only then apply business behavior
Platform Direction
Onelink should evolve as a layered product instead of a single fork with domain-specific conditionals spread across the codebase. The intended layers are:upstream/core: the Chatwoot-compatible base and the smallest possible set of fork-specific patchesonelink platform: shared branded product capabilities used by all customersdomain zones: isolated vertical extensions such as healthcare and construction
- shared product behavior goes into platform
- vertical-specific behavior goes into the matching domain zone
- tenant-specific variation should prefer configuration before new code
CRM Layering
CRM should not be split into separate standalone products per vertical. It should be one shared engine with domain extensions. Recommended structure:core CRM: deals, tasks, pipelines, stages, activities, ownership, labels, search, permissionsdomain CRM: healthcare-specific and construction-specific rules, fields, screens, workflows, reports, integrationstenant config: company-level overrides through settings, custom attributes, forms, dashboards, and automation configuration
generic should be treated as a first-class domain profile, not as an undefined fallback mode.
Core vs Domain Responsibility
Short rule set:coreowns stable shared entities and mechanicsdomainowns vertical vocabulary, validations, workflows, screens, and reportscustom attributesextend entities but do not replace core entities
DealandTaskshould stay separate shared entities- system-critical fields should be first-class columns or associations
- domain-specific fields can live in custom attributes
- tenant-only needs should prefer configuration and custom fields before new models
deal stage,owner,status,pipeline,due datebelong in core entitiesinsurance_provider,doctor_name,project_area,site_address_notescan be domain or tenant custom attributes
Delivery Model For Domain Zones
Domain development should be isolated by ownership and compatibility checks. Recommended model:- one platform owner responsible for shared architecture and upstream sync
- one owner per domain zone
- changes in a domain zone should avoid modifying shared core unless the capability is reusable
- if a feature is needed by two or more domains, promote it into platform
Development Guardrails
To keep the fork maintainable while domain logic grows:- minimize direct patches in Chatwoot-like core code
- prefer extension points, services, concerns, and isolated routes/screens
- avoid scattering healthcare and construction conditionals throughout shared models and controllers
- treat
enterprise/as a technical overlay, not as the product boundary - keep plan gating separate from domain specialization
Working Rule For Future CRM Work
When designing a new CRM feature, answer these in order:- Is this a shared platform capability or a domain-specific behavior?
- Does it belong to a stable core entity or to custom attributes?
- Can tenant configuration solve it before new code is added?
- If code is needed, should it live in
core,platform, or a specific domain zone?