Skip to content

YeboID Consolidation Strategy — Merging Users Across Products

This document details the strategy for consolidating users from all Yebo products into the central YeboID system using phone number as the unique identifier.


The Problem

Users are currently scattered across multiple products with separate accounts:

ProductDatabaseAuth MethodUser Count
EnezaCloud SQLphone + OTP12,000+
YeboShops (Vavu)Neonphone + OTP~500
YeboJobsNeonphone + password~200
YeboLinkNeonphone + API key~100
BamzuNeonphone + OTP~50
YeboLearnNeonphone + ?~50

Same person, different accounts everywhere.


The Solution

YeboID as Central Authority

┌──────────────────────────────────────────────────────────────┐
│                    YeboID (Central)                           │
│                                                               │
│  • Single user table (phone = unique identifier)             │
│  • Issues JWT tokens (access + refresh)                      │
│  • @handle.yebo identifiers                                  │
│  • KYC status (via YeboVerify)                               │
│                                                               │
│  Database: Neon PostgreSQL (NEW)                             │
└──────────────────────────────────────────────────────────────┘

                             │ JWT tokens (validated locally)

    ┌───────────┬───────────┬───────────┬───────────┬──────────┐
    │           │           │           │           │          │
    ▼           ▼           ▼           ▼           ▼          ▼
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│ Eneza  │ │YeboShop│ │YeboJobs│ │YeboLink│ │ Bamzu  │ │YeboLern│
└────────┘ └────────┘ └────────┘ └────────┘ └────────┘ └────────┘

Each product:
- Validates tokens LOCALLY (shared JWT secret)
- Stores yeboid_user_id as foreign key
- Keeps product-specific data

Migration Phases

Phase 1: Extract All Phone Numbers

Run queries across all product databases to find unique phones:

sql
-- Vavu (YeboShops)
SELECT DISTINCT phone FROM users WHERE phone IS NOT NULL;

-- YeboJobs
SELECT DISTINCT phone FROM users WHERE phone IS NOT NULL;

-- Eneza
SELECT DISTINCT phone FROM users WHERE phone IS NOT NULL;

-- YeboLink
SELECT DISTINCT phone FROM api_users WHERE phone IS NOT NULL;

-- Bamzu
SELECT DISTINCT phone FROM users WHERE phone IS NOT NULL;

Output: List of all unique phone numbers across ecosystem.


Phase 2: Create YeboID Records

Import all unique phones into YeboID:

sql
-- In YeboID database
INSERT INTO users (id, phone, created_at)
SELECT 
  gen_random_uuid(),
  phone,
  NOW()
FROM (
  -- Union of all phones
  SELECT phone FROM vavu.users
  UNION
  SELECT phone FROM yebojobs.users  
  UNION
  SELECT phone FROM eneza.users
  UNION
  SELECT phone FROM yebolink.api_users
  UNION
  SELECT phone FROM bamzu.users
) all_phones
ON CONFLICT (phone) DO NOTHING;

Result: Every unique phone now has a YeboID record.


Phase 3: Add yeboid_user_id to Each Product

Update each product's schema:

sql
-- In each product database
ALTER TABLE users ADD COLUMN yeboid_user_id UUID;

-- Create index
CREATE UNIQUE INDEX idx_users_yeboid ON users(yeboid_user_id);

Prisma schema update:

prisma
model User {
  id           String   @id @default(uuid())
  yeboidUserId String?  @unique @map("yeboid_user_id")
  
  // Existing product-specific fields...
  phone        String   @unique  // Keep for now during migration
}

Match product users to YeboID records:

sql
-- In each product database
UPDATE users u
SET yeboid_user_id = (
  SELECT y.id 
  FROM yeboid.users y 
  WHERE y.phone = u.phone
)
WHERE u.phone IS NOT NULL;

Verify linkage:

sql
SELECT 
  COUNT(*) as total,
  COUNT(yeboid_user_id) as linked,
  COUNT(*) - COUNT(yeboid_user_id) as unlinked
FROM users;

Phase 5: Update Product Auth Middleware

Before (product's own auth):

javascript
// YeboShops - old auth
const user = await prisma.user.findUnique({ 
  where: { phone } 
});
const token = jwt.sign({ userId: user.id }, VAVU_SECRET);

After (YeboID auth):

javascript
// YeboShops - new auth
const { validateToken } = require('@yeboid/node');

app.use('/api', validateToken, async (req, res, next) => {
  // req.yeboUserId from YeboID token
  const user = await prisma.user.findUnique({ 
    where: { yeboidUserId: req.yeboUserId } 
  });
  
  if (!user) {
    // First time - auto-create local profile
    user = await prisma.user.create({
      data: { yeboidUserId: req.yeboUserId }
    });
  }
  
  req.user = user;
  next();
});

Phase 6: User Communication

Notify users about the transition:

Subject: Your Yebo accounts are now unified! 🎉

Hi [Name],

Great news! We've unified all your Yebo accounts into one YeboID.

Your YeboID: @[handle]
Phone: +268 78 XXX XXX

You now have:
✓ One login for all Yebo products
✓ Your data from YeboShops, Eneza, etc. - all connected
✓ A unique @handle to use everywhere

Next time you sign in, use your phone + PIN.

Questions? Reply to this message.

— The Yebo Team

Phase 7: Deprecate Old Auth

After migration is stable:

  1. Remove old login endpoints
  2. Remove password/PIN columns from product DBs
  3. Remove phone column (use yeboidUserId only)
  4. Clean up old auth middleware

Database Schema Changes

Before (Product's own users)

prisma
// YeboShops
model User {
  id        String   @id @default(uuid())
  phone     String   @unique
  pinHash   String?  // Product manages auth
  name      String?
  
  // Product data
  shops     Shop[]
  orders    Order[]
}

After (YeboID linked)

prisma
// YeboShops
model User {
  id           String   @id @default(uuid())
  yeboidUserId String   @unique @map("yeboid_user_id")
  
  // Product data only - no auth fields!
  name         String?  // Can sync from YeboID
  rating       Float    @default(0)
  
  shops        Shop[]
  orders       Order[]
}

Data Ownership After Migration

YeboID OwnsProducts Own
Phone numberProduct-specific data
@handleOrders, jobs, listings
Name, avatarPreferences, history
KYC statusLocal user settings
Session managementProduct relationships

Handling Edge Cases

Same Phone, Multiple Product Accounts

Scenario: User has different profiles on YeboShops vs YeboJobs

Solution: All product accounts link to same YeboID

YeboID (phone: +26878422613)

    ├── YeboShops User (seller profile)
    │   └── shops, products, orders

    └── YeboJobs User (job seeker profile)
        └── resume, applications

Phone Number Changes

Scenario: User changes phone number

Solution: YeboID phone update propagates automatically (products use yeboidUserId, not phone)

Duplicate Detection

Scenario: Same person created accounts with different phones (typo, changed number)

Solution: Manual merge via admin tool (future)

javascript
// Future: Admin merge tool
await yeboid.mergeUsers(primaryId, duplicateId);
// - Transfers all product links to primary
// - Deletes duplicate YeboID record

Token Validation Strategy

All products validate tokens locally using shared JWT secret:

javascript
const { verify } = require('jsonwebtoken');
const JWT_SECRET = process.env.YEBOID_JWT_SECRET;

function validateYeboID(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  
  try {
    const payload = verify(token, JWT_SECRET);
    req.yeboUserId = payload.userId;
    next();
  } catch {
    res.status(401).json({ error: 'Invalid token' });
  }
}

Benefits:

  • No API call to YeboID per request
  • Works even if YeboID API is down
  • Scales infinitely

Migration Timeline

WeekTask
1Create YeboID database, deploy API
2Extract all phones, create YeboID records
3Add yeboid_user_id to 2 products
4Update auth middleware for 2 products
5Add yeboid_user_id to remaining products
6Update auth middleware for remaining products
7User communication & testing
8Deprecate old auth flows

Monitoring & Rollback

Success Metrics

sql
-- Track migration progress
SELECT 
  'vavu' as product,
  COUNT(*) as total_users,
  COUNT(yeboid_user_id) as linked,
  ROUND(COUNT(yeboid_user_id)::numeric / COUNT(*) * 100, 1) as percent
FROM vavu.users
UNION ALL
SELECT 'yebojobs', COUNT(*), COUNT(yeboid_user_id), ...
FROM yebojobs.users;

Rollback Plan

If issues arise:

  1. Immediate: Product can fall back to old auth
  2. Keep old columns until migration proven stable
  3. Dual-write tokens during transition (both old and new)

SQL Scripts

Full Migration Script (Per Product)

sql
-- 1. Add column
ALTER TABLE users ADD COLUMN IF NOT EXISTS yeboid_user_id UUID;

-- 2. Link by phone
UPDATE users u
SET yeboid_user_id = y.id
FROM yeboid.users y
WHERE u.phone = y.phone
AND u.yeboid_user_id IS NULL;

-- 3. Create index
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_yeboid 
ON users(yeboid_user_id) 
WHERE yeboid_user_id IS NOT NULL;

-- 4. Verify
SELECT 
  COUNT(*) as total,
  COUNT(yeboid_user_id) as linked,
  COUNT(*) FILTER (WHERE yeboid_user_id IS NULL) as unlinked
FROM users;

Current Status

Productyeboid_user_id AddedUsers LinkedAuth Migrated
Eneza⏳ Pending
YeboShops⏳ Pending
YeboJobs⏳ Pending
YeboLink⏳ Pending
Bamzu⏳ Pending
YeboLearn⏳ Pending

This is the foundation. Get this right, everything else follows.

One chat. Everything done.