diff --git a/app/api/v1/reports/bug/route.ts b/app/api/v1/reports/bug/route.ts
new file mode 100644
index 000000000..e99e19618
--- /dev/null
+++ b/app/api/v1/reports/bug/route.ts
@@ -0,0 +1,158 @@
+/**
+ * POST /api/v1/reports/bug
+ * Receives structured bug reports from clients like Milady and forwards them
+ * through the existing Cloud email service.
+ */
+
+import { randomUUID } from "node:crypto";
+import { type NextRequest, NextResponse } from "next/server";
+import { z } from "zod";
+import { RateLimitPresets, withRateLimit } from "@/lib/middleware/rate-limit";
+import { emailService } from "@/lib/services/email";
+import { logger } from "@/lib/utils/logger";
+
+const bugReportSchema = z.object({
+ source: z.string().max(120).default("unknown-client"),
+ submittedAt: z.string().optional(),
+ category: z.enum(["general", "startup-failure"]).default("general"),
+ description: z.string().min(1).max(500),
+ stepsToReproduce: z.string().min(1).max(10000),
+ expectedBehavior: z.string().max(10000).optional(),
+ actualBehavior: z.string().max(10000).optional(),
+ environment: z.string().max(200).optional(),
+ nodeVersion: z.string().max(200).optional(),
+ modelProvider: z.string().max(200).optional(),
+ appVersion: z.string().max(200).optional(),
+ releaseChannel: z.string().max(200).optional(),
+ logs: z.string().max(50000).optional(),
+ startup: z
+ .object({
+ reason: z.string().max(120).optional(),
+ phase: z.string().max(120).optional(),
+ message: z.string().max(1000).optional(),
+ detail: z.string().max(10000).optional(),
+ status: z.number().int().optional(),
+ path: z.string().max(500).optional(),
+ })
+ .optional(),
+});
+
+function escapeHtml(text: string): string {
+ return text
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+}
+
+function section(title: string, content?: string) {
+ if (!content) return "";
+ return `
+
+
${escapeHtml(title)}
+
${escapeHtml(content)}
+
`;
+}
+
+async function handlePOST(request: NextRequest) {
+ const body = await request.json();
+ const validated = bugReportSchema.parse(body);
+
+ const reportId = `rpt_${randomUUID()}`;
+ const receivedAt = new Date().toISOString();
+ const recipient =
+ process.env.BUG_REPORT_EMAIL_TO ??
+ process.env.SUPPORT_EMAIL ??
+ "developer@elizalabs.ai";
+
+ const startupSummary = validated.startup
+ ? JSON.stringify(validated.startup, null, 2)
+ : undefined;
+
+ const html = `
+
+
+
+
+ New Structured Bug Report
+
+
+
+
Structured Bug Report
+
+
+ | Report ID | ${escapeHtml(reportId)} |
+ | Source | ${escapeHtml(validated.source)} |
+ | Category | ${escapeHtml(validated.category)} |
+ | Received | ${escapeHtml(receivedAt)} |
+ | App Version | ${escapeHtml(validated.appVersion || "unknown")} |
+ | Release Channel | ${escapeHtml(validated.releaseChannel || "unknown")} |
+ | Environment | ${escapeHtml(validated.environment || "unknown")} |
+
+
+ ${section("Description", validated.description)}
+ ${section("Steps to Reproduce", validated.stepsToReproduce)}
+ ${section("Expected Behavior", validated.expectedBehavior)}
+ ${section("Actual Behavior", validated.actualBehavior)}
+ ${section("Startup Context", startupSummary)}
+ ${section("Logs", validated.logs)}
+
+
+`;
+
+ const text = [
+ "Structured Bug Report",
+ `Report ID: ${reportId}`,
+ `Source: ${validated.source}`,
+ `Category: ${validated.category}`,
+ `Received: ${receivedAt}`,
+ `App Version: ${validated.appVersion ?? "unknown"}`,
+ `Release Channel: ${validated.releaseChannel ?? "unknown"}`,
+ `Environment: ${validated.environment ?? "unknown"}`,
+ "",
+ "Description:",
+ validated.description,
+ "",
+ "Steps to Reproduce:",
+ validated.stepsToReproduce,
+ validated.expectedBehavior ? `\nExpected Behavior:\n${validated.expectedBehavior}` : "",
+ validated.actualBehavior ? `\nActual Behavior:\n${validated.actualBehavior}` : "",
+ startupSummary ? `\nStartup Context:\n${startupSummary}` : "",
+ validated.logs ? `\nLogs:\n${validated.logs}` : "",
+ ]
+ .filter(Boolean)
+ .join("\n");
+
+ const sent = await emailService.send({
+ to: recipient,
+ subject: `[Bug Report] ${validated.source} - ${validated.description.slice(0, 100)}`,
+ html,
+ text,
+ });
+
+ if (!sent) {
+ logger.error("[BugReport] Failed to send bug report email", {
+ reportId,
+ source: validated.source,
+ category: validated.category,
+ });
+ return NextResponse.json(
+ { accepted: false, error: "Email service unavailable" },
+ { status: 503 },
+ );
+ }
+
+ logger.info("[BugReport] Structured bug report accepted", {
+ reportId,
+ source: validated.source,
+ category: validated.category,
+ });
+
+ return NextResponse.json({
+ accepted: true,
+ id: reportId,
+ });
+}
+
+export const POST = withRateLimit(handlePOST, RateLimitPresets.STRICT);