Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
65dcae8
feat: implement WhatsApp integration for Eliza App
hanzlamateen Feb 9, 2026
b4a3a1b
feat: add WhatsApp identity columns to users table
hanzlamateen Feb 9, 2026
5d7555c
feat: enhance user data cleanup script with Discord and WhatsApp support
hanzlamateen Feb 9, 2026
f27ade4
feat: add WhatsApp fields to user info response
hanzlamateen Feb 9, 2026
a03a5d9
feat: add WhatsApp ID support across authentication routes and sessio…
hanzlamateen Feb 9, 2026
5f73b79
feat: enhance WhatsApp message handling with retry logic
hanzlamateen Feb 9, 2026
7b8ab3c
feat: improve WhatsApp webhook processing and validation
hanzlamateen Feb 9, 2026
9a19842
Merge branch 'dev' into feat/eliza-app-whatsapp-support
hanzlamateen Feb 10, 2026
35721a0
Merge branch 'dev' into feat/eliza-app-whatsapp-support
hanzlamateen Feb 10, 2026
755b527
feat: implement organization-level WhatsApp integration
hanzlamateen Feb 10, 2026
1845962
Merge branch 'dev' into feat/eliza-app-whatsapp-support
hanzlamateen Feb 13, 2026
c8bbe6e
refactor: remove WhatsApp identity migration and update migration index
hanzlamateen Feb 13, 2026
c822770
Merge branch 'dev' into feat/eliza-app-whatsapp-support
hanzlamateen Feb 25, 2026
594bf14
Fixed migration name
hanzlamateen Feb 25, 2026
6d03a60
Fixed migration index
hanzlamateen Feb 25, 2026
40ffe95
Merge branch 'dev' into feat/eliza-app-whatsapp-support
hanzlamateen Feb 25, 2026
1c91188
feat(whatsapp): implement typing indicator and performance tracing in…
hanzlamateen Feb 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions example.env.local → .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -552,3 +552,60 @@ ELIZA_APP_DISCORD_CLIENT_SECRET=

# Leader election key for Discord bot (for distributed deployments)
# ELIZA_APP_LEADER_KEY=discord:eliza-app-bot:leader:local

# =============================================================================
# ELIZA APP - WHATSAPP (Optional)
# =============================================================================
# WhatsApp Business Cloud API integration for the Eliza App public bot.
# Requires a Meta Business account and WhatsApp Business App.
# Setup guide: https://developers.facebook.com/docs/whatsapp/cloud-api/get-started

# Permanent access token from Meta Business Settings > System Users
# Generate with whatsapp_business_messaging permission
# For testing, use the temporary token from Meta App Dashboard > WhatsApp > API Setup
ELIZA_APP_WHATSAPP_ACCESS_TOKEN=

# Phone Number ID from Meta App Dashboard > WhatsApp > API Setup
# This is NOT the phone number itself — it's Meta's internal ID for your number
ELIZA_APP_WHATSAPP_PHONE_NUMBER_ID=

# App Secret from Meta App Dashboard > Settings > Basic
# Used for HMAC-SHA256 webhook signature verification (X-Hub-Signature-256)
ELIZA_APP_WHATSAPP_APP_SECRET=

# Custom verify token for webhook handshake (you choose this value)
# Must match what you enter in Meta App Dashboard > WhatsApp > Configuration > Callback URL
# Generate with: openssl rand -hex 32
ELIZA_APP_WHATSAPP_VERIFY_TOKEN=

# Display phone number in E.164 format (e.g. +15551649988)
# Shown to users on the frontend so they know which number to message
ELIZA_APP_WHATSAPP_PHONE_NUMBER=

# =============================================================================
# ORGANIZATION-LEVEL WHATSAPP (Dashboard Connections)
# =============================================================================
# Per-organization WhatsApp Business integration via the dashboard.
# Credentials are stored in the database (secrets service) — NO env vars needed.
#
# How it works:
# 1. Go to Dashboard > Settings > Connections
# 2. Find "WhatsApp Business" card and enter your Meta credentials
# 3. After connecting, copy the Webhook URL and Verify Token
# 4. Configure the webhook in Meta App Dashboard > WhatsApp > Configuration
#
# For local development, expose your server via ngrok:
# ELIZA_API_URL=https://your-ngrok-url.ngrok.io
#
# Dev-only fallback env vars (used when org has no stored credentials):
# WHATSAPP_ACCESS_TOKEN=
# WHATSAPP_PHONE_NUMBER_ID=
# WHATSAPP_APP_SECRET=

# =============================================================================
# WEBHOOK DEVELOPMENT (Optional)
# =============================================================================
# Skip webhook signature verification in non-production environments.
# Useful for testing WhatsApp/Blooio/Telegram webhooks with curl.
# NEVER set this in production — it is ignored even if set.
# SKIP_WEBHOOK_VERIFICATION=true
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ OPENAI_API_KEY=sk-your_openai_key
AI_GATEWAY_API_KEY=your_gateway_key
```

**Eliza App variables** (for Telegram, iMessage, and Discord integrations):
**Eliza App variables** (for Telegram, iMessage, Discord, and WhatsApp integrations):

```env
# JWT secret for Eliza App user sessions (required)
Expand All @@ -441,8 +441,19 @@ ELIZA_APP_BLOOIO_API_KEY= # From Blooio dashboard
ELIZA_APP_DISCORD_BOT_TOKEN= # Developer Portal → Bot
ELIZA_APP_DISCORD_APPLICATION_ID= # Developer Portal → General Information (also the OAuth2 Client ID)
ELIZA_APP_DISCORD_CLIENT_SECRET= # Developer Portal → OAuth2 → Client Secret

# WhatsApp Business Cloud API (optional — for the public Eliza App bot)
ELIZA_APP_WHATSAPP_ACCESS_TOKEN= # Meta Business Settings → System Users → Generate Token
ELIZA_APP_WHATSAPP_PHONE_NUMBER_ID= # Meta App Dashboard → WhatsApp → API Setup
ELIZA_APP_WHATSAPP_APP_SECRET= # Meta App Dashboard → Settings → Basic → App Secret
ELIZA_APP_WHATSAPP_VERIFY_TOKEN= # Generate: openssl rand -hex 32
ELIZA_APP_WHATSAPP_PHONE_NUMBER= # Display phone number in E.164 format (e.g. +14245074963)
```

**Organization-level WhatsApp** (Dashboard > Settings > Connections):

> Per-organization WhatsApp credentials are stored in the database via the dashboard UI — **no env vars required**. Each organization connects their own WhatsApp Business account by entering their Access Token, Phone Number ID, and App Secret in the connections settings. The webhook URL and verify token are auto-generated and displayed after connecting.

See [example.env.local](example.env.local) for the full list of Eliza App environment variables.

**Generate secure passwords:**
Expand Down
1 change: 1 addition & 0 deletions app/api/eliza-app/auth/discord/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ async function handleDiscordAuth(
discordId: discordUser.id,
...(user.phone_number && { phoneNumber: user.phone_number }),
...(user.telegram_id && { telegramId: user.telegram_id }),
...(user.whatsapp_id && { whatsappId: user.whatsapp_id }),
},
);

Expand Down
1 change: 1 addition & 0 deletions app/api/eliza-app/auth/telegram/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ async function handleTelegramAuth(
telegramId: String(authData.id),
phoneNumber: user.phone_number || phoneNumber,
...(user.discord_id && { discordId: user.discord_id }),
...(user.whatsapp_id && { whatsappId: user.whatsapp_id }),
},
);

Expand Down
220 changes: 220 additions & 0 deletions app/api/eliza-app/auth/whatsapp/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
/**
* Eliza App - WhatsApp Authentication Endpoint
*
* Issues JWT session tokens for users already identified via WhatsApp webhook.
* Since WhatsApp users are auto-provisioned on first message, this endpoint
* allows the eliza-app frontend to authenticate them using their WhatsApp ID.
*
* Flow:
* 1. User messages the WhatsApp bot → auto-provisioned with whatsapp_id
* 2. Frontend redirects user with whatsapp_id claim
* 3. This endpoint verifies the user exists and issues a session token
*
* POST /api/eliza-app/auth/whatsapp
*/

import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { logger } from "@/lib/utils/logger";
import { RateLimitPresets, withRateLimit } from "@/lib/middleware/rate-limit";
import {
elizaAppUserService,
elizaAppSessionService,
type ValidatedSession,
} from "@/lib/services/eliza-app";

/**
* Request body schema
*/
const whatsappAuthSchema = z.object({
// WhatsApp ID (digits only, e.g. "14245074963")
whatsapp_id: z.string()
.min(7, "WhatsApp ID must be at least 7 digits")
.max(15, "WhatsApp ID must be at most 15 digits")
.regex(/^\d+$/, "WhatsApp ID must contain only digits"),
});

/**
* Success response type
*/
interface AuthSuccessResponse {
success: true;
user: {
id: string;
whatsapp_id: string;
whatsapp_name: string | null;
phone_number: string | null;
name: string | null;
organization_id: string;
};
session: {
token: string;
expires_at: string;
};
}

/**
* Error response type
*/
interface AuthErrorResponse {
success: false;
error: string;
code: string;
}

async function handleWhatsAppAuth(
request: NextRequest,
): Promise<NextResponse<AuthSuccessResponse | AuthErrorResponse>> {
// Parse and validate request body
let body: unknown;
try {
body = await request.json();
} catch {
return NextResponse.json(
{ success: false, error: "Invalid JSON body", code: "INVALID_JSON" },
{ status: 400 },
);
}

const parseResult = whatsappAuthSchema.safeParse(body);
if (!parseResult.success) {
return NextResponse.json(
{ success: false, error: "Invalid request body", code: "INVALID_REQUEST" },
{ status: 400 },
);
}

const { whatsapp_id: whatsappId } = parseResult.data;

// Check for existing session (session-based linking)
const authHeader = request.headers.get("authorization");
let existingSession: ValidatedSession | null = null;
if (authHeader) {
existingSession = await elizaAppSessionService.validateAuthHeader(authHeader);
if (existingSession) {
logger.info("[ElizaApp WhatsAppAuth] Session-based linking detected", {
existingUserId: existingSession.userId,
});
}
}

if (existingSession) {
// ---- SESSION-BASED LINKING: Link WhatsApp to existing user ----
const linkResult = await elizaAppUserService.linkWhatsAppToUser(
existingSession.userId,
{ whatsappId },
);

if (!linkResult.success) {
return NextResponse.json(
{
success: false,
error: linkResult.error || "This WhatsApp account is already linked to another account",
code: "WHATSAPP_ALREADY_LINKED",
},
{ status: 409 },
);
}

// Fetch the updated user
const updatedUser = await elizaAppUserService.getById(existingSession.userId);
if (!updatedUser || !updatedUser.organization) {
return NextResponse.json(
{ success: false, error: "User not found after linking", code: "INTERNAL_ERROR" },
{ status: 500 },
);
}

const session = await elizaAppSessionService.createSession(
updatedUser.id,
updatedUser.organization.id,
{
whatsappId,
phoneNumber: updatedUser.phone_number || undefined,
...(updatedUser.telegram_id && { telegramId: updatedUser.telegram_id }),
...(updatedUser.discord_id && { discordId: updatedUser.discord_id }),
},
);

logger.info("[ElizaApp WhatsAppAuth] Session-based WhatsApp linking successful", {
userId: updatedUser.id,
whatsappId,
});

return NextResponse.json({
success: true,
user: {
id: updatedUser.id,
whatsapp_id: updatedUser.whatsapp_id!,
whatsapp_name: updatedUser.whatsapp_name,
phone_number: updatedUser.phone_number,
name: updatedUser.name,
organization_id: updatedUser.organization.id,
},
session: {
token: session.token,
expires_at: session.expiresAt.toISOString(),
},
});
}

// ---- STANDARD FLOW: Look up existing user by WhatsApp ID ----
// WhatsApp users must first message the bot to be auto-provisioned
const userWithOrg = await elizaAppUserService.getByWhatsAppId(whatsappId);

if (!userWithOrg || !userWithOrg.organization) {
return NextResponse.json(
{
success: false,
error: "WhatsApp account not found. Please message our WhatsApp bot first to create your account.",
code: "USER_NOT_FOUND",
},
{ status: 404 },
);
}

logger.info("[ElizaApp WhatsAppAuth] Authentication successful", {
userId: userWithOrg.id,
whatsappId,
});

// Create session
const session = await elizaAppSessionService.createSession(
userWithOrg.id,
userWithOrg.organization.id,
{
whatsappId,
phoneNumber: userWithOrg.phone_number || undefined,
...(userWithOrg.telegram_id && { telegramId: userWithOrg.telegram_id }),
...(userWithOrg.discord_id && { discordId: userWithOrg.discord_id }),
},
);

return NextResponse.json({
success: true,
user: {
id: userWithOrg.id,
whatsapp_id: userWithOrg.whatsapp_id!,
whatsapp_name: userWithOrg.whatsapp_name,
phone_number: userWithOrg.phone_number,
name: userWithOrg.name,
organization_id: userWithOrg.organization.id,
},
session: {
token: session.token,
expires_at: session.expiresAt.toISOString(),
},
});
}

// Export with rate limiting
export const POST = withRateLimit(handleWhatsAppAuth, RateLimitPresets.STANDARD);

// Health check
export async function GET(): Promise<NextResponse> {
return NextResponse.json({
status: "ok",
service: "eliza-app-whatsapp-auth",
timestamp: new Date().toISOString(),
});
}
4 changes: 4 additions & 0 deletions app/api/eliza-app/user/me/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ interface UserInfoResponse {
discord_username: string | null;
discord_global_name: string | null;
discord_avatar_url: string | null;
whatsapp_id: string | null;
whatsapp_name: string | null;
phone_number: string | null;
name: string | null;
avatar: string | null;
Expand Down Expand Up @@ -106,6 +108,8 @@ async function handleGetUser(
discord_username: user.discord_username,
discord_global_name: user.discord_global_name,
discord_avatar_url: user.discord_avatar_url,
whatsapp_id: user.whatsapp_id,
whatsapp_name: user.whatsapp_name,
phone_number: user.phone_number,
name: user.name,
avatar: user.avatar,
Expand Down
Loading