refactor(web): support same-origin backend and zero urls

This commit is contained in:
Anish Sarkar 2026-06-15 11:03:45 +05:30
parent 2373014943
commit f5d04cf8ba
11 changed files with 62 additions and 36 deletions

View file

@ -7,7 +7,7 @@ import { FAQJsonLd, JsonLd } from "@/components/seo/json-ld";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import type { AnonModel } from "@/contracts/types/anonymous-chat.types";
import { BACKEND_URL } from "@/lib/env-config";
import { SERVER_BACKEND_URL } from "@/lib/env-config";
interface PageProps {
params: Promise<{ model_slug: string }>;
@ -16,7 +16,7 @@ interface PageProps {
async function getModel(slug: string): Promise<AnonModel | null> {
try {
const res = await fetch(
`${BACKEND_URL}/api/v1/public/anon-chat/models/${encodeURIComponent(slug)}`,
`${SERVER_BACKEND_URL}/api/v1/public/anon-chat/models/${encodeURIComponent(slug)}`,
{ next: { revalidate: 300 } }
);
if (!res.ok) return null;
@ -28,7 +28,7 @@ async function getModel(slug: string): Promise<AnonModel | null> {
async function getAllModels(): Promise<AnonModel[]> {
try {
const res = await fetch(`${BACKEND_URL}/api/v1/public/anon-chat/models`, {
const res = await fetch(`${SERVER_BACKEND_URL}/api/v1/public/anon-chat/models`, {
next: { revalidate: 300 },
});
if (!res.ok) return [];
@ -136,7 +136,7 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
export async function generateStaticParams() {
const models = await getAllModels();
return models.filter((m) => m.seo_slug).map((m) => ({ model_slug: m.seo_slug! }));
return models.flatMap((m) => (m.seo_slug ? [{ model_slug: m.seo_slug }] : []));
}
export default async function FreeModelPage({ params }: PageProps) {

View file

@ -16,7 +16,7 @@ import {
TableRow,
} from "@/components/ui/table";
import type { AnonModel } from "@/contracts/types/anonymous-chat.types";
import { BACKEND_URL } from "@/lib/env-config";
import { SERVER_BACKEND_URL } from "@/lib/env-config";
export const metadata: Metadata = {
title: "Free AI Chat, No Login Required | SurfSense",
@ -94,7 +94,7 @@ export const metadata: Metadata = {
async function getModels(): Promise<AnonModel[]> {
try {
const res = await fetch(`${BACKEND_URL}/api/v1/public/anon-chat/models`, {
const res = await fetch(`${SERVER_BACKEND_URL}/api/v1/public/anon-chat/models`, {
next: { revalidate: 300 },
});
if (!res.ok) return [];

View file

@ -1,7 +1,7 @@
import { mustGetQuery } from "@rocicorp/zero";
import { handleQueryRequest } from "@rocicorp/zero/server";
import { NextResponse } from "next/server";
import { BACKEND_URL } from "@/lib/env-config";
import { SERVER_BACKEND_URL } from "@/lib/env-config";
import type { Context } from "@/types/zero";
import { queries } from "@/zero/queries";
import { schema } from "@/zero/schema";
@ -11,11 +11,7 @@ import { schema } from "@/zero/schema";
// (e.g. http://backend:8000). The browser-facing NEXT_PUBLIC_FASTAPI_BACKEND_URL
// (e.g. http://localhost:8929) does NOT resolve from inside the frontend
// container and would make every authenticated Zero query fail with a 503.
const backendURL = (
process.env.FASTAPI_BACKEND_INTERNAL_URL ||
process.env.BACKEND_URL ||
"http://localhost:8000"
).replace(/\/$/, "");
const backendURL = SERVER_BACKEND_URL.replace(/\/$/, "");
async function authenticateRequest(
request: Request

View file

@ -2,6 +2,7 @@ import { loader } from "fumadocs-core/source";
import type { MetadataRoute } from "next";
import { blog, changelog } from "@/.source/server";
import { source as docsSource } from "@/lib/source";
import { SERVER_BACKEND_URL } from "@/lib/env-config";
const blogSource = loader({
baseUrl: "/blog",
@ -14,11 +15,10 @@ const changelogSource = loader({
});
const BASE_URL = "https://www.surfsense.com";
const BACKEND_URL = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000";
async function getFreeModelSlugs(): Promise<string[]> {
try {
const res = await fetch(`${BACKEND_URL}/api/v1/public/anon-chat/models`, {
const res = await fetch(`${SERVER_BACKEND_URL}/api/v1/public/anon-chat/models`, {
next: { revalidate: 3600 },
});
if (!res.ok) return [];

View file

@ -1,6 +1,7 @@
"use client";
import { authenticatedFetch } from "@/lib/auth-utils";
import { BACKEND_URL } from "@/lib/env-config";
export type MemoryScope = "user" | "team";
@ -30,7 +31,7 @@ function getMemoryPath(scope: MemoryScope, searchSpaceId?: number | null) {
}
function getBackendUrl(path: string) {
return `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}${path}`;
return `${BACKEND_URL}${path}`;
}
export function getMemoryLimitState(length: number, limits?: MemoryLimits | null) {

View file

@ -820,8 +820,8 @@ function AuthenticatedDocumentsSidebarBase({
try {
const endpoint =
doc.document_type === "USER_MEMORY"
? `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/users/me/memory`
: `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}/memory`;
? `${BACKEND_URL}/api/v1/users/me/memory`
: `${BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}/memory`;
const response = await authenticatedFetch(endpoint, { method: "GET" });
if (!response.ok) {
const errorData = await response.json().catch(() => ({ detail: "Export failed" }));
@ -1028,8 +1028,8 @@ function AuthenticatedDocumentsSidebarBase({
}
const endpoint =
doc.document_type === "USER_MEMORY"
? `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/users/me/memory/reset`
: `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}/memory/reset`;
? `${BACKEND_URL}/api/v1/users/me/memory/reset`
: `${BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}/memory/reset`;
try {
const response = await authenticatedFetch(endpoint, { method: "POST" });
if (!response.ok) {

View file

@ -12,7 +12,15 @@ import { getBearerToken, handleUnauthorized, refreshAccessToken } from "@/lib/au
import { queries } from "@/zero/queries";
import { schema } from "@/zero/schema";
const cacheURL = process.env.NEXT_PUBLIC_ZERO_CACHE_URL || "http://localhost:4848";
const configuredCacheURL = process.env.NEXT_PUBLIC_ZERO_CACHE_URL;
function getCacheURL() {
if (configuredCacheURL) return configuredCacheURL;
if (typeof window !== "undefined") {
return `${window.location.origin}/zero`;
}
return "http://localhost:4848";
}
function ZeroAuthSync() {
const zero = useZero();
@ -42,6 +50,7 @@ function ZeroAuthSync() {
export function ZeroProvider({ children }: { children: React.ReactNode }) {
const { data: user } = useAtomValue(currentUserAtom);
const cacheURL = useMemo(() => getCacheURL(), []);
const userId = user?.id;
const hasUser = !!userId;
@ -65,7 +74,7 @@ export function ZeroProvider({ children }: { children: React.ReactNode }) {
cacheURL,
auth,
}),
[userID, context, auth]
[userID, context, cacheURL, auth]
);
return (

View file

@ -12,21 +12,31 @@
const fs = require("fs");
const path = require("path");
function envValue(name, fallback, { allowEmpty = false } = {}) {
if (Object.hasOwn(process.env, name)) {
const value = process.env[name];
if (allowEmpty || value) {
return value ?? "";
}
}
return fallback;
}
const replacements = [
[
"__NEXT_PUBLIC_FASTAPI_BACKEND_URL__",
process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000",
envValue("NEXT_PUBLIC_FASTAPI_BACKEND_URL", "http://localhost:8000", { allowEmpty: true }),
],
[
"__NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE__",
process.env.NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE || "LOCAL",
envValue("NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE", "LOCAL"),
],
["__NEXT_PUBLIC_ETL_SERVICE__", process.env.NEXT_PUBLIC_ETL_SERVICE || "DOCLING"],
["__NEXT_PUBLIC_ETL_SERVICE__", envValue("NEXT_PUBLIC_ETL_SERVICE", "DOCLING")],
[
"__NEXT_PUBLIC_ZERO_CACHE_URL__",
process.env.NEXT_PUBLIC_ZERO_CACHE_URL || "http://localhost:4848",
envValue("NEXT_PUBLIC_ZERO_CACHE_URL", "http://localhost:4848", { allowEmpty: true }),
],
["__NEXT_PUBLIC_DEPLOYMENT_MODE__", process.env.NEXT_PUBLIC_DEPLOYMENT_MODE || "self-hosted"],
["__NEXT_PUBLIC_DEPLOYMENT_MODE__", envValue("NEXT_PUBLIC_DEPLOYMENT_MODE", "self-hosted")],
];
let filesProcessed = 0;

View file

@ -93,11 +93,6 @@ class BaseApiService {
},
};
// Validate the base URL
if (!this.baseUrl) {
throw new AppError("Base URL is not set.");
}
// Validate the bearer token
const isNoAuthEndpoint =
this.noAuthEndpoints.includes(url) ||
@ -107,8 +102,8 @@ class BaseApiService {
throw new AuthenticationError("You are not authenticated. Please login again.");
}
// Construct the full URL
const fullUrl = new URL(url, this.baseUrl).toString();
// Construct the full URL. Empty baseUrl is valid for same-origin proxy mode.
const fullUrl = this.baseUrl ? new URL(url, this.baseUrl).toString() : url;
// Prepare fetch options
const fetchOptions: RequestInit = {

View file

@ -15,9 +15,18 @@ import packageJson from "../package.json";
// Placeholder: __NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE__
export const AUTH_TYPE = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE || "GOOGLE";
// Backend API URL
// Backend API URL. An empty string is valid in proxy mode and means
// same-origin relative requests (e.g. /api/v1/... and /auth/...).
// Placeholder: __NEXT_PUBLIC_FASTAPI_BACKEND_URL__
export const BACKEND_URL = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000";
export const BACKEND_URL = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL ?? "http://localhost:8000";
// Server-side backend URL. Relative browser URLs do not work from RSC/API route
// code, so server callers should use Docker DNS or an explicit public backend.
export const SERVER_BACKEND_URL =
process.env.FASTAPI_BACKEND_INTERNAL_URL ||
process.env.BACKEND_URL ||
BACKEND_URL ||
"http://localhost:8000";
// ETL Service: "DOCLING", "UNSTRUCTURED", or "LLAMACLOUD"
// Placeholder: __NEXT_PUBLIC_ETL_SERVICE__

View file

@ -2,12 +2,17 @@ import { defineConfig, devices } from "@playwright/test";
const PORT = process.env.PORT || "3000";
const BACKEND_PORT = process.env.BACKEND_PORT || "8000";
const ZERO_CACHE_PORT = process.env.ZERO_CACHE_PORT || "4848";
const baseURL = process.env.PLAYWRIGHT_BASE_URL || `http://localhost:${PORT}`;
const useProxyOrigin = process.env.PLAYWRIGHT_USE_PROXY_ORIGIN === "true";
const backendURL = useProxyOrigin ? baseURL : `http://localhost:${BACKEND_PORT}`;
const zeroCacheURL = useProxyOrigin ? `${baseURL}/zero` : `http://localhost:${ZERO_CACHE_PORT}`;
process.env.PLAYWRIGHT_TEST_EMAIL ??= "e2e-test@surfsense.net";
process.env.PLAYWRIGHT_TEST_PASSWORD ??= "E2eTestPassword123!";
process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL ??= `http://localhost:${BACKEND_PORT}`;
process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL ??= backendURL;
process.env.NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE ??= "LOCAL";
process.env.NEXT_PUBLIC_ZERO_CACHE_URL ??= zeroCacheURL;
/**
* Playwright configuration for SurfSense web E2E tests.
@ -68,6 +73,7 @@ export default defineConfig({
env: {
NEXT_PUBLIC_FASTAPI_BACKEND_URL: process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL,
NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE: process.env.NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE,
NEXT_PUBLIC_ZERO_CACHE_URL: process.env.NEXT_PUBLIC_ZERO_CACHE_URL,
},
},
});