Midday / apps /api /src /index.ts
Jules
Final deployment with all fixes and verified content
c09f67c
// Import Sentry instrumentation first, before any other modules
import "./instrument";
import { trpcServer } from "@hono/trpc-server";
import { OpenAPIHono } from "@hono/zod-openapi";
import {
buildDependenciesResponse,
buildReadinessResponse,
checkDependencies,
} from "@midday/health/checker";
import { apiDependencies } from "@midday/health/probes";
import { logger } from "@midday/logger";
import { Scalar } from "@scalar/hono-api-reference";
import * as Sentry from "@sentry/bun";
import { cors } from "hono/cors";
import { secureHeaders } from "hono/secure-headers";
import { routers } from "./rest/routers";
import type { Context } from "./rest/types";
import { createTRPCContext } from "./trpc/init";
import { appRouter } from "./trpc/routers/_app";
import { httpLogger } from "./utils/logger";
const app = new OpenAPIHono<Context>();
app.use(httpLogger());
app.use(
secureHeaders({
crossOriginResourcePolicy: "cross-origin",
}),
);
app.use(
"*",
cors({
origin: process.env.ALLOWED_API_ORIGINS?.split(",") ?? [],
allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"],
allowHeaders: [
"Authorization",
"Content-Type",
"User-Agent",
"accept-language",
"x-trpc-source",
"x-user-locale",
"x-user-timezone",
"x-user-country",
"x-force-primary",
// Slack webhook headers
"x-slack-signature",
"x-slack-request-timestamp",
],
exposeHeaders: [
"Content-Length",
"Content-Type",
"Cache-Control",
"Cross-Origin-Resource-Policy",
],
maxAge: 86400,
}),
);
app.use(
"/trpc/*",
trpcServer({
router: appRouter,
createContext: createTRPCContext,
onError: ({ error, path }) => {
logger.error(`[tRPC] ${path}`, {
message: error.message,
code: error.code,
cause: error.cause instanceof Error ? error.cause.message : undefined,
stack: error.stack,
});
// Send to Sentry (skip client errors like NOT_FOUND, UNAUTHORIZED)
if (error.code === "INTERNAL_SERVER_ERROR") {
Sentry.captureException(error, {
tags: { source: "trpc", path: path ?? "unknown" },
});
}
},
}),
);
app.get("/favicon.ico", (c) => c.body(null, 204));
app.get("/robots.txt", (c) => c.body(null, 204));
app.get("/health", (c) => {
return c.json({ status: "ok" }, 200);
});
app.get("/info", (c) => {
return c.html(`
<html>
<head>
<title>Midday API Info</title>
<style>
body { font-family: sans-serif; line-height: 1.6; max-width: 800px; margin: 40px auto; padding: 20px; }
h1 { color: #333; }
.use-case { margin-bottom: 20px; border-left: 4px solid #333; padding-left: 15px; }
</style>
</head>
<body>
<h1>Midday API</h1>
<p>Welcome to the Midday API. This service provides a suite of business management tools.</p>
<h2>Core Use Cases</h2>
<div class="use-case">
<h3>1. AI-Powered Financial Insights</h3>
<p>Get automated analysis of your revenue, expenses, and profit trends. The API uses Helmholtz Blablador LLMs to provide actionable advice.</p>
</div>
<div class="use-case">
<h3>2. Intelligent Document Extraction</h3>
<p>Upload receipts or invoices and get structured JSON data back. No more manual data entry.</p>
</div>
<div class="use-case">
<h3>3. Seamless Invoicing</h3>
<p>Generate and manage professional invoices via API, integrated with your transaction history.</p>
</div>
<p>Explore the full <a href="/">API Documentation (Scalar)</a> to get started.</p>
</body>
</html>
`);
});
app.get("/health/ready", async (c) => {
const results = await checkDependencies(apiDependencies(), 1);
const response = buildReadinessResponse(results);
return c.json(response, response.status === "ok" ? 200 : 503);
});
app.get("/health/dependencies", async (c) => {
const results = await checkDependencies(apiDependencies());
const response = buildDependenciesResponse(results);
return c.json(response, response.status === "ok" ? 200 : 503);
});
app.doc("/openapi", {
openapi: "3.1.0",
info: {
version: "1.0.0",
title: "Midday API",
description:
"Midday is an all-in-one platform for business management. This API provides access to core features including:\n\n" +
"- **Financial Insights**: AI-generated reports on business performance and spending patterns.\n" +
"- **Document Processing**: Automatic extraction of data from receipts and invoices using advanced LLMs.\n" +
"- **Transaction Management**: Real-time tracking and categorization of business transactions.\n" +
"- **Invoicing**: Create and manage professional invoices for your clients.\n\n" +
"Use this API to integrate Midday's powerful business assistant into your own workflows.",
contact: {
name: "Midday Support",
email: "engineer@midday.ai",
url: "https://midday.ai",
},
license: {
name: "AGPL-3.0 license",
url: "https://github.com/midday-ai/midday/blob/main/LICENSE",
},
},
servers: [
{
url: "https://api.midday.ai",
description: "Production API",
},
],
security: [
{
oauth2: [],
},
{ token: [] },
],
});
// Register security scheme
app.openAPIRegistry.registerComponent("securitySchemes", "token", {
type: "http",
scheme: "bearer",
description: "Default authentication mechanism",
"x-speakeasy-example": "MIDDAY_API_KEY",
});
app.get(
"/",
Scalar({ url: "/openapi", pageTitle: "Midday API", theme: "saturn" }),
);
app.route("/", routers);
// Global error handler — captures unhandled route errors to Sentry
app.onError((err, c) => {
Sentry.captureException(err, {
tags: { source: "hono", path: c.req.path, method: c.req.method },
});
logger.error(`[Hono] ${c.req.method} ${c.req.path}`, {
message: err.message,
stack: err.stack,
});
return c.json({ error: "Internal Server Error" }, 500);
});
/**
* Unhandled exception and rejection handlers
*/
process.on("uncaughtException", (err) => {
logger.error("Uncaught exception", { error: err.message, stack: err.stack });
Sentry.captureException(err, {
tags: { errorType: "uncaught_exception" },
});
});
process.on("unhandledRejection", (reason, promise) => {
logger.error("Unhandled rejection", {
reason: reason instanceof Error ? reason.message : String(reason),
stack: reason instanceof Error ? reason.stack : undefined,
});
Sentry.captureException(
reason instanceof Error ? reason : new Error(String(reason)),
{
tags: { errorType: "unhandled_rejection" },
},
);
});
export default {
port: process.env.PORT ? Number.parseInt(process.env.PORT, 10) : 3000,
fetch: app.fetch,
host: "0.0.0.0", // Listen on all interfaces
idleTimeout: 60,
};