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.