diff --git a/docker/.env.example b/docker/.env.example
index 625cb48d5..b9d544f97 100644
--- a/docker/.env.example
+++ b/docker/.env.example
@@ -93,6 +93,10 @@ CERT_EMAIL=
# If a CDN/load balancer sits in front of Caddy, narrow this to that proxy's CIDRs.
# TRUSTED_PROXIES=0.0.0.0/0
# SURFSENSE_MAX_BODY_SIZE=5GB
+# Per-file cap for authenticated document uploads (MB). Keep it at or below
+# SURFSENSE_MAX_BODY_SIZE above, which limits the whole request at the proxy.
+# Passed to both the backend (enforcement) and the frontend (pre-upload check).
+# MAX_FILE_SIZE_MB=500
#
# Browser API and Zero URLs are same-origin relative behind bundled Caddy.
# Next.js server-side calls use Docker DNS through SURFSENSE_BACKEND_INTERNAL_URL
diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml
index 03c46f6d2..2995a4ab8 100644
--- a/docker/docker-compose.dev.yml
+++ b/docker/docker-compose.dev.yml
@@ -127,6 +127,7 @@ services:
- NEXT_FRONTEND_URL=${NEXT_FRONTEND_URL:-http://localhost:3000}
- WHATSAPP_BRIDGE_URL=${WHATSAPP_BRIDGE_URL:-http://whatsapp-bridge:9929}
- SEARXNG_URL=${SEARXNG_URL:-http://searxng:8080}
+ - MAX_FILE_SIZE_MB=${MAX_FILE_SIZE_MB:-500}
# Daytona Sandbox – uncomment and set credentials to enable cloud code execution
# - DAYTONA_SANDBOX_ENABLED=TRUE
# - DAYTONA_API_KEY=${DAYTONA_API_KEY:-}
@@ -268,6 +269,7 @@ services:
ETL_SERVICE: ${ETL_SERVICE:-DOCLING}
DEPLOYMENT_MODE: ${DEPLOYMENT_MODE:-self-hosted}
SURFSENSE_BACKEND_INTERNAL_URL: http://backend:8000
+ MAX_FILE_SIZE_MB: ${MAX_FILE_SIZE_MB:-500}
depends_on:
backend:
condition: service_healthy
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 2b3f8ece2..71a000a6a 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -148,6 +148,7 @@ services:
BACKEND_URL: ${BACKEND_URL:-${SURFSENSE_PUBLIC_URL:-http://localhost:${LISTEN_HTTP_PORT:-3929}}}
WHATSAPP_BRIDGE_URL: ${WHATSAPP_BRIDGE_URL:-http://whatsapp-bridge:9929}
SEARXNG_URL: ${SEARXNG_URL:-http://searxng:8080}
+ MAX_FILE_SIZE_MB: ${MAX_FILE_SIZE_MB:-500}
# Daytona Sandbox – uncomment and set credentials to enable cloud code execution
# DAYTONA_SANDBOX_ENABLED: "TRUE"
# DAYTONA_API_KEY: ${DAYTONA_API_KEY:-}
@@ -295,6 +296,7 @@ services:
ETL_SERVICE: ${ETL_SERVICE:-DOCLING}
DEPLOYMENT_MODE: ${DEPLOYMENT_MODE:-self-hosted}
SURFSENSE_BACKEND_INTERNAL_URL: http://backend:8000
+ MAX_FILE_SIZE_MB: ${MAX_FILE_SIZE_MB:-500}
labels:
- "com.centurylinklabs.watchtower.enable=true"
depends_on:
diff --git a/surfsense_backend/app/routes/documents_routes.py b/surfsense_backend/app/routes/documents_routes.py
index 574bf78b5..fdd8df7a2 100644
--- a/surfsense_backend/app/routes/documents_routes.py
+++ b/surfsense_backend/app/routes/documents_routes.py
@@ -56,7 +56,10 @@ logger = logging.getLogger(__name__)
router = APIRouter()
-MAX_FILE_SIZE_BYTES = 500 * 1024 * 1024 # 500 MB per file
+# Per-file upload cap. Operators raise MAX_FILE_SIZE_MB when self-hosting on
+# hardware that can take it; the frontend reads the same value for its
+# pre-upload check.
+MAX_FILE_SIZE_BYTES = int(os.getenv("MAX_FILE_SIZE_MB", "500")) * 1024 * 1024
@router.post("/documents")
diff --git a/surfsense_web/components/providers/runtime-config.server.tsx b/surfsense_web/components/providers/runtime-config.server.tsx
index c515820c2..3ad9bfb6f 100644
--- a/surfsense_web/components/providers/runtime-config.server.tsx
+++ b/surfsense_web/components/providers/runtime-config.server.tsx
@@ -13,6 +13,7 @@ export async function RuntimeConfig({ children }: { children: React.ReactNode })
authType: process.env.AUTH_TYPE ?? BUILD_TIME_AUTH_TYPE,
etlService: process.env.ETL_SERVICE ?? BUILD_TIME_ETL_SERVICE,
deploymentMode: process.env.DEPLOYMENT_MODE ?? BUILD_TIME_DEPLOYMENT_MODE,
+ maxFileSizeMB: Number(process.env.MAX_FILE_SIZE_MB ?? "500")
};
return {children};
diff --git a/surfsense_web/components/providers/runtime-config.tsx b/surfsense_web/components/providers/runtime-config.tsx
index 574573f6f..e64eda298 100644
--- a/surfsense_web/components/providers/runtime-config.tsx
+++ b/surfsense_web/components/providers/runtime-config.tsx
@@ -9,6 +9,7 @@ export interface RuntimeConfigValue {
authType: AuthType;
etlService: string;
deploymentMode: DeploymentMode;
+ maxFileSizeMB: number;
}
const RuntimeConfigContext = createContext(null);
diff --git a/surfsense_web/components/sources/DocumentUploadTab.tsx b/surfsense_web/components/sources/DocumentUploadTab.tsx
index 9ce27ff11..dced0e78d 100644
--- a/surfsense_web/components/sources/DocumentUploadTab.tsx
+++ b/surfsense_web/components/sources/DocumentUploadTab.tsx
@@ -121,8 +121,7 @@ function flattenTree(
const FOLDER_BATCH_SIZE_BYTES = 20 * 1024 * 1024;
const FOLDER_BATCH_MAX_FILES = 10;
-const MAX_FILE_SIZE_MB = 500;
-const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;
+
const toggleRowClass =
"flex items-center justify-between rounded-lg bg-slate-400/5 dark:bg-white/5 p-3";
@@ -133,7 +132,7 @@ export function DocumentUploadTab({
onAccordionStateChange,
}: DocumentUploadTabProps) {
const t = useTranslations("upload_documents");
- const { etlService } = useRuntimeConfig();
+ const { etlService, maxFileSizeMB } = useRuntimeConfig();
const [files, setFiles] = useState([]);
const [uploadProgress, setUploadProgress] = useState(0);
const [accordionValue, setAccordionValue] = useState("");
@@ -146,7 +145,8 @@ export function DocumentUploadTab({
const progressIntervalRef = useRef | null>(null);
const [folderUpload, setFolderUpload] = useState(null);
const [isFolderUploading, setIsFolderUploading] = useState(false);
-
+ const MAX_FILE_SIZE_BYTES = maxFileSizeMB * 1024 * 1024;
+
useEffect(() => {
return () => {
if (progressIntervalRef.current) {
@@ -175,7 +175,7 @@ export function DocumentUploadTab({
toast.error(t("file_too_large"), {
description: t("file_too_large_desc", {
name: oversized[0].name,
- maxMB: MAX_FILE_SIZE_MB,
+ maxMB: maxFileSizeMB,
}),
});
}
@@ -191,7 +191,7 @@ export function DocumentUploadTab({
return [...prev, ...newEntries];
});
},
- [t]
+ [t, MAX_FILE_SIZE_BYTES, maxFileSizeMB]
);
const onDrop = useCallback(