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:
| Product | Database | Auth Method | User Count |
|---|---|---|---|
| Eneza | Cloud SQL | phone + OTP | 12,000+ |
| YeboShops (Vavu) | Neon | phone + OTP | ~500 |
| YeboJobs | Neon | phone + password | ~200 |
| YeboLink | Neon | phone + API key | ~100 |
| Bamzu | Neon | phone + OTP | ~50 |
| YeboLearn | Neon | phone + ? | ~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 dataMigration Phases
Phase 1: Extract All Phone Numbers
Run queries across all product databases to find unique phones:
-- 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:
-- 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:
-- 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:
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
}Phase 4: Link Existing Users by Phone
Match product users to YeboID records:
-- 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:
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):
// YeboShops - old auth
const user = await prisma.user.findUnique({
where: { phone }
});
const token = jwt.sign({ userId: user.id }, VAVU_SECRET);After (YeboID auth):
// 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 TeamPhase 7: Deprecate Old Auth
After migration is stable:
- Remove old login endpoints
- Remove password/PIN columns from product DBs
- Remove
phonecolumn (use yeboidUserId only) - Clean up old auth middleware
Database Schema Changes
Before (Product's own users)
// 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)
// 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 Owns | Products Own |
|---|---|
| Phone number | Product-specific data |
| @handle | Orders, jobs, listings |
| Name, avatar | Preferences, history |
| KYC status | Local user settings |
| Session management | Product 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, applicationsPhone 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)
// Future: Admin merge tool
await yeboid.mergeUsers(primaryId, duplicateId);
// - Transfers all product links to primary
// - Deletes duplicate YeboID recordToken Validation Strategy
All products validate tokens locally using shared JWT secret:
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
| Week | Task |
|---|---|
| 1 | Create YeboID database, deploy API |
| 2 | Extract all phones, create YeboID records |
| 3 | Add yeboid_user_id to 2 products |
| 4 | Update auth middleware for 2 products |
| 5 | Add yeboid_user_id to remaining products |
| 6 | Update auth middleware for remaining products |
| 7 | User communication & testing |
| 8 | Deprecate old auth flows |
Monitoring & Rollback
Success Metrics
-- 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:
- Immediate: Product can fall back to old auth
- Keep old columns until migration proven stable
- Dual-write tokens during transition (both old and new)
SQL Scripts
Full Migration Script (Per Product)
-- 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
| Product | yeboid_user_id Added | Users Linked | Auth Migrated |
|---|---|---|---|
| Eneza | ⏳ Pending | — | — |
| YeboShops | ⏳ Pending | — | — |
| YeboJobs | ⏳ Pending | — | — |
| YeboLink | ⏳ Pending | — | — |
| Bamzu | ⏳ Pending | — | — |
| YeboLearn | ⏳ Pending | — | — |
This is the foundation. Get this right, everything else follows.