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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions