YeboLink Services Deep Dive
All business logic is encapsulated in service classes located in src/services/. This page documents every service in detail.
AuthService (auth.service.ts)
Handles all authentication flows including signup, login, JWT management, and password reset.
Constants
private static ACCESS_TOKEN_EXPIRES_IN = '7d';
private static REFRESH_TOKEN_EXPIRES_DAYS = 30;
private static VERIFICATION_TOKEN_EXPIRES_HOURS = 24;
private static RESET_TOKEN_EXPIRES_HOURS = 2;Methods
signup(data)
Creates a new workspace account with email verification.
static async signup(data: {
email: string;
password: string;
company_name: string;
phone?: string;
country?: string;
}): Promise<{ workspace: Workspace; message: string }>Flow:
- Check if email already exists
- Hash password (bcrypt, 12 rounds)
- Create workspace in database
- Generate verification token (32 bytes, 24h expiry)
- Send verification email via EmailService
login(email, password)
Authenticates user and returns tokens.
static async login(email: string, password: string): Promise<AuthResult>Returns:
interface AuthResult {
workspace: Omit<Workspace, 'password_hash'>;
token: string; // JWT access token
refreshToken: string; // 64-byte hex refresh token
}refresh(refreshTokenValue)
Exchanges refresh token for new access + refresh tokens (token rotation).
static async refresh(refreshTokenValue: string): Promise<{
token: string;
refreshToken: string;
}>verifyEmail(token)
Marks workspace email as verified after user clicks verification link.
requestPasswordReset(email) / resetPassword(token, newPassword)
Two-step password reset flow with secure tokens.
generateJwt(payload) / verifyJwt(token)
JWT helpers using jsonwebtoken:
static generateJwt(payload: JwtPayload): string {
return jwt.sign(payload, env.JWT_SECRET, {
expiresIn: this.ACCESS_TOKEN_EXPIRES_IN,
});
}MessageService (message.service.ts)
Orchestrates message sending across all channels.
Credit Costs
private static CREDIT_COSTS: Record<string, number> = {
sms: 1,
whatsapp: 0.5,
email: 0.1,
voice: 2,
push: 0.05,
web: 0,
};Methods
sendMessage(workspaceId, apiKeyId, data)
Queues a single message for delivery.
static async sendMessage(
workspaceId: string,
apiKeyId: string | undefined,
data: SendMessageRequest
): Promise<Message>Flow:
- Calculate credit cost based on channel
- Check workspace balance
- Find or create contact
- Create message record (status:
queued) - Enqueue to Cloud Tasks via
enqueueMessage()
sendBulkMessages(workspaceId, apiKeyId, data)
Sends to multiple recipients with template personalization.
static async sendBulkMessages(
workspaceId: string,
apiKeyId: string | undefined,
data: BulkSendRequest
): Promise<{ queued: number; failed: number; messages: Message[] }>Features:
- Pre-checks total credits needed
- Supports
personalization:
private static personalizeContent(content: any, recipient: Record<string, any>): any {
let personalizedText = content.text;
Object.keys(recipient).forEach((key) => {
const placeholder = new RegExp(`{{${key}}}`, 'g');
personalizedText = personalizedText.replace(placeholder, recipient[key]);
});
return { ...content, text: personalizedText };
}getUsageStats(workspaceId, startDate, endDate)
Returns aggregated statistics:
- Total messages & credits
- Breakdown by channel
- Breakdown by status
MessageProcessor (message-processor.service.ts)
Processes queued messages asynchronously (called by Cloud Tasks or inline fallback).
static async process(messageId: string, workspaceId: string): Promise<boolean>Flow:
- Fetch message from database
- Skip if already processed (
status !== 'queued') - Deduct credits via
WorkspaceModel.deductCredits() - Route to appropriate provider:
- SMS →
TwilioService.sendSMS() - Email →
EmailService.sendEmail() - WhatsApp → Not yet implemented
- SMS →
- Update message status to
sentorfailed - Trigger workspace webhooks
Provider Routing:
if (message.channel === 'sms') {
const result = await TwilioService.sendSMS(
message.recipient,
message.content.text || '',
senderName,
statusCallback
);
providerMessageId = result.sid;
} else if (message.channel === 'email') {
await EmailService.sendEmail(
message.recipient,
subject,
htmlContent,
textContent,
fromName
);
}BillingService (billing.service.ts)
Manages Stripe integration and credit packages.
Credit Packages
private static CREDIT_PACKAGES = [
{ credits: 125, price: 1000, name: 'Starter Pack' }, // $10 → $0.080/credit
{ credits: 340, price: 2500, name: 'Growth Pack' }, // $25 → $0.074/credit
{ credits: 715, price: 5000, name: 'Business Pack' }, // $50 → $0.070/credit
{ credits: 1500, price: 10000, name: 'Pro Pack' }, // $100 → $0.067/credit
{ credits: 3200, price: 20000, name: 'Scale Pack' }, // $200 → $0.063/credit
{ credits: 8500, price: 50000, name: 'Enterprise Pack' }, // $500 → $0.059/credit
];Methods
createCheckoutSession(workspaceId, credits)
Creates a Stripe Checkout session with localized pricing.
static async createCheckoutSession(
workspaceId: string,
credits: number
): Promise<{ sessionId: string; url: string }>Multi-currency Support:
const localised = localisePackage(basePkg, workspace.country);
const session = await this.stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [{
price_data: {
currency: localised.stripeCurrency,
product_data: {
name: basePkg.name,
description: `${credits} messaging credits for YeboLink`,
},
unit_amount: localised.stripeAmount,
},
quantity: 1,
}],
mode: 'payment',
success_url: `${env.FRONTEND_URL}/billing/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${env.FRONTEND_URL}/credits`,
metadata: { workspaceId, credits: credits.toString() },
});handleWebhook(signature, payload)
Processes Stripe webhook events:
checkout.session.completed→ Add creditspayment_intent.succeeded→ Log successpayment_intent.payment_failed→ Log failure
getCreditPackages(countryCode?)
Returns packages with localized pricing.
TwilioService (twilio.service.ts)
Handles SMS and Voice via Twilio.
Alphanumeric Sender ID Support
const ALPHA_SENDER_SUPPORTED = new Set([
'SZ', // Eswatini
'ZA', // South Africa
'KE', // Kenya
'GH', // Ghana
'TZ', // Tanzania
'UG', // Uganda
// ...more African countries
]);Methods
sendSMS(to, message, senderName?, statusCallback?)
static async sendSMS(
to: string,
message: string,
senderName?: string,
statusCallback?: string
): Promise<{ sid: string; status: string }>Sender Logic:
- If country supports alphanumeric sender AND senderName provided → use brand name
- Otherwise → use TWILIO_PHONE_NUMBER
let from: string;
if (senderName && supportsAlphaSender(to)) {
from = sanitizeSenderName(senderName) || env.TWILIO_PHONE_NUMBER;
} else {
from = env.TWILIO_PHONE_NUMBER;
}validateWebhookSignature(signature, url, params)
Validates incoming Twilio webhooks using twilio.validateRequest().
parseStatusCallback(body)
Maps Twilio statuses to internal statuses:
private static mapTwilioStatus(s: string): string {
return ({
queued: 'queued',
sending: 'queued',
sent: 'sent',
delivered: 'delivered',
undelivered: 'failed',
failed: 'failed',
received: 'delivered'
})[s?.toLowerCase()] || 'queued';
}EmailService (email.service.ts)
Sends transactional emails via Resend.
Methods
sendEmail(to, subject, html, text?, fromName?)
static async sendEmail(
to: string,
subject: string,
html: string,
text?: string,
fromName?: string
): Promise<void> {
const result = await resend.emails.send({
from: `${fromName || env.FROM_NAME} <${env.FROM_EMAIL}>`,
to,
subject,
html,
text: text || html.replace(/<[^>]*>/g, ''),
});
}Template Emails
sendVerificationEmail(email, name, token)— Account verificationsendPasswordResetEmail(email, name, token)— Password resetsendLowBalanceAlert(email, name, balance)— Credit warning
WebhookService (webhook.service.ts)
Manages customer webhook endpoints.
Supported Events
static getSupportedEvents(): string[] {
return [
'message.sent',
'message.delivered',
'message.failed',
'message.received',
'credit.low',
'credit.depleted',
];
}Methods
create(workspaceId, url, events)
Creates webhook with HMAC secret:
const secret = crypto.randomBytes(32).toString('hex');
const secretHash = crypto.createHash('sha256').update(secret).digest('hex');trigger(workspaceId, eventType, payload)
Queues webhook deliveries via Cloud Tasks:
for (const webhook of webhooks) {
await enqueueWebhook(webhook.id, eventType, payload);
}generateSignature(payload, secret) / verifySignature()
HMAC-SHA256 signature for webhook verification:
static generateSignature(payload: string, secret: string): string {
return crypto.createHmac('sha256', secret).update(payload).digest('hex');
}SenderVerificationService (sender-verification.service.ts)
AI-powered sender name validation using Google Gemini.
Verification Flow
- Sanitize — Remove non-alphanumeric, limit to 11 chars
- Hard-block patterns — All numeric, blocked words
- Allowlist check — Bypass AI for known-good names
- Gemini AI check — Impersonation, offensive, misleading
Blocked Words
const BLOCKED_WORDS = [
'police', 'govt', 'government', 'bank', 'reserve', 'central',
'interpol', 'fbi', 'cia', 'army', 'military', 'court',
'judge', 'president', 'minister', 'customs', 'immigration',
'tax', 'revenue', 'treasury', 'emergency', '911', '999', '112',
];Gemini Prompt
const prompt = `You are a compliance checker for SMS/email sender IDs...
Evaluate this sender name: "${name}"
ONLY reject if it clearly:
1. Impersonates a well-known major brand
2. Impersonates government/emergency services
3. Contains obviously offensive content
4. Is an obvious scam pattern
DO NOT reject:
- Unusual or made-up brand names
- Short acronyms
- Names that are just unconventional
Respond with ONLY valid JSON:
{"approved": true/false, "flag_for_review": true/false, "reason": "..."}`;DashboardService (dashboard.service.ts)
Provides platform-wide metrics for the CEO dashboard.
Metrics Returned
interface DashboardMetrics {
messagesSent: number;
messagesTrend: 'up' | 'down' | 'neutral';
messagesChange: number;
newContacts: number;
contactsTrend: 'up' | 'down' | 'neutral';
contactsChange: number;
deliveryRate: number;
deliveryTrend: 'up' | 'down' | 'neutral';
deliveryChange: number;
activeWorkspaces: number;
workspacesTrend: 'up' | 'down' | 'neutral';
workspacesChange: number;
}Compares today vs yesterday for trend calculation.
BlogService (blog.service.ts)
Manages blog posts for the autoblogger integration.
Methods
createBlogPost(data)— Create new post with unique sluggetPublishedBlogPosts(page, limit, category)— Paginated listgetBlogPostBySlug(slug)— Single post lookup
Cloud Tasks Integration (jobs/queues.ts)
Manages async job queues.
Queue Functions
enqueueTask(options)
Generic task enqueue:
const task = {
httpRequest: {
httpMethod: 'POST',
url,
headers: { 'Content-Type': 'application/json' },
body: Buffer.from(JSON.stringify(options.payload)).toString('base64'),
},
};
if (options.delaySeconds) {
task.scheduleTime = {
seconds: Math.floor(Date.now() / 1000) + options.delaySeconds,
};
}enqueueMessage(messageId, workspaceId)
Queues message processing with inline fallback:
const taskName = await enqueueTask({
queue: 'message-queue',
url: '/internal/process-message',
payload: { messageId, workspaceId },
});
// Inline fallback if Cloud Tasks unavailable
if (taskName === null) {
const { MessageProcessor } = await import('../services/message-processor.service');
await MessageProcessor.process(messageId, workspaceId);
}enqueueWebhook(webhookId, eventType, payload)
Queues webhook delivery to customer endpoints.