Description
On self-hosted deployments, the /embed/<videoId> route incorrectly redirects unauthenticated visitors to /login, making iframe embeds unusable. The /s/<videoId> share-page route works correctly because it's whitelisted; /embed/* was apparently overlooked.
Reproduction
- Self-host Cap with
NEXT_PUBLIC_IS_CAP unset (or set to anything other than "true")
- Make a video public
- Copy the embed code from the share page (e.g.
<iframe src="https://your-cap.example.com/embed/abc123">)
- Open the iframe URL directly in a new private/incognito tab — observe the redirect to
/login
Expected: The embed page renders the video player, regardless of authentication state, when the underlying video is public — same as /s/<videoId> does today.
Actual: Server returns 307 Location: /login. The iframe loads the login page (or, if the user is logged in, the dashboard, depending on auth state).
Root cause
In apps/web/proxy.ts (lines 38-56), the self-hosted whitelist allows /s/, /dashboard, /api, /login, /signup, /invite, /self-hosting, /terms, /verify-otp — but not /embed/:
if (buildEnv.NEXT_PUBLIC_IS_CAP !== "true") {
if (
!(
path.startsWith("/s/") ||
path.startsWith("/middleware") ||
path.startsWith("/dashboard") ||
path.startsWith("/onboarding") ||
path.startsWith("/api") ||
path.startsWith("/login") ||
path.startsWith("/signup") ||
path.startsWith("/invite") ||
path.startsWith("/self-hosting") ||
path.startsWith("/terms") ||
path.startsWith("/verify-otp")
// /embed missing here
) &&
process.env.NODE_ENV !== "development"
)
return NextResponse.redirect(new URL("/login", url.origin));
The /embed/[videoId]/page.tsx itself uses provideOptionalAuth and gracefully handles missing auth, so the page is built correctly — it's just unreachable due to the proxy redirect.
Fix
Add path.startsWith("/embed/") to the whitelist. One-line change.
Verified via
curl -i https://my-cap.example.com/embed/<videoId> returns 307 Location: /login
- Same behavior with direct IP via
--resolve, ruling out Cloudflare/CDN
- Same behavior in private browser tabs
Environment
- Cap version:
ghcr.io/capsoftware/cap-web:latest (current as of 2026-04-28)
- Configuration: standard self-hosted Docker compose
- All other features working: Studio Mode recording, share-page playback, auth via Google OAuth + magic link
Description
On self-hosted deployments, the
/embed/<videoId>route incorrectly redirects unauthenticated visitors to/login, making iframe embeds unusable. The/s/<videoId>share-page route works correctly because it's whitelisted;/embed/*was apparently overlooked.Reproduction
NEXT_PUBLIC_IS_CAPunset (or set to anything other than"true")<iframe src="https://your-cap.example.com/embed/abc123">)/loginExpected: The embed page renders the video player, regardless of authentication state, when the underlying video is public — same as
/s/<videoId>does today.Actual: Server returns
307 Location: /login. The iframe loads the login page (or, if the user is logged in, the dashboard, depending on auth state).Root cause
In
apps/web/proxy.ts(lines 38-56), the self-hosted whitelist allows/s/,/dashboard,/api,/login,/signup,/invite,/self-hosting,/terms,/verify-otp— but not/embed/:The
/embed/[videoId]/page.tsxitself usesprovideOptionalAuthand gracefully handles missing auth, so the page is built correctly — it's just unreachable due to the proxy redirect.Fix
Add
path.startsWith("/embed/")to the whitelist. One-line change.Verified via
curl -i https://my-cap.example.com/embed/<videoId>returns307 Location: /login--resolve, ruling out Cloudflare/CDNEnvironment
ghcr.io/capsoftware/cap-web:latest(current as of 2026-04-28)