Network CRM β design + build record (A.5) β
Status: SHIPPED (both surfaces). The 7
network_*tables are live on the shared Supabase project (network_persons,network_organizations,network_roles,network_relationships,network_interactions,network_deals,network_deal_contactsβ verified 2026-07-05), and the relationship-CRM ("who-knows-who" + deal participants) ships on React web (src/components/network/) and Flutter (/network/*,features/network/).kV1HiddenRoutesis empty, so the routes are un-gated. This file is retained as the schema/design record; the sections below describe the applied design, not an unbuilt proposal. Followscross-platform-design-parity.
Anchor on the identity spine β don't duplicate persons β
unified_persons (id, canonical_name, civil_id, email, phone, person_type[]) is the deduplicated identity row that clients.unified_person_id already points at. network_persons references unified_persons β a network profile is an existing person + relationship metadata, so a banker who is also a client is ONE identity, not two. This is what keeps the feature in parity instead of becoming a mobile/web-only island.
Proposed schema (7 tables) β
All: id uuid pk default gen_random_uuid(), system text (brand scope β 'aldilaijan'/'khobara'/null=both), created_by uuid default auth.uid(), created_at/updated_at timestamptz. RLS pattern below.
-- 1. Organizations (banks, developers, gov bodies, law firms, vendorsβ¦)
create table network_organizations (
id uuid primary key default gen_random_uuid(),
name text not null, name_ar text,
org_type text not null, -- bank|developer|government|law_firm|vendor|brokerage|other
website text, notes text,
system text, created_by uuid default auth.uid(),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
-- 2. Network persons = a unified_persons identity + relationship metadata
create table network_persons (
id uuid primary key default gen_random_uuid(),
unified_person_id uuid not null references unified_persons(id) on delete cascade,
display_name text, -- optional override of canonical_name
title text, seniority text, -- e.g. "CFO", "owner"
primary_org_id uuid references network_organizations(id) on delete set null,
tags text[] not null default '{}',
importance text not null default 'normal', -- vip|high|normal
source text, notes text,
owner_id uuid default auth.uid(), -- staff who owns the relationship
system text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (unified_person_id) -- one network profile per identity
);
-- 3. Roles: a person's role AT an org, with tenure (personβorg junction)
create table network_roles (
id uuid primary key default gen_random_uuid(),
person_id uuid not null references network_persons(id) on delete cascade,
organization_id uuid not null references network_organizations(id) on delete cascade,
role_title text not null,
is_current boolean not null default true,
started_on date, ended_on date,
created_at timestamptz not null default now()
);
-- 4. Relationships: directed edges of the people graph
create table network_relationships (
id uuid primary key default gen_random_uuid(),
from_person_id uuid not null references network_persons(id) on delete cascade,
to_person_id uuid not null references network_persons(id) on delete cascade,
relationship_type text not null, -- knows|reports_to|family|referred_by|colleague|partner
strength text not null default 'medium', -- weak|medium|strong
notes text, created_by uuid default auth.uid(),
created_at timestamptz not null default now(),
check (from_person_id <> to_person_id)
);
-- 5. Interactions: the touchpoint timeline
create table network_interactions (
id uuid primary key default gen_random_uuid(),
person_id uuid references network_persons(id) on delete cascade,
organization_id uuid references network_organizations(id) on delete cascade,
interaction_type text not null, -- call|meeting|message|email|event
occurred_at timestamptz not null default now(),
summary text, sentiment text, -- positive|neutral|negative
follow_up_at timestamptz,
logged_by uuid default auth.uid(),
created_at timestamptz not null default now(),
check (person_id is not null or organization_id is not null)
);
-- 6. Network deals: referral/partnership/introduction opportunities
-- (distinct from brokerage `deals` = property transactions; optional link)
create table network_deals (
id uuid primary key default gen_random_uuid(),
title text not null,
deal_type text not null, -- referral|partnership|introduction|investment
stage text not null default 'open',
value_kwd numeric, expected_close date,
property_id uuid references properties_base(id) on delete set null, -- optional
owner_id uuid default auth.uid(), system text, status text not null default 'active',
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
-- 7. Deal contacts: which network persons play which role in a network deal
create table network_deal_contacts (
id uuid primary key default gen_random_uuid(),
network_deal_id uuid not null references network_deals(id) on delete cascade,
person_id uuid not null references network_persons(id) on delete cascade,
role text not null, -- introducer|principal|advisor|banker|lawyer|agent
notes text,
unique (network_deal_id, person_id, role)
);Indexes: every FK; network_persons(owner_id), (primary_org_id); network_roles(person_id) where is_current; network_relationships(from_person_id), (to_person_id); network_interactions(person_id, occurred_at desc); network_deal_contacts(network_deal_id), (person_id).
RLS (mirrors the house pattern: has_role / app_role, anon-blocked) β
A firm-wide relationship CRM is most useful shared among staff (the point is cross-firm visibility of who-knows-who). Proposed per table:
alter table network_persons enable row level security;
-- Staff read all; owner or admin write. (Repeat shape for each table.)
create policy "staff read" on network_persons for select
using (has_any_role(auth.uid(), array['admin','agent','secretary_aldilaijan',
'secretary_khobara','accountant']::app_role[])
and coalesce((auth.jwt()->>'is_anonymous')::boolean,false)=false);
create policy "owner or admin write" on network_persons for all
using (owner_id = auth.uid() or has_role(auth.uid(),'admin'::app_role))
with check (owner_id = auth.uid() or has_role(auth.uid(),'admin'::app_role));(Junction/edge tables β roles, relationships, interactions, deal_contacts β gate writes on the parent's owner or admin; reads on staff.)
Mobile feature architecture (house conventions + parity) β
lib/data/models/ network_person.dart, network_organization.dart,
network_role.dart, network_relationship.dart,
network_interaction.dart, network_deal.dart,
network_deal_contact.dart (hand-written fromJson)
lib/data/repositories/ network_repository.dart (or split persons/orgs/deals)
lib/features/network/screens/
network_dashboard_screen.dart (overview: counts, recent interactions, follow-ups)
persons_list_screen.dart (search/filter, importance, tags)
person_detail_screen.dart (profile + roles + relationships + interaction timeline)
organizations_list_screen.dart / organization_detail_screen.dart
network_deals_screen.dart (referral/partnership pipeline)
relationship_graph_screen.dart (optional: who-knows-who viz)
lib/features/network/widgets/ interaction_form_sheet, relationship_picker, β¦- Riverpod
FutureProvider.familyper list/detail (house pattern). - Routes under
/network/*inrouter.dart, persona-gated (admin/agent), and added tokV1HiddenRoutesuntil the screens are ready (build behind the gate). - Shared i18n keys (ar/en) + design tokens; no surface-specific copy/hex.
- Contract tests: register the new models in
test/contract/so the PostgREST shape stays pinned. - Golden tests per screen (RTL + brand-switch + dark).
Build sequence β
- Confirm the open decisions below.
- Apply the schema migration (additive; RLS from day one).
- Generate contract fixtures + write the 7 models (+ contract tests).
- Repositories (+ pure-mapping unit tests).
- Screens behind
kV1HiddenRoutes; wire providers. - Build the web surface on the same tables (parity); update
docs/CROSS_PLATFORM_PARITY.md+ adocs/screens/entry. - Un-gate once both surfaces reach parity + QA.
Open product decisions (need your call before applying) β
- Network deals vs brokerage
dealsβ keep separate (proposed: relationship/ referral opportunities, optionally linked to a property) or extenddeals? - RLS sharing β firm-wide staff read (proposed) or strictly owner-scoped?
- Identity β require every
network_personsrow to map to aunified_personsidentity (proposed) or allow standalone external contacts? - Orgs β net-new
network_organizations(proposed) or reuse any existing org/vendor concept? (None found βunified_personsis people-only.) person_typeβ add a'network'member tounified_persons.person_type[]when a person joins the network?
Confirm these and I'll apply the migration + build the mobile feature (and the web surface, if you point me at that repo).
