<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Tiani pekins Ebika</title>
    <description>The latest articles on DEV Community by Tiani pekins Ebika (@tianipekinsebika).</description>
    <link>https://dev.to/tianipekinsebika</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3909522%2Fad200e25-3855-4932-b852-06d60a7c842a.jpeg</url>
      <title>DEV Community: Tiani pekins Ebika</title>
      <link>https://dev.to/tianipekinsebika</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tianipekinsebika"/>
    <language>en</language>
    <item>
      <title>Architecting Digital Trust: A Relational Deep Dive into the LocalHands Prisma Schema</title>
      <dc:creator>Tiani pekins Ebika</dc:creator>
      <pubDate>Sat, 02 May 2026 21:10:12 +0000</pubDate>
      <link>https://dev.to/tianipekinsebika/architecting-digital-trust-a-relational-deep-dive-into-the-localhands-prisma-schema-12dk</link>
      <guid>https://dev.to/tianipekinsebika/architecting-digital-trust-a-relational-deep-dive-into-the-localhands-prisma-schema-12dk</guid>
      <description>&lt;p&gt;&lt;strong&gt;Subtitle: How to model a secure, escrow-based marketplace for emerging economies using Prisma and PostgreSQL.&lt;br&gt;
Schema.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In my previous article on Medium, I discussed the sociotechnical challenge of Information Poverty in the African gig economy. But as engineers, we know that solving social problems requires more than vision it requires a robust, type-safe, and scalable data architecture.&lt;br&gt;
For LocalHands, I chose Prisma ORM with PostgreSQL. The goal was to build a "Technical Source of Truth" that could handle the complexity of service listings, competitive bidding (proposals), and secure escrow payments.&lt;br&gt;
Below, I break down the core relational logic of the LocalHands schema&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The Core Actor Model: User vs. Profile&lt;/strong&gt;&lt;br&gt;
In a marketplace, users often play multiple roles. However, security is paramount. I separated the User (authentication and roles) from the Profile (sensitive KYC data)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;id&lt;/span&gt;               &lt;span class="nx"&gt;Int&lt;/span&gt;              &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;id&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;autoincrement&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;             &lt;span class="nx"&gt;UserRole&lt;/span&gt;         &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CLIENT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;phoneNumber&lt;/span&gt;      &lt;span class="nb"&gt;String&lt;/span&gt;           &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;unique&lt;/span&gt;
  &lt;span class="nx"&gt;email&lt;/span&gt;            &lt;span class="nb"&gt;String&lt;/span&gt;           &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;unique&lt;/span&gt;
  &lt;span class="nx"&gt;passwordHash&lt;/span&gt;     &lt;span class="nb"&gt;String&lt;/span&gt;
  &lt;span class="nx"&gt;profile&lt;/span&gt;          &lt;span class="nx"&gt;Profile&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
  &lt;span class="c1"&gt;// ... relations to orders, contracts, and services&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="nx"&gt;Profile&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;id&lt;/span&gt;                 &lt;span class="nx"&gt;Int&lt;/span&gt;              &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;id&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;autoincrement&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="nx"&gt;userId&lt;/span&gt;             &lt;span class="nx"&gt;Int&lt;/span&gt;              &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;unique&lt;/span&gt;
  &lt;span class="nx"&gt;user&lt;/span&gt;               &lt;span class="nx"&gt;User&lt;/span&gt;             &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;relation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;references&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="nx"&gt;verificationStatus&lt;/span&gt; &lt;span class="nx"&gt;VerificationStatus&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PENDING&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;nationalIdUrl&lt;/span&gt;      &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;          &lt;span class="c1"&gt;// URL to encrypted storage&lt;/span&gt;
  &lt;span class="nx"&gt;mobileMoneyNumber&lt;/span&gt;  &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Engineering Decision:&lt;/strong&gt; By using a 1:1 relation for the Profile, we keep the User model lean for frequent authentication checks while isolating heavier metadata and verification documents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Modeling the Bidding Lifecycle (Service -&amp;gt; Order -&amp;gt; Proposal)&lt;/strong&gt;&lt;br&gt;
Unlike standard e-commerce, a service marketplace is dynamic. A client doesn't just "buy"; they post a &lt;strong&gt;ServiceOrder&lt;/strong&gt;, and providers reply with &lt;strong&gt;Proposals&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="nx"&gt;ServiceOrder&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;id&lt;/span&gt;            &lt;span class="nx"&gt;Int&lt;/span&gt;              &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;id&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;autoincrement&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="nx"&gt;serviceId&lt;/span&gt;     &lt;span class="nx"&gt;Int&lt;/span&gt;
  &lt;span class="nx"&gt;clientId&lt;/span&gt;      &lt;span class="nx"&gt;Int&lt;/span&gt;
  &lt;span class="nx"&gt;budget&lt;/span&gt;        &lt;span class="nx"&gt;Float&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
  &lt;span class="nx"&gt;status&lt;/span&gt;        &lt;span class="nx"&gt;ServiceOrderStatus&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PENDING&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;contract&lt;/span&gt;      &lt;span class="nx"&gt;Contract&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;          &lt;span class="c1"&gt;// Only exists once a proposal is accepted&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="nx"&gt;Proposal&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;id&lt;/span&gt;           &lt;span class="nx"&gt;Int&lt;/span&gt;          &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;id&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;autoincrement&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="nx"&gt;providerId&lt;/span&gt;   &lt;span class="nx"&gt;Int&lt;/span&gt;
  &lt;span class="nx"&gt;serviceId&lt;/span&gt;    &lt;span class="nx"&gt;Int&lt;/span&gt;
  &lt;span class="nx"&gt;bidAmount&lt;/span&gt;    &lt;span class="nx"&gt;Float&lt;/span&gt;
  &lt;span class="nx"&gt;status&lt;/span&gt;       &lt;span class="nx"&gt;ProposalStatus&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PENDING&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;contractId&lt;/span&gt;   &lt;span class="nx"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Relational Integrity:&lt;/strong&gt; Notice the optional contractId in the Proposal. This allows multiple providers to bid on one job, but ensures that only the &lt;strong&gt;accepted&lt;/strong&gt; proposal transitions into a formal, binding Contract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The Trust Engine: Contract and Escrow&lt;/strong&gt;&lt;br&gt;
This is where the code solves the &lt;strong&gt;Trust Gap&lt;/strong&gt;. The Contract model acts as the central node for the entire transaction lifecycle.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="nx"&gt;Contract&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;id&lt;/span&gt;            &lt;span class="nx"&gt;Int&lt;/span&gt;              &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;id&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;autoincrement&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="nx"&gt;serviceOrderId&lt;/span&gt; &lt;span class="nx"&gt;Int&lt;/span&gt;              &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;unique&lt;/span&gt;
  &lt;span class="nx"&gt;escrowAmount&lt;/span&gt;  &lt;span class="nx"&gt;Float&lt;/span&gt;
  &lt;span class="nx"&gt;status&lt;/span&gt;        &lt;span class="nx"&gt;ContractStatus&lt;/span&gt;   &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ACTIVE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;payments&lt;/span&gt;      &lt;span class="nx"&gt;Payment&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="nx"&gt;reviews&lt;/span&gt;       &lt;span class="nx"&gt;Review&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By enforcing a &lt;a class="mentioned-user" href="https://dev.to/unique"&gt;@unique&lt;/a&gt; constraint on the serviceOrderId, we prevent the "Double-Payment" bug. The contract is the only entity authorized to trigger a Payment release.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Localized FinTech Integration&lt;/strong&gt;&lt;br&gt;
To meet the reality of the Cameroonian market, the schema explicitly supports MTN Mobile Money and localized currency settings.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="nx"&gt;Payment&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;id&lt;/span&gt;            &lt;span class="nx"&gt;Int&lt;/span&gt;             &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;id&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;autoincrement&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="nx"&gt;contractId&lt;/span&gt;    &lt;span class="nx"&gt;Int&lt;/span&gt;
  &lt;span class="nx"&gt;amount&lt;/span&gt;        &lt;span class="nx"&gt;Float&lt;/span&gt;
  &lt;span class="nx"&gt;paymentMethod&lt;/span&gt; &lt;span class="nx"&gt;PaymentMethod&lt;/span&gt;   &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MTN_MOBILE_MONEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;status&lt;/span&gt;        &lt;span class="nx"&gt;PaymentStatus&lt;/span&gt;   &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PENDING&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="nx"&gt;SystemSettings&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;currency&lt;/span&gt;           &lt;span class="nb"&gt;String&lt;/span&gt;   &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;XAF&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;currency_symbol&lt;/span&gt;    &lt;span class="nb"&gt;String&lt;/span&gt;   &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FCFA&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;payment_gateway&lt;/span&gt;     &lt;span class="nb"&gt;String&lt;/span&gt;   &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fapshi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; Hardcoding these enums and settings at the database level ensures that the business logic remains consistent and compliant with regional financial regulations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;br&gt;
This schema is designed to do more than just store data; it is designed to &lt;strong&gt;enforce trust&lt;/strong&gt;. By leveraging Prisma's powerful relational features, I have built a foundation where Information Poverty is replaced by a transparent, verifiable history of service.&lt;br&gt;
&lt;strong&gt;What’s Next?&lt;/strong&gt;&lt;br&gt;
Currently, I am stabilizing the &lt;strong&gt;Escrow Algorithm&lt;/strong&gt; and the &lt;strong&gt;Fapshi payment integration&lt;/strong&gt; logic. In my next post, I will dive deep into the system UI then later "Fund-Lock-Release" cycle and real-time payment webhooks.&lt;/p&gt;

</description>
      <category>prisma</category>
      <category>postgressql</category>
      <category>softwareengineering</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
