diff --git a/docs/superpowers/plans/2026-05-06-require-web-login.md b/docs/superpowers/plans/2026-05-06-require-web-login.md
new file mode 100644
index 00000000..f02c15a4
--- /dev/null
+++ b/docs/superpowers/plans/2026-05-06-require-web-login.md
@@ -0,0 +1,203 @@
+# Require Web Login Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Block the self-hosted web app UI until a user is authenticated.
+
+**Architecture:** Add a small web-only root extension component registered by `packages/hoppscotch-selfhost-web/src/main.ts`. The component observes the existing auth platform state and renders a full-screen login gate only for web mode while no confirmed user exists.
+
+**Tech Stack:** Vue 3, Hoppscotch platform auth streams, RxJS `BehaviorSubject`, Vitest for focused state logic.
+
+---
+
+### Task 1: Auth Gate State Logic
+
+**Files:**
+- Create: `packages/hoppscotch-common/src/helpers/appLoginGate.ts`
+- Test: `packages/hoppscotch-common/src/helpers/__tests__/appLoginGate.spec.ts`
+
+- [ ] **Step 1: Write the failing test**
+
+```ts
+import { describe, expect, test } from "vitest"
+import { shouldBlockAppForLogin } from "../appLoginGate"
+
+describe("shouldBlockAppForLogin", () => {
+ const user = {
+ uid: "user-1",
+ displayName: "User",
+ email: "user@example.com",
+ photoURL: null,
+ emailVerified: true,
+ }
+
+ test("blocks the web app while auth is still being checked", () => {
+ expect(
+ shouldBlockAppForLogin({
+ platform: "web",
+ isAuthInitComplete: false,
+ currentUser: null,
+ })
+ ).toBe(true)
+ })
+
+ test("blocks the web app when auth is confirmed anonymous", () => {
+ expect(
+ shouldBlockAppForLogin({
+ platform: "web",
+ isAuthInitComplete: true,
+ currentUser: null,
+ })
+ ).toBe(true)
+ })
+
+ test("does not block the web app once a user is authenticated", () => {
+ expect(
+ shouldBlockAppForLogin({
+ platform: "web",
+ isAuthInitComplete: true,
+ currentUser: user,
+ })
+ ).toBe(false)
+ })
+
+ test("does not block desktop", () => {
+ expect(
+ shouldBlockAppForLogin({
+ platform: "desktop",
+ isAuthInitComplete: true,
+ currentUser: null,
+ })
+ ).toBe(false)
+ })
+})
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `pnpm --dir packages/hoppscotch-common exec vitest --run src/helpers/__tests__/appLoginGate.spec.ts`
+Expected: FAIL because `../appLoginGate` does not exist.
+
+- [ ] **Step 3: Write minimal implementation**
+
+```ts
+type KernelMode = "web" | "desktop"
+
+export type LoginGateState = {
+ platform: KernelMode
+ isAuthInitComplete: boolean
+ currentUser: unknown | null
+}
+
+export function shouldBlockAppForLogin(state: LoginGateState) {
+ return (
+ state.platform === "web" &&
+ (!state.isAuthInitComplete || !state.currentUser)
+ )
+}
+```
+
+- [ ] **Step 4: Run test to verify it passes**
+
+Run: `pnpm --dir packages/hoppscotch-common exec vitest --run src/helpers/__tests__/appLoginGate.spec.ts`
+Expected: PASS.
+
+### Task 2: Web Login Gate UI
+
+**Files:**
+- Create: `packages/hoppscotch-selfhost-web/src/components/WebLoginGate.vue`
+- Create: `packages/hoppscotch-selfhost-web/src/services/webLoginGate.service.ts`
+- Modify: `packages/hoppscotch-selfhost-web/src/main.ts`
+
+- [ ] **Step 1: Create the root UI extension registration service**
+
+Create `packages/hoppscotch-selfhost-web/src/services/webLoginGate.service.ts`:
+
+```ts
+import { Service } from "dioc"
+import { getService } from "@hoppscotch/common/modules/dioc"
+import { UIExtensionService } from "@hoppscotch/common/services/ui-extension.service"
+
+import WebLoginGate from "@app/components/WebLoginGate.vue"
+
+export class WebLoginGateService extends Service {
+ public static readonly ID = "WEB_LOGIN_GATE_SERVICE"
+
+ override onServiceInit() {
+ getService(UIExtensionService).addRootUIExtension(WebLoginGate)
+ }
+}
+```
+
+- [ ] **Step 2: Register the service for web only**
+
+In `packages/hoppscotch-selfhost-web/src/main.ts`, import the service:
+
+```ts
+import { WebLoginGateService } from "@app/services/webLoginGate.service"
+```
+
+Then change:
+
+```ts
+addedServices: [],
+```
+
+to:
+
+```ts
+addedServices: platform === "web" ? [WebLoginGateService] : [],
+```
+
+- [ ] **Step 3: Implement the blocking component**
+
+Create `packages/hoppscotch-selfhost-web/src/components/WebLoginGate.vue`:
+
+```vue
+
+
+
+
+
+
+
+```
+
+- [ ] **Step 4: Run targeted checks**
+
+Run: `pnpm --dir packages/hoppscotch-common exec vitest --run src/helpers/__tests__/appLoginGate.spec.ts`
+Expected: PASS.
+
+Run: `pnpm --dir packages/hoppscotch-selfhost-web run lint`
+Expected: PASS or only pre-existing unrelated failures.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add docs/superpowers/plans/2026-05-06-require-web-login.md packages/hoppscotch-common/src/helpers/appLoginGate.ts packages/hoppscotch-common/src/helpers/__tests__/appLoginGate.spec.ts packages/hoppscotch-selfhost-web/src/components/WebLoginGate.vue packages/hoppscotch-selfhost-web/src/services/webLoginGate.service.ts packages/hoppscotch-selfhost-web/src/main.ts
+git commit -m "feat: require login for web app"
+```
diff --git a/packages/hoppscotch-common/src/helpers/__tests__/appLoginGate.spec.ts b/packages/hoppscotch-common/src/helpers/__tests__/appLoginGate.spec.ts
new file mode 100644
index 00000000..d4076d9f
--- /dev/null
+++ b/packages/hoppscotch-common/src/helpers/__tests__/appLoginGate.spec.ts
@@ -0,0 +1,53 @@
+import { describe, expect, test } from "vitest"
+
+import { shouldBlockAppForLogin } from "../appLoginGate"
+
+describe("shouldBlockAppForLogin", () => {
+ const user = {
+ uid: "user-1",
+ displayName: "User",
+ email: "user@example.com",
+ photoURL: null,
+ emailVerified: true,
+ }
+
+ test("blocks the web app while auth is still being checked", () => {
+ expect(
+ shouldBlockAppForLogin({
+ platform: "web",
+ isAuthInitComplete: false,
+ currentUser: null,
+ })
+ ).toBe(true)
+ })
+
+ test("blocks the web app when auth is confirmed anonymous", () => {
+ expect(
+ shouldBlockAppForLogin({
+ platform: "web",
+ isAuthInitComplete: true,
+ currentUser: null,
+ })
+ ).toBe(true)
+ })
+
+ test("does not block the web app once a user is authenticated", () => {
+ expect(
+ shouldBlockAppForLogin({
+ platform: "web",
+ isAuthInitComplete: true,
+ currentUser: user,
+ })
+ ).toBe(false)
+ })
+
+ test("does not block desktop", () => {
+ expect(
+ shouldBlockAppForLogin({
+ platform: "desktop",
+ isAuthInitComplete: true,
+ currentUser: null,
+ })
+ ).toBe(false)
+ })
+})
diff --git a/packages/hoppscotch-common/src/helpers/appLoginGate.ts b/packages/hoppscotch-common/src/helpers/appLoginGate.ts
new file mode 100644
index 00000000..81fe3d8c
--- /dev/null
+++ b/packages/hoppscotch-common/src/helpers/appLoginGate.ts
@@ -0,0 +1,14 @@
+type KernelMode = "web" | "desktop"
+
+export type LoginGateState = {
+ platform: KernelMode
+ isAuthInitComplete: boolean
+ currentUser: unknown | null
+}
+
+export function shouldBlockAppForLogin(state: LoginGateState) {
+ return (
+ state.platform === "web" &&
+ (!state.isAuthInitComplete || !state.currentUser)
+ )
+}
diff --git a/packages/hoppscotch-selfhost-web/src/components/WebLoginGate.vue b/packages/hoppscotch-selfhost-web/src/components/WebLoginGate.vue
new file mode 100644
index 00000000..6ac47764
--- /dev/null
+++ b/packages/hoppscotch-selfhost-web/src/components/WebLoginGate.vue
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
diff --git a/packages/hoppscotch-selfhost-web/src/main.ts b/packages/hoppscotch-selfhost-web/src/main.ts
index b03f7ee5..c1f3680b 100644
--- a/packages/hoppscotch-selfhost-web/src/main.ts
+++ b/packages/hoppscotch-selfhost-web/src/main.ts
@@ -26,6 +26,7 @@ import { stdSupportOptionItems } from "@hoppscotch/common/platform/std/ui/suppor
import { InfraPlatform } from "@app/platform/infra/infra.platform"
import { kernelIO } from "@hoppscotch/common/platform/std/kernel-io"
import { HeaderDownloadableLinksService } from "@app/services/headerDownloadableLinks.service"
+import { WebLoginGateService } from "@app/services/webLoginGate.service"
import DesktopSettingsSection from "@hoppscotch/common/components/settings/Desktop.vue"
@@ -188,7 +189,7 @@ async function initApp() {
infra: InfraPlatform,
backend: stdBackendDef,
additionalLinks: [HeaderDownloadableLinksService],
- addedServices: [],
+ addedServices: platform === "web" ? [WebLoginGateService] : [],
})
if (platform === "desktop") {
diff --git a/packages/hoppscotch-selfhost-web/src/services/webLoginGate.service.ts b/packages/hoppscotch-selfhost-web/src/services/webLoginGate.service.ts
new file mode 100644
index 00000000..b76d2753
--- /dev/null
+++ b/packages/hoppscotch-selfhost-web/src/services/webLoginGate.service.ts
@@ -0,0 +1,13 @@
+import { Service } from "dioc"
+import { getService } from "@hoppscotch/common/modules/dioc"
+import { UIExtensionService } from "@hoppscotch/common/services/ui-extension.service"
+
+import WebLoginGate from "@app/components/WebLoginGate.vue"
+
+export class WebLoginGateService extends Service {
+ public static readonly ID = "WEB_LOGIN_GATE_SERVICE"
+
+ override onServiceInit() {
+ getService(UIExtensionService).addRootUIExtension(WebLoginGate)
+ }
+}