Company
Company is the central legal-entity relation in the Well financial graph. It represents any organization or counterparty a workspace interacts with — customers, vendors, banks, or the workspace's own operating company — and is the anchor for invoices, transactions, contacts, and connector-sourced enrichment. Every Company belongs to exactly one Workspace (workspace-scoped) and carries both human-readable profile fields and machine-normalized canonical identity fields (S0 tier) used by the invoice-payment scorer and deduplication pipeline. Companies are created by connector sync, invoice extraction, or manual entry, and enriched asynchronously via the @Enrichable decorator pipeline.
| Naming | Value |
|---|---|
| Object | Company |
Resource type (JSON:API type) | company |
| Collection / records root | companies |
| REST base | /v1/companies |
| Entity class | Company |
API operations
| Operation | Method & path | Status |
|---|---|---|
| List | GET /v1/companies | ✅ Implemented |
| Retrieve | GET /v1/companies/{id} | ✅ Implemented |
| Create | POST /v1/companies | ✅ Implemented |
| Update | PATCH /v1/companies/{id} | ✅ Implemented |
| Delete | DELETE /v1/companies/{id} | ✅ Implemented |
| Enrich | POST /v1/companies/enrich | ✅ Implemented |
Data model
Attributes
| Field | Type | Required | Constraints | Allowed values | Description |
|---|---|---|---|---|---|
| company_id | string, UUID, 🔒 system | ✅ Yes | unique; generated by gen_random_uuid() at INSERT | — | Stable public identifier for the company. Used in all API requests and JSON:API payloads. Internal pk is never exposed. |
| name | string | ✅ Yes | — | — | Display name of the company. May be set from trade_name or registered_name by the enrichment pipeline; this is the primary human-readable label surfaced in the UI. |
| description | string | ⚪ No | max 250 characters; @Enrichable — populated by enrichment pipeline | — | Short textual description of the company's activities or industry. Populated by the enrichment pipeline; editable by the user. |
| domain | string | ⚪ No | max 500 characters | — | Primary website domain (e.g. 'anthropic.com'). Used as a Tier-2 identity signal for deduplication and enrichment. User-editable or connector-populated. |
| locale | string (ISO 639-1 enum) | ⚪ No | default 'en'; nativeEnumName: local_enum | ISO 639-1 two-letter codes: aa, ab, ae, af, ak, am, an, ar … +176 more | Preferred display locale for this company's communications. Defaults to English ('en'). |
| tax_id_value | string | ⚪ No | @Enrichable — populated by enrichment pipeline | — | The raw tax identifier string (e.g. '88-1234567' for a US EIN, '58879425137' for a French SIREN). Interpreted together with tax_id_type. |
| tax_id_type | string (TaxIdTypeEnum) | ⚪ No | @Enrichable; nativeEnumName: tax_id_type_enum | VAT, VATIN, TVA, IVA, MwSt, BTW, MOSS, OSS … +90 more | Classification of the tax identifier. Paired with tax_id_value to form a globally unambiguous tax identity. Used by the canonical_tax_id normalizer to construct the Tier-1 identity key. |
| trade_name | string | ⚪ No | max 100 characters; CHECK: CHAR_LENGTH(trade_name) >= 1; @Enrichable | — | DBA / trading name distinct from the legal registered_name (e.g. 'Anthropic' vs 'Anthropic PBC'). The CHECK constraint ensures it is never an empty string if set. |
| registered_name | string | ⚪ No | max 255 characters; nullable; @Enrichable | — | Full legal name as registered with the relevant authority (e.g. company registry, chamber of commerce). Used by canonical_legal_name normalizer. |
| business_type | string (BusinessTypeEnum) | ⚪ No | @Enrichable; nativeEnumName: business_type_enum | Inc, Corp, LLC, LLP, LP, LLLP, PC, PLLC … +176 more | Legal entity form of the company. Covers legal structures from 40+ jurisdictions. Used by the enrichment pipeline to classify the organization type. |
| registered_value | string | ⚪ No | max 100 characters; @Enrichable | — | Registration number in the applicable registry (e.g. a company number from Delaware Division of Corporations, or a SIREN number). Combined with registry_name and registry_country to form the canonical_registry key. |
| registry_name | string | ⚪ No | @Enrichable | — | Human-readable name of the registry where the company is registered (e.g. 'Delaware Division of Corporations', 'RCS Paris'). Used in canonical_registry construction. |
| registry_country | string (CountryCodeEnum, ISO 3166-1 alpha-2) | ⚪ No | @Enrichable; nativeEnumName: country_code_enum | ISO 3166-1 alpha-2 country codes (e.g. US, FR, DE, GB, ...) | Country of the registry authority. Forms the geographic prefix of canonical_registry and enables registry-system-aware deduplication. |
| canonical_tax_id | string | ⚪ No | max 64 characters; partial index on (workspace_pk, canonical_tax_id) WHERE deleted_at IS NULL; NULL until S2 backfill runs | — | Country-prefixed, normalized tax identifier. Format: <ISO2><stripped_digits> — e.g. 'FR58879425137'. Tier-1 identity signal used by the invoice-payment scorer and deduplication pipeline (S0/S3+). Populated exclusively by the S1 normalization module and S2 backfill. |
| canonical_registry | string | ⚪ No | max 128 characters; partial index on (workspace_pk, canonical_registry) WHERE deleted_at IS NULL; NULL until S2 backfill runs | — | Country + registry name + registered_value concatenated. Format: <ISO2>-<REGISTRY>-<VALUE> — e.g. 'FR-RCS-PARIS-879425137'. Tier-1 identity signal for registry-system-aware dedup. Populated by S1 normalization module. |
| domain_normalized | string | ⚪ No | max 255 characters; partial index on (workspace_pk, domain_normalized) WHERE deleted_at IS NULL; NULL until S2 backfill runs | — | Root domain, lowercase, no www, no path. Derived from company.domain or the first email domain. Tier-2 identity signal for domain-level deduplication. Populated by S1 normalization module. |
| email_domains | string[] | ⚪ No | PostgreSQL text[] array type; GIN index on email_domains WHERE deleted_at IS NULL; NULL until S2 backfill runs | — | Array of normalized email domains derived from the company_emails pivot table. GIN-indexed for set-containment queries (@>, &&). Tier-2 identity signal for email-domain overlap matching. |
| canonical_legal_name | string | ⚪ No | max 255 characters; partial index on (workspace_pk, canonical_legal_name, registry_country) WHERE deleted_at IS NULL; NULL until S2 backfill runs | — | Punctuation-stripped, case-folded, legal-suffix-dropped company name. Example: 'Anthropic PBC' → 'anthropic'. Tier-3 fuzzy signal used only when Tier-1/2 signals are absent. Populated by S1 normalization module. |
| identity_completeness | number (numeric(4,3)) | ⚪ No | range 0.000–1.000; NULL until first S1/S2 computation | — | Completeness score representing the fraction of canonical identity fields (canonical_tax_id, canonical_registry, domain_normalized, email_domains, canonical_legal_name) that are non-NULL. Recomputed by S1 normalization on every write and by S2 backfill. Used to prioritize enrichment and dedup confidence. |
| created_at | datetime, 🔒 system | ✅ Yes | set by onCreate lifecycle hook; never writable by API consumer | — | Timestamp when this company record was first created. Set automatically at INSERT. |
| updated_at | datetime, 🔒 system | ⚪ No | set by onCreate and onUpdate lifecycle hooks | — | Timestamp of the most recent modification to this company record. Auto-updated on every write. |
| deleted_at | datetime | ⚪ No | nullable; NULL = active; non-NULL = soft-deleted. All queries must filter deleted_at IS NULL. | — | Soft-delete timestamp. When set, the company is hidden from all standard queries. Hard deletes are not used for this entity. All partial indexes are scoped to WHERE deleted_at IS NULL. |
Relationships
| Name | Type | Required | Description |
|---|---|---|---|
| workspace | to-one (workspace) | ⚪ No | The Workspace this company belongs to. Provides tenant isolation — all company queries filter by workspace_pk. Nullable to support global/catalog companies in future patterns, but in practice every workspace-scoped company has this set. The partial indexes include workspace_pk in every WHERE clause. |
| source_workspace_connector | to-one (workspace_connector) | ⚪ No | The WorkspaceConnector instance (connector sync) that originally created this company row. NULL means the row was created manually, by invoice extraction, or by enrichment fallback. Used by the counterparty-bank discovery skill to distinguish 'connector-confirmed' from 'inferred' banks. |
| relations | to-many (company_relation) | — | CompanyRelation pivot rows where this company is the source. Represents inter-company relationships (e.g. parent/subsidiary, affiliated entity). Maps to CompanyRelation.source_company. |
| people | to-many (company_person) | — | CompanyPerson junction rows linking this company to Person entities. Carries role/title metadata. Maps to CompanyPerson.company. |
| social_links | to-many (company_web_link) | — | @Enrichable. Web link entries (LinkedIn, Twitter/X, website, etc.) attached to the company. Populated by the enrichment pipeline. |
| locations | to-many (company_location) | — | @Enrichable. Physical address/location entries for the company (HQ, branch offices, etc.). Populated by the enrichment pipeline. |
| emails | to-many (company_email) | — | @Enrichable. CompanyEmail pivot rows carrying email address, is_primary, is_verify, and label metadata. Used as Tier-2 identity signal via email_domains array. Populated by enrichment pipeline and connector sync. |
| phones | to-many (company_phone) | — | @Enrichable. CompanyPhone pivot rows with phone number, is_primary, is_verify, and label. Populated by enrichment pipeline. |
| categories | to-many (company_category) | — | CompanyCategory pivot rows classifying the company into industry/sector taxonomy. Not @Enrichable — assigned programmatically or by users. |
| media | to-many (company_media) | — | @Enrichable. CompanyMedia entries (logos, cover images) attached to this company. The logo is the primary visual identifier used in composite cells on the records table. |
| company_financial | to-one (company_financial) | ⚪ No | One-to-one CompanyFinancial record carrying aggregated financial summary data for this company (e.g. balance, open invoices). Owned by the CompanyFinancial side. Nullable — only present once financial data has been computed. |
| company_workspace_connectors | to-many (company_workspace_connector) | — | CompanyWorkspaceConnector junction rows recording which connector sync instances have touched this company, including the direction (input/output). Used to distinguish connector-confirmed banks from inferred ones in the counterparty-bank discovery skill, and to render the composite_sourced_from / composite_routed_to virtual fields on the records table. |
System-computed
- company_id is generated by PostgreSQL gen_random_uuid() at INSERT via
defaultRaw. It is immutable after creation and used as the public JSON:API id. - created_at is set by an onCreate MikroORM lifecycle hook to new Date() at INSERT time. Never updated thereafter.
- updated_at is set by both onCreate and onUpdate lifecycle hooks to new Date(). Reflects the most recent flush of any column on the entity.
- deleted_at is NULL on active records. Set to a non-null timestamp to soft-delete. All active queries filter WHERE deleted_at IS NULL. Hard deletes are not used for this entity.
- canonical_tax_id is computed by the S1 normalisation module from tax_id_value and registry_country. Format: <ISO2><stripped_digits>. Written only by the normalizer — never by connector sync or user PATCH. NULL until the S2 backfill runs.
- canonical_registry is computed by S1 from registry_country + registry_name + registered_value. Format: <ISO2>-<REGISTRY>-<VALUE>. NULL until S2 backfill runs.
- domain_normalized is derived from company.domain or the first email domain: root domain, lowercase, no www, no trailing path. NULL until S2 backfill runs.
- email_domains is a text[] array populated by the S1 normalizer by aggregating all CompanyEmail rows for this company and extracting their domain parts. GIN-indexed. Kept in sync on CompanyEmail writes.
- canonical_legal_name is produced by stripping punctuation, case-folding, and dropping common legal suffixes from registered_name or trade_name. E.g. 'Anthropic PBC' → 'anthropic'. NULL until S2 backfill runs.
- identity_completeness is a numeric(4,3) score in [0.000, 1.000] computed by S1 as the fraction of {canonical_tax_id, canonical_registry, domain_normalized, email_domains, canonical_legal_name} that are non-NULL. Recomputed on every S1 write and by S2 backfill. NULL until first computation.
- source_workspace_connector (FK workspace_connector_pk) records which connector sync originally created this company row. NULL means the row was created by invoice extraction, manual entry, or enrichment fallback — not by a connector. Presence in company_workspace_connectors.direction='input' is the 'connector-confirmed' signal used by the counterparty-bank discovery scorer.
- Fields decorated with @Enrichable (description, tax_id_value, tax_id_type, trade_name, registered_name, business_type, registered_value, registry_name, registry_country, social_links, locations, emails, phones, media) are eligible for population by the asynchronous enrichment pipeline (Cloud Tasks). The decorator marks the field for inclusion in the enrichment delta computation.
- locale defaults to LocalEnum.EN ('en') via the ORM default. Represents ISO 639-1 locale for the company's communications context.
Example
{
"data": {
"type": "company",
"id": "3fa1c2d4-87ae-4b10-a9f3-ec5d1234abcd",
"attributes": {
"company_id": "3fa1c2d4-87ae-4b10-a9f3-ec5d1234abcd",
"name": "Anthropic PBC",
"description": "AI safety company building reliable, interpretable AI systems.",
"domain": "anthropic.com",
"locale": "en",
"tax_id_value": "88-1234567",
"tax_id_type": "EIN",
"trade_name": "Anthropic",
"registered_name": "Anthropic PBC",
"business_type": "PBC",
"registered_value": "7543821",
"registry_name": "Delaware Division of Corporations",
"registry_country": "US",
"canonical_tax_id": "US881234567",
"canonical_registry": "US-DELAWARE-7543821",
"domain_normalized": "anthropic.com",
"email_domains": ["anthropic.com"],
"canonical_legal_name": "anthropic",
"identity_completeness": 1.000,
"created_at": "2025-03-15T10:22:00.000Z",
"updated_at": "2026-01-04T08:11:45.000Z",
"deleted_at": null
},
"relationships": {
"workspace": {
"data": { "type": "workspace", "id": "a1b2c3d4-0000-4000-8000-000000000001" }
},
"source_workspace_connector": {
"data": { "type": "workspace_connector", "id": "c9d8e7f6-1111-4111-8111-000000000002" }
},
"emails": {
"data": [{ "type": "company_email", "id": "e1f2a3b4-2222-4222-8222-000000000003" }]
},
"phones": { "data": [] },
"locations": {
"data": [{ "type": "company_location", "id": "d5e6f7a8-3333-4333-8333-000000000004" }]
},
"social_links": {
"data": [{ "type": "company_web_link", "id": "b9c0d1e2-4444-4444-8444-000000000005" }]
},
"people": {
"data": [{ "type": "company_person", "id": "f3a4b5c6-5555-4555-8555-000000000006" }]
},
"categories": { "data": [] },
"media": {
"data": [{ "type": "company_media", "id": "a7b8c9d0-6666-4666-8666-000000000007" }]
},
"company_financial": {
"data": { "type": "company_financial", "id": "e1e2e3e4-7777-4777-8777-000000000008" }
},
"relations": { "data": [] }
}
}
}apps/api/src/database/entities/Company.ts · domain: financial-graph · tier: Main