Skip to main content

Source: ebisu/docs/adr/0056-maritime-kyc-entity-system.md | ✏️ Edit on GitHub

ADR-0056: Maritime KYC/AML Entity and Association System

Status

Accepted

Context

Our intelligence platform needs to track complex relationships between vessels, people, and companies for:

  1. Sanctions compliance - Identifying sanctioned entities and their networks
  2. Beneficial ownership - Understanding who ultimately controls vessels
  3. Risk assessment - Tracking crew involved in IUU fishing or forced labor
  4. Fleet analysis - Mapping corporate structures and vessel ownership networks
  5. Crew tracking - Monitoring captain/crew movement between vessels

Current limitations:

  • WRO data includes company-level bans (e.g., Dalian Ocean Fishing) with no vessel mapping
  • SDN lists include associated entities but no structured relationships
  • No way to track beneficial ownership chains
  • No crew role tracking (captain, first mate, engineer, etc.)
  • No temporal tracking of relationship changes

Decision

Implement a comprehensive KYC/AML-compliant entity and association system following financial industry best practices adapted for maritime domain:

Core Entity Tables

-- Central entity table for all people and organizations
CREATE TABLE entities (
entity_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
entity_type entity_type_enum NOT NULL, -- PERSON, ORGANIZATION, VESSEL_GROUP
entity_subtype entity_subtype_enum, -- INDIVIDUAL, COMPANY, GOVERNMENT, TRUST, etc.
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Person-specific information
CREATE TABLE entity_persons (
entity_id UUID PRIMARY KEY REFERENCES entities(entity_id),
given_names TEXT[],
family_names TEXT[],
full_name_primary TEXT NOT NULL,
name_type name_type_enum, -- LATIN, CHINESE, ARABIC, etc.
gender gender_enum,
date_of_birth DATE,
place_of_birth TEXT,
nationalities TEXT[], -- Multiple nationalities
primary_nationality TEXT,
-- Maritime specific
seafarer_id TEXT,
imo_crew_id TEXT,
maritime_licenses JSONB, -- Licenses, certifications
-- KYC fields
identification_documents JSONB, -- Passports, IDs
last_known_location TEXT,
risk_score NUMERIC(3,2),
pep_status BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Organization-specific information
CREATE TABLE entity_organizations (
entity_id UUID PRIMARY KEY REFERENCES entities(entity_id),
organization_name TEXT NOT NULL,
organization_type organization_type_enum, -- COMPANY, GOVERNMENT, NGO
registration_country TEXT,
incorporation_date DATE,
registration_numbers JSONB, -- Multiple registration IDs
tax_ids JSONB,
-- Business details
industry_codes TEXT[],
business_description TEXT,
annual_revenue NUMERIC,
employee_count INTEGER,
-- Compliance
lei_code TEXT, -- Legal Entity Identifier
duns_number TEXT,
swift_code TEXT,
-- Risk
risk_score NUMERIC(3,2),
sanctioned BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Alternative names/aliases
CREATE TABLE entity_aliases (
alias_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
entity_id UUID REFERENCES entities(entity_id),
alias_name TEXT NOT NULL,
alias_type alias_type_enum, -- AKA, DBA, MAIDEN, PREVIOUS
script_type name_type_enum, -- LATIN, CHINESE, ARABIC
valid_from DATE,
valid_to DATE,
is_primary BOOLEAN DEFAULT FALSE,
source_id UUID REFERENCES original_sources_vessels(source_id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Entity addresses
CREATE TABLE entity_addresses (
address_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
entity_id UUID REFERENCES entities(entity_id),
address_type address_type_enum, -- REGISTERED, OPERATIONAL, RESIDENTIAL
address_line1 TEXT,
address_line2 TEXT,
city TEXT,
state_province TEXT,
postal_code TEXT,
country_code TEXT,
formatted_address TEXT,
geo_location GEOGRAPHY(POINT),
valid_from DATE,
valid_to DATE,
is_current BOOLEAN DEFAULT TRUE,
source_id UUID REFERENCES original_sources_vessels(source_id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Association Tables

-- Entity-to-Entity relationships (ownership, employment, family)
CREATE TABLE entity_associations (
association_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
source_entity_id UUID REFERENCES entities(entity_id),
target_entity_id UUID REFERENCES entities(entity_id),
association_type association_type_enum NOT NULL, -- See below
association_subtype TEXT,
-- Ownership specific
ownership_percentage NUMERIC(5,2),
ownership_type ownership_type_enum, -- DIRECT, INDIRECT, BENEFICIAL
control_level control_level_enum, -- FULL, SIGNIFICANT, MINORITY
-- Temporal
valid_from DATE,
valid_to DATE,
is_current BOOLEAN DEFAULT TRUE,
-- Evidence
source_id UUID REFERENCES original_sources_vessels(source_id),
evidence_documents JSONB,
confidence_score NUMERIC(3,2),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(source_entity_id, target_entity_id, association_type, valid_from)
);

-- Vessel-Entity relationships (ownership, operation, crew)
CREATE TABLE vessel_entity_associations (
association_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
vessel_uuid UUID REFERENCES vessels(vessel_uuid),
entity_id UUID REFERENCES entities(entity_id),
association_type vessel_association_type_enum NOT NULL, -- See below
role_details JSONB, -- Role-specific information
-- Temporal
valid_from DATE,
valid_to DATE,
is_current BOOLEAN DEFAULT TRUE,
-- Evidence
source_id UUID REFERENCES original_sources_vessels(source_id),
report_id UUID REFERENCES intelligence_reports(report_id),
confidence_score NUMERIC(3,2),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Sanctions and restrictions
CREATE TABLE entity_sanctions (
sanction_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
entity_id UUID REFERENCES entities(entity_id),
sanction_type sanction_type_enum, -- SDN, WRO, EU, UN, etc.
sanction_program TEXT, -- "Global Magnitsky", "Forced Labor", etc.
listing_date DATE NOT NULL,
removal_date DATE,
is_current BOOLEAN DEFAULT TRUE,
reason TEXT,
details JSONB,
source_id UUID REFERENCES original_sources_vessels(source_id),
source_reference TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Enum Definitions

-- Entity types
CREATE TYPE entity_type_enum AS ENUM (
'PERSON',
'ORGANIZATION',
'VESSEL_GROUP',
'UNKNOWN'
);

CREATE TYPE entity_subtype_enum AS ENUM (
-- Person subtypes
'INDIVIDUAL',
'CREW_MEMBER',
'VESSEL_MASTER',
'COMPANY_OFFICER',
-- Organization subtypes
'COMPANY',
'PARTNERSHIP',
'TRUST',
'FOUNDATION',
'GOVERNMENT_ENTITY',
'STATE_OWNED_ENTERPRISE',
'COOPERATIVE',
'JOINT_VENTURE'
);

-- Association types between entities
CREATE TYPE association_type_enum AS ENUM (
-- Ownership
'OWNS',
'OWNED_BY',
'BENEFICIAL_OWNER',
'BENEFICIAL_OWNERSHIP',
-- Control
'CONTROLS',
'CONTROLLED_BY',
'MANAGES',
'MANAGED_BY',
-- Corporate
'SUBSIDIARY_OF',
'PARENT_OF',
'AFFILIATE_OF',
'DIRECTOR_OF',
'OFFICER_OF',
-- Employment
'EMPLOYS',
'EMPLOYED_BY',
-- Family (for UBO tracking)
'FAMILY_MEMBER',
'SPOUSE_OF',
'CHILD_OF',
'PARENT_OF'
);

-- Vessel association types
CREATE TYPE vessel_association_type_enum AS ENUM (
-- Ownership
'REGISTERED_OWNER',
'BENEFICIAL_OWNER',
'BAREBOAT_CHARTERER',
'TIME_CHARTERER',
-- Operations
'OPERATOR',
'MANAGER',
'ISM_MANAGER',
'COMMERCIAL_MANAGER',
'TECHNICAL_MANAGER',
-- Crew roles
'MASTER',
'CHIEF_MATE',
'SECOND_MATE',
'THIRD_MATE',
'CHIEF_ENGINEER',
'SECOND_ENGINEER',
'THIRD_ENGINEER',
'BOSUN',
'COOK',
'ORDINARY_SEAMAN',
'ABLE_SEAMAN',
'FISHING_MASTER',
'CREW_MEMBER',
-- Other
'INSURER',
'P_AND_I_CLUB',
'CLASS_SOCIETY',
'FLAG_STATE',
'PORT_STATE'
);

-- Ownership types for AML compliance
CREATE TYPE ownership_type_enum AS ENUM (
'DIRECT',
'INDIRECT',
'BENEFICIAL',
'NOMINEE',
'TRUSTEE',
'CUSTODIAN'
);

CREATE TYPE control_level_enum AS ENUM (
'FULL', -- >75%
'MAJORITY', -- >50%
'SIGNIFICANT', -- >25%
'MINORITY', -- <25%
'NONE'
);

Beneficial Ownership Calculation

-- View to calculate ultimate beneficial ownership
CREATE VIEW vessel_beneficial_ownership AS
WITH RECURSIVE ownership_chain AS (
-- Direct ownership
SELECT
vea.vessel_uuid,
vea.entity_id,
vea.association_type,
1 as depth,
ARRAY[vea.entity_id] as path,
1.0 as ownership_percentage
FROM vessel_entity_associations vea
WHERE vea.association_type IN ('REGISTERED_OWNER', 'BENEFICIAL_OWNER')
AND vea.is_current = TRUE

UNION ALL

-- Indirect ownership through corporate chains
SELECT
oc.vessel_uuid,
ea.source_entity_id as entity_id,
'INDIRECT_OWNER' as association_type,
oc.depth + 1,
oc.path || ea.source_entity_id,
oc.ownership_percentage * COALESCE(ea.ownership_percentage/100, 1.0)
FROM ownership_chain oc
JOIN entity_associations ea ON ea.target_entity_id = oc.entity_id
WHERE ea.association_type IN ('OWNS', 'BENEFICIAL_OWNER', 'CONTROLS')
AND ea.is_current = TRUE
AND NOT ea.source_entity_id = ANY(oc.path) -- Prevent cycles
AND oc.depth < 10 -- Max depth
)
SELECT DISTINCT ON (vessel_uuid, entity_id)
vessel_uuid,
entity_id,
MAX(ownership_percentage) as total_ownership_percentage,
MIN(depth) as ownership_depth,
CASE
WHEN MAX(ownership_percentage) > 0.25 THEN TRUE
ELSE FALSE
END as is_beneficial_owner
FROM ownership_chain
GROUP BY vessel_uuid, entity_id
ORDER BY vessel_uuid, entity_id, total_ownership_percentage DESC;

Intelligence Integration Functions

-- Process WRO company-level bans
CREATE FUNCTION process_wro_company_ban(
p_company_name TEXT,
p_association_types TEXT[],
p_wro_date DATE,
p_source_id UUID
) RETURNS void AS $$
DECLARE
v_entity_id UUID;
BEGIN
-- Create or find company entity
INSERT INTO entities (entity_type, entity_subtype)
VALUES ('ORGANIZATION', 'COMPANY')
ON CONFLICT DO NOTHING
RETURNING entity_id INTO v_entity_id;

-- Add company details
INSERT INTO entity_organizations (entity_id, organization_name)
VALUES (v_entity_id, p_company_name);

-- Add sanction
INSERT INTO entity_sanctions (
entity_id,
sanction_type,
sanction_program,
listing_date,
is_current,
source_id
) VALUES (
v_entity_id,
'WRO',
'Forced Labor',
p_wro_date,
TRUE,
p_source_id
);

-- Flag all vessels associated with this entity
UPDATE vessel_entity_associations
SET risk_flags = jsonb_set(
COALESCE(risk_flags, '{}'::jsonb),
'{wro_company_ban}',
'true'
)
WHERE entity_id = v_entity_id
AND association_type = ANY(ARRAY[
'REGISTERED_OWNER',
'BENEFICIAL_OWNER',
'OPERATOR'
]::vessel_association_type_enum[]);
END;
$$ LANGUAGE plpgsql;

-- Track crew movement between vessels
CREATE VIEW crew_vessel_history AS
SELECT
e.entity_id,
ep.full_name_primary as crew_name,
ep.primary_nationality,
vea.vessel_uuid,
v.vessel_name,
vea.association_type as role,
vea.valid_from,
vea.valid_to,
vea.is_current,
-- Check for suspicious patterns
CASE
WHEN COUNT(*) OVER (
PARTITION BY e.entity_id
ORDER BY vea.valid_from
RANGE BETWEEN INTERVAL '1 year' PRECEDING AND CURRENT ROW
) > 5 THEN TRUE
ELSE FALSE
END as high_vessel_turnover
FROM entities e
JOIN entity_persons ep ON e.entity_id = ep.entity_id
JOIN vessel_entity_associations vea ON e.entity_id = vea.entity_id
JOIN vessels v ON vea.vessel_uuid = v.vessel_uuid
WHERE vea.association_type IN (
'MASTER', 'CHIEF_MATE', 'FISHING_MASTER', 'CHIEF_ENGINEER'
)
ORDER BY e.entity_id, vea.valid_from DESC;

Consequences

Positive

  • Complete KYC/AML compliance - Follows financial industry standards for entity tracking
  • Beneficial ownership transparency - Can trace ownership through complex corporate structures
  • Crew accountability - Track crew movement and identify patterns
  • Company-level sanctions - Properly handle fleet-wide bans like Dalian Ocean Fishing
  • Network analysis - Identify connected entities and hidden relationships
  • Risk scoring - Aggregate risk across ownership chains
  • Temporal tracking - Full history of relationships and changes

Negative

  • Complexity - Requires careful data modeling and maintenance
  • Data quality challenges - Name matching, entity resolution
  • Performance - Recursive queries for ownership chains
  • Privacy concerns - Storing personal information requires compliance

Neutral

  • Aligns with international AML/CTF standards
  • Compatible with financial institution requirements
  • Supports future blockchain/DLT integration
  • Enables advanced analytics and ML models

Implementation Notes

  1. Phased Rollout:

    • Phase 1: Core entity tables and basic associations
    • Phase 2: Beneficial ownership calculation
    • Phase 3: Risk scoring and analytics
    • Phase 4: Integration with existing vessel data
  2. Data Sources:

    • WRO company bans (immediate)
    • SDN entity relationships
    • RFMO crew lists
    • Corporate registries
    • Vessel registries
  3. Privacy & Security:

    • Implement role-based access control
    • Audit all entity data access
    • Comply with GDPR/privacy laws
    • Encrypt sensitive personal data
  4. Entity Resolution:

    • Use fuzzy matching for names
    • Cross-reference multiple identifiers
    • Machine learning for duplicate detection
    • Manual review for high-value entities