feat: agnostic image with runtime variables + typed config

This commit is contained in:
Aditya Tripathi
2025-05-16 00:03:13 +00:00
parent 15092dd7c5
commit 37a8a4ff42
29 changed files with 244 additions and 130 deletions

View File

@@ -81,7 +81,7 @@ You can set up Zero in two ways:
```
- Configure your environment variables (see below)
- Setup cloudflare with `bun run cf-install`, you will need to run this everytime there is a `.env` change
- Start the database with the provided docker compose setup: `bun docker:up`
- Start the database with the provided docker compose setup: `bun docker:db:up`
- Initialize the database: `bun db:push`
3. **Start the App**

View File

@@ -10,7 +10,7 @@ import { TriangleAlert } from 'lucide-react';
import { useRouter } from 'next/navigation';
import Image from 'next/image';
import { toast } from 'sonner';
import Link from 'next/link';
import { env } from '@/lib/env';
interface EnvVarStatus {
name: string;
@@ -121,7 +121,7 @@ function LoginClientContent({ providers, isProd }: LoginClientProps) {
toast.promise(
signIn.social({
provider: provider.id as any,
callbackURL: `${process.env.NEXT_PUBLIC_APP_URL}/mail`,
callbackURL: `${env.NEXT_PUBLIC_APP_URL}/mail`,
}),
{
error: 'Login redirect failed',

View File

@@ -1,24 +1,28 @@
import { authProviders, customProviders, isProviderEnabled } from '@zero/server/auth-providers';
import { LoginClient } from './login-client';
import { env } from '@/lib/env';
export default function LoginPage() {
const envNodeEnv = process.env.NODE_ENV;
const envNodeEnv = env.NODE_ENV;
const isProd = envNodeEnv === 'production';
const authProviderStatus = authProviders(process.env as Record<string, string>).map(
const authProviderStatus = authProviders(env as unknown as Record<string, string>).map(
(provider) => {
const envVarStatus =
provider.envVarInfo?.map((envVar) => ({
name: envVar.name,
set: !!process.env[envVar.name],
source: envVar.source,
defaultValue: envVar.defaultValue,
})) || [];
provider.envVarInfo?.map((envVar) => {
const envVarName = envVar.name as keyof typeof env;
return {
name: envVar.name,
set: !!env[envVarName],
source: envVar.source,
defaultValue: envVar.defaultValue,
};
}) || [];
return {
id: provider.id,
name: provider.name,
enabled: isProviderEnabled(provider, process.env as Record<string, string>),
enabled: isProviderEnabled(provider, env as Record<string, string>),
required: provider.required,
envVarInfo: provider.envVarInfo,
envVarStatus,

View File

@@ -24,6 +24,7 @@ import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
import { env } from '@/lib/env';
import Image from 'next/image';
import { toast } from 'sonner';
@@ -147,7 +148,7 @@ export default function ConnectionsPage() {
onClick={async () => {
await authClient.linkSocial({
provider: connection.providerId,
callbackURL: `${process.env.NEXT_PUBLIC_APP_URL}/settings/connections`,
callbackURL: `${env.NEXT_PUBLIC_APP_URL}/settings/connections`,
});
}}
>

View File

@@ -2,6 +2,7 @@ import { ClientProviders } from '@/providers/client-providers';
import { ServerProviders } from '@/providers/server-providers';
import { SpeedInsights } from '@vercel/speed-insights/next';
import { Geist, Geist_Mono } from 'next/font/google';
import { PublicEnvScript } from 'next-runtime-env';
import { siteConfig } from '@/lib/site-config';
import type { PropsWithChildren } from 'react';
import type { Viewport } from 'next';
@@ -33,6 +34,7 @@ export default async function RootLayout({ children }: PropsWithChildren) {
<html suppressHydrationWarning>
<head>
<Script src="https://unpkg.com/web-streams-polyfill/dist/polyfill.js" />
<PublicEnvScript />
</head>
<body className={cn(geistSans.variable, geistMono.variable, 'antialiased')}>
<ServerProviders>

View File

@@ -1,4 +1,6 @@
import { ImageResponse } from 'next/og';
import { env } from '@/lib/env';
export const runtime = 'edge';
export async function GET() {
@@ -18,7 +20,7 @@ export async function GET() {
}
try {
const appUrl = process.env.NEXT_PUBLIC_APP_URL;
const appUrl = NEXT_PUBLIC_APP_URL;
if (!appUrl) {
throw new Error('NEXT_PUBLIC_APP_URL is not defined');
}
@@ -27,7 +29,7 @@ export async function GET() {
if (!mailResponse.ok) {
throw new Error('Failed to fetch SVG');
}
const mailBuffer = await mailResponse.arrayBuffer();
const mailBase64 = btoa(String.fromCharCode(...new Uint8Array(mailBuffer)));
const mail = `data:image/svg+xml;base64,${mailBase64}`;
@@ -52,7 +54,10 @@ export async function GET() {
<span tw="text-[#A1A1A1]">is here</span>
</div>
<div tw="text-[36px] text-center text-neutral-400 mt-10" style={{ fontFamily: 'light' }}>
<div
tw="text-[36px] text-center text-neutral-400 mt-10"
style={{ fontFamily: 'light' }}
>
Experience email the way you want with 0 - the first open source email app that puts
your privacy and safety first.
</div>

View File

@@ -15,6 +15,7 @@ import { useTranslations } from 'next-intl';
import { Button } from '../ui/button';
import { motion } from 'motion/react';
import { cn } from '@/lib/utils';
import { env } from '@/lib/env';
import { useMemo } from 'react';
export const AddConnectionDialog = ({
@@ -110,7 +111,7 @@ export const AddConnectionDialog = ({
onClick={async () =>
await authClient.linkSocial({
provider: provider.providerId,
callbackURL: `${process.env.NEXT_PUBLIC_APP_URL}/${pathname}`,
callbackURL: `${env.NEXT_PUBLIC_APP_URL}/${pathname}`,
})
}
>

View File

@@ -22,6 +22,7 @@ import { format } from 'date-fns-tz';
import { useQueryState } from 'nuqs';
import { Input } from '../ui/input';
import { useState } from 'react';
import { env } from '@/lib/env';
import VoiceChat from './voice';
import Image from 'next/image';
import { toast } from 'sonner';
@@ -142,7 +143,7 @@ export function AIChat() {
const { attach, track, refetch: refetchBilling } = useBilling();
const { messages, input, setInput, error, handleSubmit, status, stop } = useChat({
api: `${process.env.NEXT_PUBLIC_BACKEND_URL}/api/chat`,
api: `${env.NEXT_PUBLIC_BACKEND_URL}/api/chat`,
fetch: (url, options) => fetch(url, { ...options, credentials: 'include' }),
maxSteps: 5,
body: {

View File

@@ -17,6 +17,9 @@ import { useSession } from '@/lib/auth-client';
import type { Sender } from '@/types';
import dedent from 'dedent';
// Utils
import { env } from '@/lib/env';
interface EmailContent {
metadata: {
isUnread: boolean;
@@ -129,7 +132,7 @@ const VoiceChat = ({ onClose }: VoiceChatProps) => {
const emailContext = emailContent.join('\n\n');
const conversationId = await conversation.startSession({
agentId: process.env.NEXT_PUBLIC_ELEVENLABS_AGENT_ID!,
agentId: env.NEXT_PUBLIC_ELEVENLABS_AGENT_ID,
dynamicVariables: {
user_name: userName,
email_context: emailContext,

View File

@@ -5,6 +5,7 @@ import { usePartySocket } from 'partysocket/react';
import { useThreads } from '@/hooks/use-threads';
import { useLabels } from '@/hooks/use-labels';
import { useSession } from '@/lib/auth-client';
import { env } from '@/lib/env';
import { funnel } from 'remeda';
const DEBOUNCE_DELAY = 10_000; // 10 seconds is appropriate for real-time notifications
@@ -36,7 +37,7 @@ export const NotificationProvider = ({ headers }: { headers: Record<string, stri
query: {
token: headers['cookie'],
},
host: process.env.NEXT_PUBLIC_BACKEND_URL!,
host: env.NEXT_PUBLIC_BACKEND_URL,
onMessage: async (message: MessageEvent<string>) => {
console.warn('party message', message);
const [threadId, type] = message.data.split(':');

View File

@@ -1,11 +1,12 @@
import * as Sentry from '@sentry/nextjs';
import { env } from '@/lib/env';
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
if (env.NEXT_RUNTIME === 'nodejs') {
await import('./sentry.server.config');
}
if (process.env.NEXT_RUNTIME === 'edge') {
if (env.NEXT_RUNTIME === 'edge') {
await import('./sentry.edge.config');
}
}

View File

@@ -1,9 +1,10 @@
import { customSessionClient } from 'better-auth/client/plugins';
import { createAuthClient } from 'better-auth/react';
import type { Auth } from '@zero/server/auth';
import { env } from '@/lib/env';
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_BACKEND_URL,
baseURL: env.NEXT_PUBLIC_BACKEND_URL,
fetchOptions: {
credentials: 'include',
},

View File

@@ -1,3 +1,5 @@
import { env } from '@/lib/env';
export const I18N_LOCALE_COOKIE_NAME = 'i18n:locale';
export const SIDEBAR_COOKIE_NAME = 'sidebar:state';
export const AI_SIDEBAR_COOKIE_NAME = 'ai-sidebar:state';
@@ -6,7 +8,7 @@ export const SIDEBAR_WIDTH = '14rem';
export const SIDEBAR_WIDTH_MOBILE = '14rem';
export const SIDEBAR_WIDTH_ICON = '3rem';
export const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
export const BASE_URL = process.env.NEXT_PUBLIC_APP_URL;
export const BASE_URL = env.NEXT_PUBLIC_APP_URL;
export const MAX_URL_LENGTH = 2000;
export const CACHE_BURST_KEY = 'cache-burst:v0.0.2';

View File

@@ -5,6 +5,7 @@ import { getListUnsubscribeAction } from '@/lib/email-utils';
import { trpcClient } from '@/providers/query-provider';
import type { ParsedMessage } from '@/types';
import { track } from '@vercel/analytics';
import { env } from '@/lib/env';
export const handleUnsubscribe = async ({ emailData }: { emailData: ParsedMessage }) => {
try {
@@ -109,10 +110,10 @@ const forceExternalLinks = (html: string): string => {
const getProxiedUrl = (url: string) => {
if (url.startsWith('data:') || url.startsWith('blob:')) return url;
const proxyUrl = process.env.NEXT_PUBLIC_IMAGE_PROXY?.trim();
const proxyUrl = env.NEXT_PUBLIC_IMAGE_PROXY?.trim();
if (!proxyUrl) return url;
return proxyUrl + encodeURIComponent(url);
};
@@ -123,12 +124,15 @@ const proxyImageUrls = (html: string): string => {
doc.querySelectorAll('img').forEach((img) => {
const src = img.getAttribute('src');
if (!src) return;
const proxiedUrl = getProxiedUrl(src);
if (proxiedUrl !== src) {
img.setAttribute('data-original-src', src);
img.setAttribute('src', proxiedUrl);
img.setAttribute('onerror', `this.onerror=null; this.src=this.getAttribute('data-original-src');`);
img.setAttribute(
'onerror',
`this.onerror=null; this.src=this.getAttribute('data-original-src');`,
);
}
});
@@ -256,7 +260,7 @@ export const template = async (html: string, imagesEnabled: boolean = false) =>
if (typeof DOMParser === 'undefined') return html;
const nonce = generateNonce();
let processedHtml = forceExternalLinks(html);
if (imagesEnabled) {
processedHtml = proxyImageUrls(processedHtml);
}

42
apps/mail/lib/env.ts Normal file
View File

@@ -0,0 +1,42 @@
import { env as runtimeEnv } from 'next-runtime-env';
import { createEnv } from '@t3-oss/env-nextjs';
import { z } from 'zod';
const getEnv = (variable: string) => runtimeEnv(variable) ?? process.env[variable];
export const env = createEnv({
skipValidation: true,
server: {
DATABASE_URL: z.string().url(),
NEXT_RUNTIME: z.string().optional(),
NODE_ENV: z.string().optional(),
DOCKER_BUILD: z.coerce.boolean().optional(),
CI: z.coerce.boolean().optional(),
GROQ_API_KEY: z.string().optional(),
AI_SYSTEM_PROMPT: z.string().optional(),
RESEND_API_KEY: z.string().optional(),
REDIS_URL: z.string().url(),
REDIS_TOKEN: z.string(),
},
client: {
NEXT_PUBLIC_APP_URL: z.string().url(),
NEXT_PUBLIC_BACKEND_URL: z.string().url(),
NEXT_PUBLIC_ELEVENLABS_AGENT_ID: z.string(),
NEXT_PUBLIC_IMAGE_PROXY: z.string().url().optional(),
NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(),
NEXT_PUBLIC_POSTHOG_HOST: z.string().optional(),
NEXT_PUBLIC_IMAGE_API_URL: z.string().optional(),
},
experimental__runtimeEnv: {
NEXT_PUBLIC_BACKEND_URL: getEnv('NEXT_PUBLIC_BACKEND_URL') ?? 'http://REPLACE-BACKEND-URL.com',
NEXT_PUBLIC_APP_URL: getEnv('NEXT_PUBLIC_APP_URL') ?? 'http://REPLACE-APP-URL.com',
NEXT_PUBLIC_ELEVENLABS_AGENT_ID: getEnv('NEXT_PUBLIC_ELEVENLABS_AGENT_ID'),
NEXT_PUBLIC_IMAGE_PROXY: getEnv('NEXT_PUBLIC_IMAGE_PROXY'),
NEXT_PUBLIC_POSTHOG_KEY: getEnv('NEXT_PUBLIC_POSTHOG_KEY'),
NEXT_PUBLIC_POSTHOG_HOST: getEnv('NEXT_PUBLIC_POSTHOG_HOST'),
NEXT_PUBLIC_IMAGE_API_URL: getEnv('NEXT_PUBLIC_IMAGE_API_URL'),
},
});

View File

@@ -1,3 +1,4 @@
import { env } from '@/lib/env';
import { z } from 'zod';
export const groqChatCompletionSchema = z.object({
@@ -106,7 +107,7 @@ export async function generateCompletions({
embeddings,
userName,
}: CompletionsParams) {
if (!process.env.GROQ_API_KEY) throw new Error('Groq API Key is missing');
if (!env.GROQ_API_KEY) throw new Error('Groq API Key is missing');
// Map OpenAI model names to Groq equivalents if needed
const groqModel = MODEL_MAPPING[model] || model;
@@ -134,7 +135,7 @@ export async function generateCompletions({
// Enhance the system prompt for email generation to improve formatting
if (enhancedSystemPrompt.toLowerCase().includes('email')) {
enhancedSystemPrompt =
process.env.AI_SYSTEM_PROMPT ||
env.AI_SYSTEM_PROMPT ||
`You are an email assistant helping ${userName} write professional and concise email replies.
Important instructions:
@@ -188,7 +189,7 @@ export async function generateCompletions({
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.GROQ_API_KEY}`,
Authorization: `Bearer ${env.GROQ_API_KEY}`,
},
body: JSON.stringify(requestBody),
});

View File

@@ -5,15 +5,16 @@ import { PostHogProvider as PHProvider } from 'posthog-js/react';
import { useSession } from '@/lib/auth-client';
import { useEffect } from 'react';
import posthog from 'posthog-js';
import { env } from '@/lib/env';
export function PostHogProvider({ children }: { children: React.ReactNode }) {
const { data: session } = useSession();
useEffect(() => {
if (!process.env.NEXT_PUBLIC_POSTHOG_KEY) return;
if (!env.NEXT_PUBLIC_POSTHOG_KEY) return;
try {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY as string, {
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
posthog.init(env.NEXT_PUBLIC_POSTHOG_KEY as string, {
api_host: env.NEXT_PUBLIC_POSTHOG_HOST,
capture_pageview: true,
});
} catch (error) {

View File

@@ -1,8 +1,9 @@
import { Redis } from '@upstash/redis';
import { env } from '@/lib/env';
import { Resend } from 'resend';
export const resend = process.env.RESEND_API_KEY
? new Resend(process.env.RESEND_API_KEY)
export const resend = env.RESEND_API_KEY
? new Resend(env.RESEND_API_KEY)
: { emails: { send: async (...args: any[]) => console.log(args) } };
export const redis = new Redis({ url: process.env.REDIS_URL, token: process.env.REDIS_TOKEN });
export const redis = new Redis({ url: env.REDIS_URL, token: env.REDIS_TOKEN });

View File

@@ -1,4 +1,5 @@
import { type Metadata } from 'next';
import { env } from '@/lib/env';
const TITLE = 'Zero';
const DESCRIPTION =
@@ -17,7 +18,7 @@ export const siteConfig: Metadata = {
description: DESCRIPTION,
images: [
{
url: `${process.env.NEXT_PUBLIC_APP_URL}/og-api/home`,
url: `${env.NEXT_PUBLIC_APP_URL}/og-api/home`,
width: 1200,
height: 630,
alt: TITLE,
@@ -26,7 +27,7 @@ export const siteConfig: Metadata = {
},
category: 'Email Client',
alternates: {
canonical: process.env.NEXT_PUBLIC_APP_URL,
canonical: env.NEXT_PUBLIC_APP_URL,
},
keywords: [
'Mail',
@@ -50,5 +51,5 @@ export const siteConfig: Metadata = {
'Email Service',
'Web Application',
],
metadataBase: new URL(process.env.NEXT_PUBLIC_APP_URL!),
metadataBase: new URL(env.NEXT_PUBLIC_APP_URL!),
};

View File

@@ -8,6 +8,7 @@ import { twMerge } from 'tailwind-merge';
import type { JSONContent } from 'novel';
import type { Sender } from '@/types';
import LZString from 'lz-string';
import { env } from '@/lib/env';
export const FOLDERS = {
SPAM: 'spam',
@@ -357,8 +358,8 @@ export const createAIJsonContent = (text: string): JSONContent => {
};
export const getEmailLogo = (email: string) => {
if (!process.env.NEXT_PUBLIC_IMAGE_API_URL) return '';
return process.env.NEXT_PUBLIC_IMAGE_API_URL + email;
if (!env.NEXT_PUBLIC_IMAGE_API_URL) return '';
return env.NEXT_PUBLIC_IMAGE_API_URL + email;
};
export const generateConversationId = (): string => {

View File

@@ -2,6 +2,7 @@ import { type NextRequest, NextResponse } from 'next/server';
import { navigationConfig } from '@/config/navigation';
import { geolocation } from '@vercel/functions';
import { EU_COUNTRIES } from './lib/countries';
import { env } from '@/lib/env';
const disabledRoutes = Object.values(navigationConfig)
.flatMap((section) => section.sections)
@@ -19,7 +20,7 @@ export function middleware(request: NextRequest) {
const isEuRegion = EU_COUNTRIES.includes(country);
response.headers.set('x-user-eu-region', String(isEuRegion));
if (process.env.NODE_ENV === 'development') {
if (env.NODE_ENV === 'development') {
response.headers.set('x-user-eu-region', 'true');
}

View File

@@ -1,13 +1,14 @@
import createNextIntlPlugin from 'next-intl/plugin';
import { withSentryConfig } from '@sentry/nextjs';
import type { NextConfig } from 'next';
import { env } from '@/lib/env';
const nextConfig: NextConfig = {
// devIndicators: false,
output: process.env.DOCKER_BUILD ? 'standalone' : undefined,
output: env.DOCKER_BUILD ? 'standalone' : undefined,
compiler: {
removeConsole:
process.env.NODE_ENV === 'production'
env.NODE_ENV === 'production'
? {
exclude: ['warn', 'error'],
}
@@ -53,7 +54,7 @@ const nextConfig: NextConfig = {
return [
{
source: '/api/mailto-handler',
destination: `${process.env.NEXT_PUBLIC_BACKEND_URL}/api/mailto-handler`,
destination: `${env.NEXT_PUBLIC_BACKEND_URL}/api/mailto-handler`,
},
];
},
@@ -69,7 +70,7 @@ export default withSentryConfig(withNextIntl(nextConfig), {
project: 'nextjs',
// Only print logs for uploading source maps in CI
silent: !process.env.CI,
silent: !env.CI,
// For all available options, see:
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/

View File

@@ -58,6 +58,7 @@
"@react-email/render": "^1.0.6",
"@sentry/cli": "^2.45.0",
"@sentry/nextjs": "^9.19.0",
"@t3-oss/env-nextjs": "^0.13.4",
"@tanstack/query-sync-storage-persister": "^5.75.0",
"@tanstack/react-query": "^5.74.4",
"@tanstack/react-query-persist-client": "^5.75.2",
@@ -117,6 +118,7 @@
"motion": "12.4.7",
"next": "15.3.1",
"next-intl": "3.26.5",
"next-runtime-env": "^3.3.0",
"next-themes": "0.4.4",
"novel": "1.0.2",
"nuqs": "2.4.0",

View File

@@ -13,6 +13,7 @@ import { CACHE_BURST_KEY } from '@/lib/constants';
import type { PropsWithChildren } from 'react';
import { get, set, del } from 'idb-keyval';
import superjson from 'superjson';
import { env } from '@/lib/env';
import { toast } from 'sonner';
function createIDBPersister(idbValidKey: IDBValidKey = 'zero-query-cache') {
@@ -92,7 +93,7 @@ const getQueryClient = (session: Session | null) => {
};
const getUrl = () => {
return process.env.NEXT_PUBLIC_BACKEND_URL + '/api/trpc';
return env.NEXT_PUBLIC_BACKEND_URL + '/api/trpc';
};
export const { TRPCProvider, useTRPC, useTRPCClient } = createTRPCContext<AppRouter>();

View File

@@ -65,6 +65,7 @@
"@react-email/render": "^1.0.6",
"@sentry/cli": "^2.45.0",
"@sentry/nextjs": "^9.19.0",
"@t3-oss/env-nextjs": "^0.13.4",
"@tanstack/query-sync-storage-persister": "^5.75.0",
"@tanstack/react-query": "^5.74.4",
"@tanstack/react-query-persist-client": "^5.75.2",
@@ -124,6 +125,7 @@
"motion": "12.4.7",
"next": "15.3.1",
"next-intl": "3.26.5",
"next-runtime-env": "^3.3.0",
"next-themes": "0.4.4",
"novel": "1.0.2",
"nuqs": "2.4.0",
@@ -295,6 +297,10 @@
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
"@ark/schema": ["@ark/schema@0.46.0", "", { "dependencies": { "@ark/util": "0.46.0" } }, "sha512-c2UQdKgP2eqqDArfBqQIJppxJHvNNXuQPeuSPlDML4rjw+f1cu0qAlzOG4b8ujgm9ctIDWwhpyw6gjG5ledIVQ=="],
"@ark/util": ["@ark/util@0.46.0", "", {}, "sha512-JPy/NGWn/lvf1WmGCPw2VGpBg5utZraE84I7wli18EDF3p3zc/e9WolT35tINeZO3l7C77SjqRJeAUoT0CvMRg=="],
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
"@babel/compat-data": ["@babel/compat-data@7.27.2", "", {}, "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ=="],
@@ -637,6 +643,8 @@
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.3.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-yP7FueWjphQEPpJQ2oKmshk/ppOt+0/bB8JC8svPUZNy0Pi3KbPx2Llkzv1p8CoQa+D2wknINlJpHf3vtChVBw=="],
"@next/swc-win32-ia32-msvc": ["@next/swc-win32-ia32-msvc@14.2.28", "", { "os": "win32", "cpu": "ia32" }, "sha512-+Kcp1T3jHZnJ9v9VTJ/yf1t/xmtFAc/Sge4v7mVc1z+NYfYzisi8kJ9AsY8itbgq+WgEwMtOpiLLJsUy2qnXZw=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.3.1", "", { "os": "win32", "cpu": "x64" }, "sha512-3PMvF2zRJAifcRNni9uMk/gulWfWS+qVI/pagd+4yLF5bcXPZPPH2xlYRYOsUjmCJOXSTAC2PjRzbhsRzR2fDQ=="],
"@noble/ciphers": ["@noble/ciphers@0.6.0", "", {}, "sha512-mIbq/R9QXk5/cTfESb1OKtyFnk7oc1Om/8onA1158K9/OZUQFDEVy55jVTato+xmp3XX6F6Qh0zz0Nc1AxAlRQ=="],
@@ -985,6 +993,10 @@
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
"@t3-oss/env-core": ["@t3-oss/env-core@0.13.4", "", { "peerDependencies": { "arktype": "^2.1.0", "typescript": ">=5.0.0", "valibot": "^1.0.0-beta.7 || ^1.0.0", "zod": "^3.24.0 || ^4.0.0-beta.0" }, "optionalPeers": ["typescript", "valibot", "zod"] }, "sha512-zVOiYO0+CF7EnBScz8s0O5JnJLPTU0lrUi8qhKXfIxIJXvI/jcppSiXXsEJwfB4A6XZawY/Wg/EQGKANi/aPmQ=="],
"@t3-oss/env-nextjs": ["@t3-oss/env-nextjs@0.13.4", "", { "dependencies": { "@t3-oss/env-core": "0.13.4" }, "peerDependencies": { "typescript": ">=5.0.0", "valibot": "^1.0.0-beta.7 || ^1.0.0", "zod": "^3.24.0 || ^4.0.0-beta.0" }, "optionalPeers": ["typescript", "valibot", "zod"] }, "sha512-6ecXR7SH7zJKVcBODIkB7wV9QLMU23uV8D9ec6P+ULHJ5Ea/YXEHo+Z/2hSYip5i9ptD/qZh8VuOXyldspvTTg=="],
"@tailwindcss/typography": ["@tailwindcss/typography@0.5.16", "", { "dependencies": { "lodash.castarray": "^4.4.0", "lodash.isplainobject": "^4.0.6", "lodash.merge": "^4.6.2", "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA=="],
"@tanstack/query-core": ["@tanstack/query-core@5.75.0", "", {}, "sha512-rk8KQuCdhoRkzjRVF3QxLgAfFUyS0k7+GCQjlGEpEGco+qazJ0eMH6aO1DjDjibH7/ik383nnztua3BG+lOnwg=="],
@@ -1333,6 +1345,8 @@
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
"arktype": ["arktype@2.1.20", "", { "dependencies": { "@ark/schema": "0.46.0", "@ark/util": "0.46.0" } }, "sha512-IZCEEXaJ8g+Ijd59WtSYwtjnqXiwM8sWQ5EjGamcto7+HVN9eK0C4p0zDlCuAwWhpqr6fIBkxPuYDl4/Mcj/+Q=="],
"array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="],
"array-includes": ["array-includes@3.1.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.4", "is-string": "^1.0.7" } }, "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ=="],
@@ -2231,6 +2245,8 @@
"next-intl": ["next-intl@3.26.5", "", { "dependencies": { "@formatjs/intl-localematcher": "^0.5.4", "negotiator": "^1.0.0", "use-intl": "^3.26.5" }, "peerDependencies": { "next": "^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" } }, "sha512-EQlCIfY0jOhRldiFxwSXG+ImwkQtDEfQeSOEQp6ieAGSLWGlgjdb/Ck/O7wMfC430ZHGeUKVKax8KGusTPKCgg=="],
"next-runtime-env": ["next-runtime-env@3.3.0", "", { "dependencies": { "next": "^14", "react": "^18" } }, "sha512-JgKVnog9mNbjbjH9csVpMnz2tB2cT5sLF+7O47i6Ze/s/GoiKdV7dHhJHk1gwXpo6h5qPj5PTzryldtSjvrHuQ=="],
"next-themes": ["next-themes@0.4.4", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-LDQ2qIOJF0VnuVrrMSMLrWGjRMkq+0mpgl6e0juCLqdJ+oo8Q84JRWT6Wh11VDQKkMMe+dVzDKLWs5n87T+PkQ=="],
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
@@ -3407,6 +3423,10 @@
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
"next-runtime-env/next": ["next@14.2.28", "", { "dependencies": { "@next/env": "14.2.28", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "graceful-fs": "^4.2.11", "postcss": "8.4.31", "styled-jsx": "5.1.1" }, "optionalDependencies": { "@next/swc-darwin-arm64": "14.2.28", "@next/swc-darwin-x64": "14.2.28", "@next/swc-linux-arm64-gnu": "14.2.28", "@next/swc-linux-arm64-musl": "14.2.28", "@next/swc-linux-x64-gnu": "14.2.28", "@next/swc-linux-x64-musl": "14.2.28", "@next/swc-win32-arm64-msvc": "14.2.28", "@next/swc-win32-ia32-msvc": "14.2.28", "@next/swc-win32-x64-msvc": "14.2.28" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "react": "^18.2.0", "react-dom": "^18.2.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-QLEIP/kYXynIxtcKB6vNjtWLVs3Y4Sb+EClTC/CSVzdLD1gIuItccpu/n1lhmduffI32iPGEK2cLLxxt28qgYA=="],
"next-runtime-env/react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="],
"novel/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ=="],
"novel/@tiptap/extension-placeholder": ["@tiptap/extension-placeholder@2.11.7", "", { "peerDependencies": { "@tiptap/core": "^2.7.0", "@tiptap/pm": "^2.7.0" } }, "sha512-/06zXV4HIjYoiaUq1fVJo/RcU8pHbzx21evOpeG/foCfNpMI4xLU/vnxdUi6/SQqpZMY0eFutDqod1InkSOqsg=="],
@@ -3809,6 +3829,32 @@
"motion/framer-motion/motion-utils": ["motion-utils@12.8.3", "", {}, "sha512-GYVauZEbca8/zOhEiYOY9/uJeedYQld6co/GJFKOy//0c/4lDqk0zB549sBYqqV2iMuX+uHrY1E5zd8A2L+1Lw=="],
"next-runtime-env/next/@next/env": ["@next/env@14.2.28", "", {}, "sha512-PAmWhJfJQlP+kxZwCjrVd9QnR5x0R3u0mTXTiZDgSd4h5LdXmjxCCWbN9kq6hkZBOax8Rm3xDW5HagWyJuT37g=="],
"next-runtime-env/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@14.2.28", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kzGChl9setxYWpk3H6fTZXXPFFjg7urptLq5o5ZgYezCrqlemKttwMT5iFyx/p1e/JeglTwDFRtb923gTJ3R1w=="],
"next-runtime-env/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@14.2.28", "", { "os": "darwin", "cpu": "x64" }, "sha512-z6FXYHDJlFOzVEOiiJ/4NG8aLCeayZdcRSMjPDysW297Up6r22xw6Ea9AOwQqbNsth8JNgIK8EkWz2IDwaLQcw=="],
"next-runtime-env/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@14.2.28", "", { "os": "linux", "cpu": "arm64" }, "sha512-9ARHLEQXhAilNJ7rgQX8xs9aH3yJSj888ssSjJLeldiZKR4D7N08MfMqljk77fAwZsWwsrp8ohHsMvurvv9liQ=="],
"next-runtime-env/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@14.2.28", "", { "os": "linux", "cpu": "arm64" }, "sha512-p6gvatI1nX41KCizEe6JkF0FS/cEEF0u23vKDpl+WhPe/fCTBeGkEBh7iW2cUM0rvquPVwPWdiUR6Ebr/kQWxQ=="],
"next-runtime-env/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@14.2.28", "", { "os": "linux", "cpu": "x64" }, "sha512-nsiSnz2wO6GwMAX2o0iucONlVL7dNgKUqt/mDTATGO2NY59EO/ZKnKEr80BJFhuA5UC1KZOMblJHWZoqIJddpA=="],
"next-runtime-env/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@14.2.28", "", { "os": "linux", "cpu": "x64" }, "sha512-+IuGQKoI3abrXFqx7GtlvNOpeExUH1mTIqCrh1LGFf8DnlUcTmOOCApEnPJUSLrSbzOdsF2ho2KhnQoO0I1RDw=="],
"next-runtime-env/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@14.2.28", "", { "os": "win32", "cpu": "arm64" }, "sha512-l61WZ3nevt4BAnGksUVFKy2uJP5DPz2E0Ma/Oklvo3sGj9sw3q7vBWONFRgz+ICiHpW5mV+mBrkB3XEubMrKaA=="],
"next-runtime-env/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@14.2.28", "", { "os": "win32", "cpu": "x64" }, "sha512-1gCmpvyhz7DkB1srRItJTnmR2UwQPAUXXIg9r0/56g3O8etGmwlX68skKXJOp9EejW3hhv7nSQUJ2raFiz4MoA=="],
"next-runtime-env/next/@swc/helpers": ["@swc/helpers@0.5.5", "", { "dependencies": { "@swc/counter": "^0.1.3", "tslib": "^2.4.0" } }, "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A=="],
"next-runtime-env/next/caniuse-lite": ["caniuse-lite@1.0.30001718", "", {}, "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw=="],
"next-runtime-env/next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
"next-runtime-env/next/styled-jsx": ["styled-jsx@5.1.1", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" } }, "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw=="],
"novel/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="],
"novel/cmdk/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="],

View File

@@ -3,21 +3,20 @@ services:
build:
context: .
dockerfile: docker/app/Dockerfile
args:
NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL:-http://localhost:3000}
DATABASE_URL: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-zerodotemail}
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:-my-better-auth-secret}
BETTER_AUTH_URL: ${BETTER_AUTH_URL:-http://localhost:3000}
BETTER_AUTH_TRUSTED_ORIGINS: ${BETTER_AUTH_TRUSTED_ORIGINS:-http://localhost:3000}
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET}
REDIS_URL: http://upstash-proxy:80
REDIS_TOKEN: ${REDIS_TOKEN:-upstash-local-token}
RESEND_API_KEY: ${RESEND_API_KEY}
OPENAI_API_KEY: ${OPENAI_API_KEY}
AI_SYSTEM_PROMPT: ${AI_SYSTEM_PROMPT}
GROQ_API_KEY: ${GROQ_API_KEY}
GOOGLE_GENERATIVE_AI_API_KEY: ${GOOGLE_GENERATIVE_AI_API_KEY}
environment:
NEXT_PUBLIC_BACKEND_URL: ${NEXT_PUBLIC_BACKEND_URL:-http://cf-worker.example}
NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL:-http://localhost:3000}
DATABASE_URL: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-zerodotemail}
REDIS_URL: ${REDIS_URL}
REDIS_TOKEN: ${REDIS_TOKEN:-upstash-local-token}
RESEND_API_KEY: ${RESEND_API_KEY}
AI_SYSTEM_PROMPT: ${AI_SYSTEM_PROMPT}
GROQ_API_KEY: ${GROQ_API_KEY}
NEXT_PUBLIC_ELEVENLABS_AGENT_ID: ${NEXT_PUBLIC_ELEVENLABS_AGENT_ID}
NEXT_PUBLIC_IMAGE_PROXY: ${NEXT_PUBLIC_IMAGE_PROXY}
NEXT_PUBLIC_POSTHOG_KEY: ${NEXT_PUBLIC_POSTHOG_KEY}
NEXT_PUBLIC_POSTHOG_HOST: ${NEXT_PUBLIC_POSTHOG_HOST}
NEXT_PUBLIC_IMAGE_API_URL: ${NEXT_PUBLIC_IMAGE_API_URL}
depends_on:
db:
condition: service_healthy
@@ -43,7 +42,8 @@ services:
environment:
- DATABASE_URL=postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-zerodotemail}
depends_on:
- db
db:
condition: service_healthy
command: ['bun', 'run', 'db:migrate']
restart: 'no'

View File

@@ -37,43 +37,13 @@ RUN bun install --omit dev --ignore-scripts
WORKDIR /app/apps/mail
RUN bun install sharp
# Define build arguments
ARG NEXT_PUBLIC_APP_URL \
DATABASE_URL \
BETTER_AUTH_SECRET \
BETTER_AUTH_URL \
BETTER_AUTH_TRUSTED_ORIGINS \
GOOGLE_CLIENT_ID \
GOOGLE_CLIENT_SECRET \
REDIS_URL \
REDIS_TOKEN \
RESEND_API_KEY \
OPENAI_API_KEY \
AI_SYSTEM_PROMPT \
GROQ_API_KEY \
GOOGLE_GENERATIVE_AI_API_KEY
# Set environment variables for build
ENV NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL} \
DATABASE_URL=${DATABASE_URL} \
BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET} \
BETTER_AUTH_URL=${BETTER_AUTH_URL} \
BETTER_AUTH_TRUSTED_ORIGINS=${BETTER_AUTH_TRUSTED_ORIGINS} \
GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID} \
GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET} \
REDIS_URL=${REDIS_URL} \
REDIS_TOKEN=${REDIS_TOKEN} \
RESEND_API_KEY=${RESEND_API_KEY} \
OPENAI_API_KEY=${OPENAI_API_KEY} \
AI_SYSTEM_PROMPT=${AI_SYSTEM_PROMPT} \
GROQ_API_KEY=${GROQ_API_KEY} \
GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY} \
NEXT_TELEMETRY_DISABLED=1 \
ENV NEXT_TELEMETRY_DISABLED=1 \
NODE_ENV=production \
DOCKER_BUILD=true
WORKDIR /app
RUN bun run build
RUN cd apps/mail && bun run build
# ========================================
# Runner Stage: Production Environment
@@ -84,42 +54,16 @@ WORKDIR /app
RUN addgroup -S -g 1001 bunjs && \
adduser -S -u 1001 nextjs -G bunjs
# Define build arguments
ARG NEXT_PUBLIC_APP_URL \
DATABASE_URL \
BETTER_AUTH_SECRET \
BETTER_AUTH_URL \
BETTER_AUTH_TRUSTED_ORIGINS \
GOOGLE_CLIENT_ID \
GOOGLE_CLIENT_SECRET \
REDIS_URL \
REDIS_TOKEN \
RESEND_API_KEY \
OPENAI_API_KEY \
AI_SYSTEM_PROMPT \
GROQ_API_KEY \
GOOGLE_GENERATIVE_AI_API_KEY
# Set environment variables for build
ENV NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL} \
DATABASE_URL=${DATABASE_URL} \
BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET} \
BETTER_AUTH_URL=${BETTER_AUTH_URL} \
BETTER_AUTH_TRUSTED_ORIGINS=${BETTER_AUTH_TRUSTED_ORIGINS} \
GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID} \
GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET} \
REDIS_URL=${REDIS_URL} \
REDIS_TOKEN=${REDIS_TOKEN} \
RESEND_API_KEY=${RESEND_API_KEY} \
OPENAI_API_KEY=${OPENAI_API_KEY} \
AI_SYSTEM_PROMPT=${AI_SYSTEM_PROMPT} \
GROQ_API_KEY=${GROQ_API_KEY} \
GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY} \
NODE_ENV=production \
ENV NODE_ENV=production \
PORT=3000 \
HOSTNAME="0.0.0.0" \
NEXT_TELEMETRY_DISABLED=1
# Copy entrypoint and run it
COPY scripts/docker/ /app/scripts
RUN chmod -R +x /app/scripts/*
# Copy public assets
COPY --from=builder --chown=nextjs:bunjs /app/apps/mail/public ./apps/mail/public
@@ -134,4 +78,4 @@ USER nextjs
EXPOSE 3000
# Start the server
CMD ["bun", "apps/mail/server.js"]
CMD ["/app/scripts/entrypoint.sh"]

View File

@@ -0,0 +1,10 @@
#!/bin/sh
set -x
# Replacing placeholder urls to runtime variables, since we're using rewrites in nextjs, this is required.
# Everything else which doesn't compile URLs at build should already be able to use runtime variables.
/app/scripts/replace-placeholder.sh "http://REPLACE-BACKEND-URL.com" "$NEXT_PUBLIC_BACKEND_URL"
/app/scripts/replace-placeholder.sh "http://REPLACE-APP-URL.com" "$NEXT_PUBLIC_APP_URL"
exec bun /app/apps/mail/server.js

View File

@@ -0,0 +1,36 @@
# The MIT License (MIT)
#
# Copyright (c) Cal.com
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
FROM=$1
TO=$2
if [ "${FROM}" = "${TO}" ]; then
echo "Nothing to replace, the value is already set to ${TO}."
exit 0
fi
# Only peform action if $FROM and $TO are different.
echo "Replacing all statically built instances of $FROM with $TO."
for file in $(egrep -r -l "${FROM}" /app/apps/mail); do
sed -i -e "s|$FROM|$TO|g" "$file"
done