Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions apps/web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ CRON_SECRET= # openssl rand -hex 32 -note: cron disabled if not set
NEXT_PUBLIC_BYPASS_PREMIUM_CHECKS=true
LOG_ZOD_ERRORS=true
# WEBHOOK_URL=
# INTERNAL_API_URL=

# =============================================================================
# LLM Configuration - Uncomment ONE provider block
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type { AnalyzeSenderPatternBody } from "@/app/api/ai/analyze-sender-pattern/route";
import { INTERNAL_API_KEY_HEADER } from "@/utils/internal-api";
import {
INTERNAL_API_KEY_HEADER,
getInternalApiUrl,
} from "@/utils/internal-api";
import { env } from "@/env";
import { createScopedLogger } from "@/utils/logger";

Expand All @@ -8,7 +11,7 @@ const logger = createScopedLogger("sender-pattern-analysis");
export async function analyzeSenderPattern(body: AnalyzeSenderPatternBody) {
try {
const response = await fetch(
`${env.NEXT_PUBLIC_BASE_URL}/api/ai/analyze-sender-pattern`,
`${getInternalApiUrl()}/api/ai/analyze-sender-pattern`,
{
method: "POST",
body: JSON.stringify(body),
Expand Down
4 changes: 2 additions & 2 deletions apps/web/app/api/resend/digest/all/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { NextResponse } from "next/server";
import { subDays } from "date-fns/subDays";
import prisma from "@/utils/prisma";
import { withError } from "@/utils/middleware";
import { env } from "@/env";
import { hasCronSecret, hasPostCronSecret } from "@/utils/cron";
import { getInternalApiUrl } from "@/utils/internal-api";
import { captureException } from "@/utils/error";
import { createScopedLogger } from "@/utils/logger";
import { publishToQstashQueue } from "@/utils/upstash";
Expand Down Expand Up @@ -40,7 +40,7 @@ async function sendDigestAllUpdate() {
eligibleAccounts: emailAccounts.length,
});

const url = `${env.NEXT_PUBLIC_BASE_URL}/api/resend/digest`;
const url = `${getInternalApiUrl()}/api/resend/digest`;

for (const emailAccount of emailAccounts) {
try {
Expand Down
4 changes: 2 additions & 2 deletions apps/web/app/api/resend/summary/all/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { NextResponse } from "next/server";
import { subDays } from "date-fns/subDays";
import prisma from "@/utils/prisma";
import { withError } from "@/utils/middleware";
import { env } from "@/env";
import { getInternalApiUrl } from "@/utils/internal-api";
import {
getCronSecretHeader,
hasCronSecret,
Expand Down Expand Up @@ -38,7 +38,7 @@ async function sendSummaryAllUpdate() {

logger.info("Sending summary to users", { count: emailAccounts.length });

const url = `${env.NEXT_PUBLIC_BASE_URL}/api/resend/summary`;
const url = `${getInternalApiUrl()}/api/resend/summary`;

for (const emailAccount of emailAccounts) {
try {
Expand Down
1 change: 1 addition & 0 deletions apps/web/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export const env = createEnv({
.optional()
.transform((value) => value?.split(",")),
WEBHOOK_URL: z.string().optional(),
INTERNAL_API_URL: z.string().optional(),
INTERNAL_API_KEY: z.string(),
WHITELIST_FROM: z.string().optional(),
USE_BACKUP_MODEL: z.coerce.boolean().optional().default(false),
Expand Down
4 changes: 2 additions & 2 deletions apps/web/utils/actions/clean.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
changeKeepToDoneSchema,
} from "@/utils/actions/clean.validation";
import { bulkPublishToQstash } from "@/utils/upstash";
import { env } from "@/env";
import { getInternalApiUrl } from "@/utils/internal-api";
import {
getLabel,
getOrCreateInboxZeroLabel,
Expand Down Expand Up @@ -139,7 +139,7 @@ export const cleanInboxAction = actionClient

if (threads.length === 0) break;

const url = `${env.WEBHOOK_URL || env.NEXT_PUBLIC_BASE_URL}/api/clean`;
const url = `${getInternalApiUrl()}/api/clean`;

logger.info("Pushing to Qstash", {
threadCount: threads.length,
Expand Down
4 changes: 2 additions & 2 deletions apps/web/utils/digest/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { env } from "@/env";
import { publishToQstashQueue } from "@/utils/upstash";
import { createScopedLogger } from "@/utils/logger";
import { emailToContent } from "@/utils/mail";
import { getInternalApiUrl } from "@/utils/internal-api";
import type { DigestBody } from "@/app/api/ai/digest/validation";
import type { ParsedMessage } from "@/utils/types";
import type { EmailForAction } from "@/utils/ai/types";
Expand All @@ -19,7 +19,7 @@ export async function enqueueDigestItem({
actionId?: string;
coldEmailId?: string;
}) {
const url = `${env.NEXT_PUBLIC_BASE_URL}/api/ai/digest`;
const url = `${getInternalApiUrl()}/api/ai/digest`;
try {
await publishToQstashQueue<DigestBody>({
queueName: "digest-item-summarize",
Expand Down
10 changes: 10 additions & 0 deletions apps/web/utils/internal-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@ import type { Logger } from "@/utils/logger";

export const INTERNAL_API_KEY_HEADER = "x-api-key";

/**
* Get the base URL for internal API calls.
* For self-hosted users behind firewalls, INTERNAL_API_URL can be set to
* an internal address like http://localhost:3000 or http://web:3000 (Docker service name).
* Falls back to WEBHOOK_URL, then NEXT_PUBLIC_BASE_URL.
*/
export function getInternalApiUrl(): string {
return env.INTERNAL_API_URL || env.WEBHOOK_URL || env.NEXT_PUBLIC_BASE_URL;
}

export const isValidInternalApiKey = (
headers: Headers,
logger: Logger,
Expand Down
3 changes: 2 additions & 1 deletion apps/web/utils/scheduled-actions/scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { createScopedLogger } from "@/utils/logger";
import { canActionBeDelayed } from "@/utils/delayed-actions";
import { env } from "@/env";
import { getCronSecretHeader } from "@/utils/cron";
import { getInternalApiUrl } from "@/utils/internal-api";
import { Client } from "@upstash/qstash";
import { addMinutes, getUnixTime } from "date-fns";

Expand Down Expand Up @@ -263,7 +264,7 @@ async function scheduleMessage({
deduplicationId: string;
}) {
const client = getQstashClient();
const url = `${env.WEBHOOK_URL || env.NEXT_PUBLIC_BASE_URL}/api/scheduled-actions/execute`;
const url = `${getInternalApiUrl()}/api/scheduled-actions/execute`;

const notBefore = getUnixTime(addMinutes(new Date(), delayInMinutes));

Expand Down
4 changes: 2 additions & 2 deletions apps/web/utils/upstash/categorize-senders.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import chunk from "lodash/chunk";
import { deleteQueue, listQueues, publishToQstashQueue } from "@/utils/upstash";
import { env } from "@/env";
import { getInternalApiUrl } from "@/utils/internal-api";
import type { AiCategorizeSenders } from "@/app/api/user/categorize/senders/batch/handle-batch-validation";
import { createScopedLogger } from "@/utils/logger";

Expand All @@ -21,7 +21,7 @@ const getCategorizeSendersQueueName = ({
export async function publishToAiCategorizeSendersQueue(
body: AiCategorizeSenders,
) {
const url = `${env.WEBHOOK_URL || env.NEXT_PUBLIC_BASE_URL}/api/user/categorize/senders/batch`;
const url = `${getInternalApiUrl()}/api/user/categorize/senders/batch`;

// Split senders into smaller chunks to process in batches
const BATCH_SIZE = 50;
Expand Down
7 changes: 5 additions & 2 deletions apps/web/utils/upstash/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Client, type FlowControl, type HeadersInit } from "@upstash/qstash";
import { env } from "@/env";
import { INTERNAL_API_KEY_HEADER } from "@/utils/internal-api";
import {
INTERNAL_API_KEY_HEADER,
getInternalApiUrl,
} from "@/utils/internal-api";
import { sleep } from "@/utils/sleep";
import { createScopedLogger } from "@/utils/logger";

Expand All @@ -17,7 +20,7 @@ export async function publishToQstash<T>(
flowControl?: FlowControl,
) {
const client = getQstashClient();
const url = `${env.WEBHOOK_URL || env.NEXT_PUBLIC_BASE_URL}${path}`;
const url = `${getInternalApiUrl()}${path}`;

if (client) {
return client.publishJSON({
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ services:
DATABASE_URL: ${DATABASE_URL:-postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-password}@db:5432/${POSTGRES_DB:-inboxzero}?schema=public}
DIRECT_URL: ${DIRECT_URL:-postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-password}@db:5432/${POSTGRES_DB:-inboxzero}?schema=public}
UPSTASH_REDIS_URL: ${UPSTASH_REDIS_URL:-http://serverless-redis-http:80}
INTERNAL_API_URL: ${INTERNAL_API_URL:-http://web:3000}
restart: always

cron:
Expand Down
26 changes: 26 additions & 0 deletions docs/hosting/self-hosting.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,32 @@ Gmail and Outlook push notification subscriptions expire periodically and must b

Replace `YOUR_CRON_SECRET` with the value of `CRON_SECRET` from your `.env` file.

## Optional: QStash for Advanced Features

[Upstash QStash](https://upstash.com/docs/qstash/overall/getstarted) is a serverless message queue that enables scheduled and delayed actions. It's optional but recommended for the full feature set.

**Features that require QStash:**

| Feature | Without QStash | With QStash |
|---------|---------------|-------------|
| **Email digest** | ❌ Not available | ✅ Full support |
| **Delayed/scheduled email actions** | ❌ Not available | ✅ Full support |
| **AI categorization of senders*** | ✅ Works (sync) | ✅ Works (async with retries) |
| **Bulk inbox cleaning*** | ❌ Not available | ✅ Full support |

*Early access features - available on the Early Access page.

**Cost**: QStash has a generous free tier and scales to zero when not in use. See [QStash pricing](https://upstash.com/pricing/qstash).

**Setup**: Add your QStash credentials to `.env`:
```bash
QSTASH_TOKEN=your-qstash-token
QSTASH_CURRENT_SIGNING_KEY=your-signing-key
QSTASH_NEXT_SIGNING_KEY=your-next-signing-key
```

Adding alternative scheduling backends (like Redis-based scheduling) for self-hosted users is on our roadmap.

## Building from Source (Optional)

If you prefer to build the image yourself instead of using the pre-built one:
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,11 +527,13 @@ Full guide: https://docs.getinboxzero.com/self-hosting/microsoft-oauth`,
env.DATABASE_URL = `postgresql://${env.POSTGRES_USER}:${env.POSTGRES_PASSWORD}@db:5432/${env.POSTGRES_DB}`;
env.DIRECT_URL = env.DATABASE_URL;
env.UPSTASH_REDIS_URL = "http://serverless-redis-http:80";
env.INTERNAL_API_URL = "http://web:3000";
} else {
// Web app runs on host: containers expose ports to localhost
env.DATABASE_URL = `postgresql://${env.POSTGRES_USER}:${env.POSTGRES_PASSWORD}@localhost:${postgresPort}/${env.POSTGRES_DB}`;
env.DIRECT_URL = env.DATABASE_URL;
env.UPSTASH_REDIS_URL = `http://localhost:${redisPort}`;
env.INTERNAL_API_URL = `http://localhost:${webPort}`;
}
} else {
// External infrastructure - set placeholders for user to fill in
Expand Down
1 change: 1 addition & 0 deletions turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@

"ADMINS",
"WEBHOOK_URL",
"INTERNAL_API_URL",
"INTERNAL_API_KEY",
"USE_BACKUP_MODEL",
"WHITELIST_FROM",
Expand Down
2 changes: 1 addition & 1 deletion version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v2.21.47
v2.21.48
Loading