-
+
Knowledge base entries are used to help draft responses to
emails.
diff --git a/apps/web/app/(app)/[emailAccountId]/automation/knowledge/KnowledgeForm.tsx b/apps/web/app/(app)/[emailAccountId]/automation/knowledge/KnowledgeForm.tsx
index 5602bdd3b2..d559d61b98 100644
--- a/apps/web/app/(app)/[emailAccountId]/automation/knowledge/KnowledgeForm.tsx
+++ b/apps/web/app/(app)/[emailAccountId]/automation/knowledge/KnowledgeForm.tsx
@@ -67,7 +67,10 @@ export function KnowledgeForm({
};
const result = editingItem
- ? await updateKnowledgeAction(emailAccountId, submitData as UpdateKnowledgeBody)
+ ? await updateKnowledgeAction(
+ emailAccountId,
+ submitData as UpdateKnowledgeBody,
+ )
: await createKnowledgeAction(emailAccountId, submitData);
if (result?.serverError) {
diff --git a/apps/web/app/(app)/[emailAccountId]/clean/helpers.ts b/apps/web/app/(app)/[emailAccountId]/clean/helpers.ts
index 304602c107..0d0757bc59 100644
--- a/apps/web/app/(app)/[emailAccountId]/clean/helpers.ts
+++ b/apps/web/app/(app)/[emailAccountId]/clean/helpers.ts
@@ -14,7 +14,9 @@ export async function getJobById({
export async function getLastJob({
emailAccountId,
-}: { emailAccountId: string }) {
+}: {
+ emailAccountId: string;
+}) {
return await prisma.cleanupJob.findFirst({
where: { emailAccountId },
orderBy: { createdAt: "desc" },
diff --git a/apps/web/app/(app)/[emailAccountId]/settings/LabelsSection.tsx b/apps/web/app/(app)/[emailAccountId]/settings/LabelsSection.tsx
index a30a23cd4a..0da30c8cd9 100644
--- a/apps/web/app/(app)/[emailAccountId]/settings/LabelsSection.tsx
+++ b/apps/web/app/(app)/[emailAccountId]/settings/LabelsSection.tsx
@@ -159,7 +159,9 @@ function LabelsSectionFormInner(props: {
});
try {
- await updateLabelsAction(emailAccountId, { labels: formLabels });
+ await updateLabelsAction(emailAccountId, {
+ labels: formLabels,
+ });
toastSuccess({ description: "Updated labels!" });
} catch (error) {
console.error(error);
diff --git a/apps/web/app/api/google/threads/basic/route.ts b/apps/web/app/api/google/threads/basic/route.ts
index 90038caa7d..5844d5f602 100644
--- a/apps/web/app/api/google/threads/basic/route.ts
+++ b/apps/web/app/api/google/threads/basic/route.ts
@@ -27,7 +27,7 @@ async function getGetThreads(
export const GET = withEmailAccount(async (request) => {
const emailAccountId = request.auth.emailAccountId;
-
+
const gmail = await getGmailClientForEmail({ emailAccountId });
const { searchParams } = new URL(request.url);
diff --git a/apps/web/app/api/user/stats/newsletters/summary/route.ts b/apps/web/app/api/user/stats/newsletters/summary/route.ts
index 06be7480c9..b8a6d0b6ac 100644
--- a/apps/web/app/api/user/stats/newsletters/summary/route.ts
+++ b/apps/web/app/api/user/stats/newsletters/summary/route.ts
@@ -8,7 +8,9 @@ export type NewsletterSummaryResponse = Awaited<
async function getNewsletterSummary({
emailAccountId,
-}: { emailAccountId: string }) {
+}: {
+ emailAccountId: string;
+}) {
const result = await prisma.newsletter.groupBy({
where: { emailAccountId },
by: ["status"],
diff --git a/apps/web/app/blog/page.tsx b/apps/web/app/blog/page.tsx
index 9a97d5994c..7112a73116 100644
--- a/apps/web/app/blog/page.tsx
+++ b/apps/web/app/blog/page.tsx
@@ -206,7 +206,11 @@ const mdxPosts: Post[] = [
export const revalidate = 60;
export default async function BlogContentsPage() {
- const posts = await sanityFetch({ query: postsQuery });
+ // Skip Sanity fetch during build with dummy credentials
+ let posts: SanityPost[] = [];
+ if (process.env.NEXT_PUBLIC_SANITY_PROJECT_ID !== 'dummy-sanity-project-id-for-build') {
+ posts = await sanityFetch({ query: postsQuery });
+ }
return (
diff --git a/apps/web/app/blog/post/[slug]/page.tsx b/apps/web/app/blog/post/[slug]/page.tsx
index 0a097c2a84..8b680ff28e 100644
--- a/apps/web/app/blog/post/[slug]/page.tsx
+++ b/apps/web/app/blog/post/[slug]/page.tsx
@@ -10,6 +10,9 @@ import { captureException } from "@/utils/error";
export const revalidate = 60;
export async function generateStaticParams() {
+ if (process.env.NEXT_PUBLIC_SANITY_PROJECT_ID === 'dummy-sanity-project-id-for-build') {
+ return [];
+ }
const posts = await client.fetch(postPathsQuery);
return posts;
}
@@ -67,5 +70,9 @@ export default async function Page(props: Props) {
const params = await props.params;
const post = await sanityFetch({ query: postQuery, params });
+ if (!post) {
+ return Blog post content unavailable.
;
+ }
+
return ;
}
diff --git a/apps/web/app/sitemap.ts b/apps/web/app/sitemap.ts
index cdabc7a1da..4007d30188 100644
--- a/apps/web/app/sitemap.ts
+++ b/apps/web/app/sitemap.ts
@@ -4,6 +4,13 @@ import { sanityFetch } from "@/sanity/lib/fetch";
import { postSlugsQuery } from "@/sanity/lib/queries";
async function getBlogPosts() {
+ // Skip Sanity fetch during build with dummy credentials
+ if (
+ process.env.NEXT_PUBLIC_SANITY_PROJECT_ID ===
+ "dummy-sanity-project-id-for-build"
+ ) {
+ return []; // Return empty array directly
+ }
const posts = await sanityFetch<{ slug: string; date: string }[]>({
query: postSlugsQuery,
});
diff --git a/apps/web/utils/ai/choose-rule/ai-choose-args.ts b/apps/web/utils/ai/choose-rule/ai-choose-args.ts
index 8966b03b50..87ac0cd92c 100644
--- a/apps/web/utils/ai/choose-rule/ai-choose-args.ts
+++ b/apps/web/utils/ai/choose-rule/ai-choose-args.ts
@@ -113,7 +113,9 @@ export async function aiGenerateArgs({
function getSystemPrompt({
emailAccount,
-}: { emailAccount: EmailAccountWithAI }) {
+}: {
+ emailAccount: EmailAccountWithAI;
+}) {
return `You are an AI assistant that helps people manage their emails.
diff --git a/apps/web/utils/categorize/senders/categorize.ts b/apps/web/utils/categorize/senders/categorize.ts
index fdb5264093..0c25999cdb 100644
--- a/apps/web/utils/categorize/senders/categorize.ts
+++ b/apps/web/utils/categorize/senders/categorize.ts
@@ -144,7 +144,9 @@ function preCategorizeSendersWithStaticRules(
export async function getCategories({
emailAccountId,
-}: { emailAccountId: string }) {
+}: {
+ emailAccountId: string;
+}) {
const categories = await getUserCategories({ emailAccountId });
if (categories.length === 0) throw new SafeError("No categories found");
return { categories };
diff --git a/apps/web/utils/category.server.ts b/apps/web/utils/category.server.ts
index 4868783a41..73214f9f6b 100644
--- a/apps/web/utils/category.server.ts
+++ b/apps/web/utils/category.server.ts
@@ -12,7 +12,9 @@ export type CategoryWithRules = Prisma.CategoryGetPayload<{
export const getUserCategories = async ({
emailAccountId,
-}: { emailAccountId: string }) => {
+}: {
+ emailAccountId: string;
+}) => {
const categories = await prisma.category.findMany({
where: { emailAccountId },
});
@@ -21,7 +23,9 @@ export const getUserCategories = async ({
export const getUserCategoriesWithRules = async ({
emailAccountId,
-}: { emailAccountId: string }) => {
+}: {
+ emailAccountId: string;
+}) => {
const categories = await prisma.category.findMany({
where: { emailAccountId },
select: {
diff --git a/apps/web/utils/group/find-matching-group.ts b/apps/web/utils/group/find-matching-group.ts
index 93b069ec2e..0c4f77478d 100644
--- a/apps/web/utils/group/find-matching-group.ts
+++ b/apps/web/utils/group/find-matching-group.ts
@@ -6,7 +6,9 @@ import { type GroupItem, GroupItemType } from "@prisma/client";
type GroupsWithRules = Awaited>;
export async function getGroupsWithRules({
emailAccountId,
-}: { emailAccountId: string }) {
+}: {
+ emailAccountId: string;
+}) {
return prisma.group.findMany({
where: { emailAccountId, rule: { isNot: null } },
include: { items: true, rule: { include: { actions: true } } },
diff --git a/apps/web/utils/redis/category.ts b/apps/web/utils/redis/category.ts
index 7708ffd73d..c9884d807b 100644
--- a/apps/web/utils/redis/category.ts
+++ b/apps/web/utils/redis/category.ts
@@ -55,7 +55,9 @@ export async function deleteCategory({
export async function deleteCategories({
emailAccountId,
-}: { emailAccountId: string }) {
+}: {
+ emailAccountId: string;
+}) {
const key = getKey({ emailAccountId });
return redis.del(key);
}
diff --git a/apps/web/utils/redis/label.ts b/apps/web/utils/redis/label.ts
index a764aa6a24..fe11de5cfe 100644
--- a/apps/web/utils/redis/label.ts
+++ b/apps/web/utils/redis/label.ts
@@ -44,7 +44,9 @@ export async function saveUserLabels({
export async function deleteUserLabels({
emailAccountId,
-}: { emailAccountId: string }) {
+}: {
+ emailAccountId: string;
+}) {
const key = getUserLabelsKey({ emailAccountId });
return redis.del(key);
}
diff --git a/apps/web/utils/redis/reply-tracker-analyzing.ts b/apps/web/utils/redis/reply-tracker-analyzing.ts
index 8cc809b6ef..44dfaa2d57 100644
--- a/apps/web/utils/redis/reply-tracker-analyzing.ts
+++ b/apps/web/utils/redis/reply-tracker-analyzing.ts
@@ -6,7 +6,9 @@ function getKey({ emailAccountId }: { emailAccountId: string }) {
export async function startAnalyzingReplyTracker({
emailAccountId,
-}: { emailAccountId: string }) {
+}: {
+ emailAccountId: string;
+}) {
const key = getKey({ emailAccountId });
// expire in 5 minutes
await redis.set(key, "true", { ex: 5 * 60 });
@@ -14,14 +16,18 @@ export async function startAnalyzingReplyTracker({
export async function stopAnalyzingReplyTracker({
emailAccountId,
-}: { emailAccountId: string }) {
+}: {
+ emailAccountId: string;
+}) {
const key = getKey({ emailAccountId });
await redis.del(key);
}
export async function isAnalyzingReplyTracker({
emailAccountId,
-}: { emailAccountId: string }) {
+}: {
+ emailAccountId: string;
+}) {
const key = getKey({ emailAccountId });
const result = await redis.get(key);
return result === "true";
diff --git a/apps/web/utils/reply-tracker/outbound.ts b/apps/web/utils/reply-tracker/outbound.ts
index bfc8d01b22..4848db185c 100644
--- a/apps/web/utils/reply-tracker/outbound.ts
+++ b/apps/web/utils/reply-tracker/outbound.ts
@@ -183,7 +183,9 @@ async function resolveReplyTrackers(
async function isOutboundTrackingEnabled({
email,
-}: { email: string }): Promise {
+}: {
+ email: string;
+}): Promise {
const userSettings = await prisma.emailAccount.findUnique({
where: { email },
select: { outboundReplyTracking: true },
diff --git a/apps/web/utils/swr.ts b/apps/web/utils/swr.ts
index 62db24450d..6096d2fe74 100644
--- a/apps/web/utils/swr.ts
+++ b/apps/web/utils/swr.ts
@@ -30,15 +30,20 @@ export function processSWRResponse<
}
// Handle potential non-Error SWR errors (less common)
if (swrError) {
- return {
- ...swrResult,
- data: null,
- error: { error: String(swrError) }, // Convert non-Error to string
- } as SWRResponse;
+ return {
+ ...swrResult,
+ data: null,
+ error: { error: String(swrError) }, // Convert non-Error to string
+ } as SWRResponse;
}
// Handle API error returned within data
- if (data && typeof data === 'object' && 'error' in data && typeof data.error === 'string') {
+ if (
+ data &&
+ typeof data === "object" &&
+ "error" in data &&
+ typeof data.error === "string"
+ ) {
return {
...swrResult,
data: null,
@@ -53,4 +58,4 @@ export function processSWRResponse<
data: data as TData | null, // SWR handles undefined during load
error: undefined,
} as SWRResponse;
-}
\ No newline at end of file
+}
diff --git a/docker-compose.yml b/docker-compose.yml
index 87aefab556..c3d750681c 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -41,6 +41,7 @@ services:
build:
context: .
dockerfile: ./docker/Dockerfile.web
+ # image: ghcr.io/elie222/inbox-zero:latest
env_file:
- ./apps/web/.env
depends_on:
diff --git a/docker/Dockerfile.prod b/docker/Dockerfile.prod
new file mode 100644
index 0000000000..8c838a04aa
--- /dev/null
+++ b/docker/Dockerfile.prod
@@ -0,0 +1,87 @@
+FROM node:22-alpine
+
+WORKDIR /app
+
+# Install necessary tools
+RUN apk add --no-cache openssl
+RUN npm install -g pnpm
+
+# Copy all package manager files first for caching
+COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc* ./
+COPY apps/web/package.json apps/web/
+COPY apps/unsubscriber/package.json apps/unsubscriber/
+COPY apps/mcp-server/package.json apps/mcp-server/
+COPY packages/eslint-config/package.json packages/eslint-config/
+COPY packages/loops/package.json packages/loops/
+COPY packages/resend/package.json packages/resend/
+COPY packages/tinybird/package.json packages/tinybird/
+COPY packages/tinybird-ai-analytics/package.json packages/tinybird-ai-analytics/
+COPY packages/tsconfig/package.json packages/tsconfig/
+
+# Copy the rest of the application code FIRST
+COPY . .
+
+# Install ALL dependencies (including dev, no pruning)
+# This will now run postinstall scripts *after* source code is copied
+RUN pnpm install --frozen-lockfile
+
+# Set NODE_ENV for build and runtime
+ENV NODE_ENV=production
+
+# Provide dummy build-time ENV VARS (Still needed for build)
+ENV DATABASE_URL="postgresql://dummy:dummy@dummy:5432/dummy?schema=public"
+ENV DIRECT_URL="postgresql://dummy:dummy@dummy:5432/dummy?schema=public"
+ENV NEXTAUTH_SECRET="dummy_secret_for_build_only"
+ENV NEXTAUTH_URL="http://localhost:3000"
+ENV GOOGLE_CLIENT_ID="dummy_id_for_build_only"
+ENV GOOGLE_CLIENT_SECRET="dummy_secret_for_build_only"
+ENV GOOGLE_ENCRYPT_SECRET="dummy_encrypt_secret_for_build_only"
+ENV GOOGLE_ENCRYPT_SALT="dummy_encrypt_salt_for_build_only"
+ENV GOOGLE_PUBSUB_TOPIC_NAME="dummy_topic_for_build_only"
+ENV GOOGLE_PUBSUB_VERIFICATION_TOKEN="dummy_pubsub_token_for_build"
+ENV INTERNAL_API_KEY="dummy_apikey_for_build_only"
+ENV API_KEY_SALT="dummy_salt_for_build_only"
+ENV UPSTASH_REDIS_URL="http://dummy-redis-for-build:6379"
+ENV UPSTASH_REDIS_TOKEN="dummy_redis_token_for_build"
+ENV REDIS_URL="redis://dummy:dummy@dummy:6379"
+ENV QSTASH_TOKEN="dummy_qstash_token_for_build"
+ENV QSTASH_CURRENT_SIGNING_KEY="dummy_qstash_curr_key_for_build"
+ENV QSTASH_NEXT_SIGNING_KEY="dummy_qstash_next_key_for_build"
+ENV NEXT_PUBLIC_SANITY_PROJECT_ID="dummy-sanity-project-id-for-build"
+ENV NEXT_PUBLIC_SANITY_DATASET="dummy-sanity-dataset-for-build"
+
+# Ensure prisma generate runs
+RUN pnpm --filter inbox-zero-ai exec -- prisma generate
+
+# Provide dummy DB URLs for build-time Prisma schema loading
+ENV DATABASE_URL="postgresql://dummy:dummy@dummy:5432/dummy?schema=public"
+ENV DIRECT_URL="postgresql://dummy:dummy@dummy:5432/dummy?schema=public"
+# Provide dummy values for other required build-time ENV VARS
+ENV NEXTAUTH_SECRET="dummy_secret_for_build_only"
+ENV GOOGLE_CLIENT_ID="dummy_id_for_build_only"
+ENV GOOGLE_CLIENT_SECRET="dummy_secret_for_build_only"
+ENV GOOGLE_ENCRYPT_SECRET="dummy_encrypt_secret_for_build_only"
+ENV GOOGLE_ENCRYPT_SALT="dummy_encrypt_salt_for_build_only"
+ENV GOOGLE_PUBSUB_TOPIC_NAME="dummy_topic_for_build_only"
+ENV INTERNAL_API_KEY="dummy_apikey_for_build_only"
+
+# Provide dummy Redis/QStash vars needed for build analysis
+ENV UPSTASH_REDIS_URL="http://dummy-redis-for-build:6379"
+ENV UPSTASH_REDIS_TOKEN="dummy_redis_token_for_build"
+ENV QSTASH_CURRENT_SIGNING_KEY="dummy_qstash_curr_key_for_build"
+ENV QSTASH_NEXT_SIGNING_KEY="dummy_qstash_next_key_for_build"
+ENV QSTASH_TOKEN="dummy_qstash_token_for_build"
+
+# Provide dummy Sanity vars needed for build
+ENV NEXT_PUBLIC_SANITY_PROJECT_ID="dummy_sanity_id_for_build"
+ENV NEXT_PUBLIC_SANITY_DATASET="dummy_sanity_dataset_for_build"
+
+# Build the Next.js application only (skip prisma migrate deploy during build)
+RUN pnpm --filter inbox-zero-ai exec -- next build
+
+# Expose port 3000
+EXPOSE 3000
+
+# Set the default command to start the production server
+# Use the simpler pnpm command, should work now as pnpm & next are installed
+CMD pnpm --filter inbox-zero-ai start
\ No newline at end of file
diff --git a/package.json b/package.json
index e131648283..a7ec7105da 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"husky": "9.1.7",
"lint-staged": "15.5.1",
"prettier": "3.5.3",
+ "prettier-plugin-tailwindcss": "0.6.11",
"turbo": "2.5.0"
},
"packageManager": "pnpm@10.8.1+sha512.c50088ba998c67b8ca8c99df8a5e02fd2ae2e2b29aaf238feaa9e124248d3f48f9fb6db2424949ff901cffbb5e0f0cc1ad6aedb602cd29450751d11c35023677",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5baf35da73..b04b7ce922 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -33,6 +33,9 @@ importers:
prettier:
specifier: 3.5.3
version: 3.5.3
+ prettier-plugin-tailwindcss:
+ specifier: 0.6.11
+ version: 0.6.11(prettier@3.5.3)
turbo:
specifier: 2.5.0
version: 2.5.0
@@ -9430,6 +9433,7 @@ packages:
node-domexception@1.0.0:
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
engines: {node: '>=10.5.0'}
+ deprecated: Use your platform's native DOMException instead
node-fetch@2.6.12:
resolution: {integrity: sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==}