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
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,15 @@ Frontend environment setup:
- Preferred variable: BACKEND_URL
- Existing deployments may continue using NEXT_PUBLIC_BACKEND_URL as a fallback
- Local development value: http://localhost:8000
- Optional browser-visible health-only variable: NEXT_PUBLIC_BACKEND_HEALTH_URL
(set this to the public backend origin; never include credentials or secrets)

The browser calls the frontend's same-origin `/api/backend` proxy. The proxy
forwards only the supported CalorieApp endpoints to the configured backend and
keeps mobile authentication sessions first-party.
keeps mobile authentication sessions first-party. The Xaman startup flow may
probe the public backend `/health` endpoint directly so a Render cold start does
not occupy the frontend proxy long enough to trigger frontend 429 responses.
All authenticated requests continue through the same-origin proxy.

Xaman sign-in opens in a separate tab while the original CalorieApp tab waits
for a short-lived, one-time browser handoff. Every CalorieApp-owned Xaman login
Expand Down
4 changes: 4 additions & 0 deletions frontend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ BACKEND_URL=http://localhost:8000
# Supported as a deployment-compatible fallback for existing environments.
NEXT_PUBLIC_BACKEND_URL=http://localhost:8000

# Optional direct, public health origin used only to wake a sleeping backend
# without holding the same-origin frontend proxy open. Do not include secrets.
NEXT_PUBLIC_BACKEND_HEALTH_URL=http://localhost:8000

# Optional: frontend-only post-login fallback route if you need custom UX.
# Keep this app-local (starts with /) and do not put secrets here.
# NEXT_PUBLIC_POST_LOGIN_FALLBACK=/
17 changes: 16 additions & 1 deletion frontend/components/XamanLoginPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ type LoginStatusResponse = {
};

const BACKEND_BASE_URL = "/api/backend";
const RENDER_BACKEND_HEALTH_URL =
"https://calorieapp-backend-rvul.onrender.com";
const LOGIN_STATUS_POLL_INTERVAL_MS = 5_000;
const LOGIN_STATUS_FALLBACK_LIFETIME_MS = 5 * 60_000;
const LOGIN_STATUS_RATE_LIMIT_DELAY_MS = 15_000;
Expand All @@ -36,6 +38,19 @@ const LOGIN_START_RETRY_DELAY_MS = 15_000;
const XAMAN_LAUNCH_MESSAGE_TYPE = "calorieapp-xaman-navigate";
const XAMAN_LAUNCH_ERROR_TYPE = "calorieapp-xaman-error";

function backendHealthBaseUrl(): string {
const configuredUrl = process.env.NEXT_PUBLIC_BACKEND_HEALTH_URL?.trim();
if (configuredUrl) {
return configuredUrl.replace(/\/$/, "");
}

if (window.location.hostname === "calorieapp-frontend.onrender.com") {
return RENDER_BACKEND_HEALTH_URL;
}

return BACKEND_BASE_URL;
}

function delay(milliseconds: number, signal: AbortSignal) {
return new Promise<void>((resolve, reject) => {
if (signal.aborted) {
Expand Down Expand Up @@ -261,7 +276,7 @@ export function XamanLoginPanel() {
);

try {
await waitForBackendReady(BACKEND_BASE_URL, controller.signal);
await waitForBackendReady(backendHealthBaseUrl(), controller.signal);
setLoginStatus("Service ready. Opening Xaman...");
Comment thread
xrpbanks marked this conversation as resolved.

const data = await startLoginWithRetry(controller.signal, () => {
Expand Down
4 changes: 2 additions & 2 deletions frontend/lib/backendRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export async function backendRequest(
try {
return await fetch(input, {
...init,
credentials: "include",
credentials: init.credentials ?? "include",
signal: controller.signal,
});
} catch (error) {
Expand Down Expand Up @@ -140,7 +140,7 @@ export async function waitForBackendReady(
try {
const response = await backendRequest(
`${backendBaseUrl}/health`,
{ cache: "no-store", signal },
{ cache: "no-store", credentials: "omit", signal },
attemptTimeoutMs
);

Expand Down