05 / Product — Web PlatformCase Study

Synccos | Connexabi

A contact intelligence and relationship graph platform — redesigned from the ground up after a core ownership model flaw caused user edits to silently mutate shared public entities, corrupting contact data across the entire network.

Next.jsTypeScriptNestJSPrismaPostgreSQL

Greenfield → Redesign · Pre-launch · Full-stack ownership

At a Glance

Schema layers

3

public canonical, user-scoped, external contact

Field types normalized

3

emails, phone numbers, addresses extracted to own tables

Mutation leakage

Eliminated

user edits no longer touch public entities

Legacy records

Migrated

full data migration with zero disruption to existing users

* This was a pre-launch system. Quantitative scale metrics were not yet available; figures reflect architectural scope rather than traffic.

The Problem

Connexabi was designed as a contact intelligence system — not a social feed, but a structured graph where each person could exist as a public canonical entity and as a private user-specific context simultaneously. The idea was sound. The data model wasn't.

When a user added a public contact to their personal network, the system was supposed to create an isolated copy for that user to work with. Instead, due to incorrect reference handling, any edits that user made mutated the original shared public record. One user's changes silently overwrote another's view. Relationship data leaked globally. Contact records drifted into inconsistency across users. The core networking model had no integrity.

Underneath the bug was a deeper problem: the system had no defined ownership model. There was no clear answer to who owned what, which layer of data was mutable by whom, or how public and private contexts should relate to each other. That ambiguity had to be resolved before any fix would hold.

Constraints

No clear ownership model existed — public vs. user-scoped data boundaries had never been formally defined
Product intent was ambiguous — key behaviors (should user edits propagate globally?) had no agreed answer
Existing contact records had to be migrated to the new schema without disrupting active users
The system needed to support both registered users and external (non-registered) contacts in the same graph
Community contributions to public data required moderation — open mutation was not acceptable
Relationship data had to be privately scoped per user, not globally visible across the network

Architecture Overview

Identity layer

Public canonical contact (immutable)

User layer

User-scoped contact instance (private copy)

External layer

Non-registered entity with contribution pipeline

Contributions

Field-level suggestions + voting system

Moderation

Self-verification (registered) + super-admin (external)

Relationships

User-scoped graph — private, non-global

Key Engineering Decisions

Resolving product ambiguity before touching the code

The mutation bug was a symptom, not the root cause. The root cause was that the team had never agreed on what the system was supposed to do: should a user's edits stay private? Should they improve the shared record? Should registered users control their own public profile? I worked directly with the CEO to get these questions answered explicitly. The outcome: public contact data is immutable by default; users own their private copy; registered contacts can approve contributions to their canonical profile; external contacts go through a moderated voting pipeline. That decision shaped every technical choice that followed.

Product behavior fully specified before implementation; no design reversals during build

Introducing user-scoped contact isolation

The fix to mutation leakage wasn't a bug patch — it required a model change. When a user adds a public contact to their network, the system now creates a dedicated user-scoped instance. All edits apply to that instance only. The public canonical record is read-only from the user's perspective. This required new schema entities, updated write paths on the backend, and UI surfaces on the frontend that clearly distinguished between 'your view' and 'the shared record' — Redux Toolkit slices were split to reflect this separation, and RTK Query endpoints were scoped per ownership context.

Cross-user mutation eliminated; each user's network is fully isolated from others

Normalizing contact fields into relational tables

The original schema embedded emails, phone numbers, and addresses directly inside contact documents — a flat structure that worked for simple lookups but couldn't support multi-valued attributes, field-level contribution tracking, or clean migration. I extracted each field type into its own normalized table in PostgreSQL via Prisma, enabling structured storage, independent updates, and the contribution voting system to operate at the field level rather than the document level. A full data migration moved all legacy embedded records into the new schema.

Field-level contributions, multi-value attributes, and clean migration all became straightforward

Building a two-track moderation pipeline for public data quality

Allowing any user to freely update public contact data would recreate the mutation problem. But blocking all community contributions would make public profiles stale. The solution was two distinct flows: registered contacts review and approve contributions to their own canonical profile (self-verification), while external non-registered contacts go through a field-level suggestion system where contributions are aggregated by votes and reviewed by a super-admin before promotion. This kept public data quality improving without compromising integrity.

Public contact data can improve over time through community input without open mutation risk

Outcomes

  • Defined and implemented a three-layer ownership model (public canonical, user-scoped instance, external contact) that eliminated mutation leakage
  • Rebuilt the core data model in PostgreSQL + Prisma with normalized contact field tables and user-scoped relationship storage
  • Performed a full schema migration of legacy contact records into the new relational structure with no disruption to existing users
  • Split frontend state into ownership-aware slices — Redux Toolkit for client state, RTK Query endpoints scoped per contact layer
  • Built a two-track moderation pipeline: self-verification for registered contacts, field-level voting + super-admin approval for external ones
  • Delivered relationship graph features with strict user-scoped privacy — no relationship data visible across user boundaries
  • Resolved product-level ambiguity through direct CEO engagement, producing a formally specified ownership and permission model that guided the entire build

Takeaway

“In systems built around shared entities, ambiguous ownership is a ticking clock. The mutation bug wasn't a coding error — it was the inevitable consequence of never deciding who owns what. The most important work on this project happened before a line of code was written: getting the ownership model agreed, specified, and documented. Everything else followed from that.”