Skip to content

/embed/* routes redirect to /login on self-hosted, breaking iframe embeds #1768

@julianwitzel

Description

@julianwitzel

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

  1. Self-host Cap with NEXT_PUBLIC_IS_CAP unset (or set to anything other than "true")
  2. Make a video public
  3. Copy the embed code from the share page (e.g. <iframe src="https://your-cap.example.com/embed/abc123">)
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions