Skip to content

feat: hosted demo mode#172

Merged
TerrifiedBug merged 11 commits intomainfrom
feat/demo-mode
Apr 25, 2026
Merged

feat: hosted demo mode#172
TerrifiedBug merged 11 commits intomainfrom
feat/demo-mode

Conversation

@TerrifiedBug
Copy link
Copy Markdown
Owner

Summary

Adds NEXT_PUBLIC_VF_DEMO_MODE env flag and demo-mode UI behaviors so the same VectorFlow image can be built as a public hosted demo at demo.terrifiedbug.com. Implements design at docs/superpowers/specs/2026-04-25-vf-demo-design.md (gitignored, in main worktree).

What's included

  • NEXT_PUBLIC_VF_DEMO_MODE env flag (build-time inlined so client components see it consistently — avoids hydration mismatch)
  • isDemoMode() helper at src/lib/is-demo-mode.ts reading process.env.NEXT_PUBLIC_VF_DEMO_MODE directly (works in both server and client components)
  • Top banner across all dashboard pages: "Public demo — resets nightly at 03:00 UTC. [Get VectorFlow on GitHub →]"
  • Login pre-fill via ?prefill=demo query param (URL-gated only — no-op in production because the demo user doesn't exist there)
  • Telemetry sender returns immediately when demo mode active (regardless of DB state — protects Pulse from demo skew)
  • 4 settings sub-pages (/settings/telemetry, /settings/service-accounts, /settings/team, /settings/users) return 404 when demo mode is active. Each refactored into a server page.tsx + client _client.tsx so the guard cannot be bypassed.
  • 3 nav entries hidden when demo mode active (corresponding to the 3 of the 4 guarded routes that actually have nav entries)
  • Sign-out button hidden in 3 places (main user dropdown, teamless dropdown, teamless standalone button)
  • README adds inline 🌐 [Try the live demo →] CTA above the existing hero

Behaviour

  • Production unchanged — NEXT_PUBLIC_VF_DEMO_MODE defaults to false / unset
  • Demo image built with NEXT_PUBLIC_VF_DEMO_MODE=true shows banner, hides admin nav + sign-out, 404s admin settings, and never pings telemetry
  • Visiting demo.terrifiedbug.com/ → Pangolin redirects to /login?prefill=demo → form pre-filled → one-click sign-in into a seeded demo dashboard

Important architecture note

NEXT_PUBLIC_* env vars are inlined at BUILD time in Next.js, not read at runtime. This means: a single VF image cannot be both prod and demo at runtime via env. The demo image must be built with the flag set. Trade-off accepted for the demo MVP — Stream B's vectorflow-demo-ops repo will build VF from a sibling clone with the flag set, OR consume a separately-tagged demo image if VF's CI grows that capability.

Test plan

  • CI green — currently 2526 tests pass + 5 pre-existing skips, build + lint clean
  • Production default (NEXT_PUBLIC_VF_DEMO_MODE unset): no banner, all settings pages reachable, telemetry behaves normally, login form not pre-filled
  • Demo build (NEXT_PUBLIC_VF_DEMO_MODE=true at build time): banner renders on every dashboard page, /settings/telemetry|service-accounts|team|users return 404, sign-out button hidden in all 3 surfaces, telemetry heartbeat does not fire even if telemetryEnabled=true in DB, demo nav links hidden in sidebar
  • ?prefill=demo query param on login page populates email and password fields with demo@demo.local / demo (works on both prod and demo builds — auth fails on prod because user doesn't exist)
  • Login submits via standard NextAuth flow (no auth-path divergence)
  • No hydration mismatch — banner, nav, sign-out are stable across server render → client hydration

Out of scope

  • The seed data, reset cron, and vectorflow-demo-ops repo (Stream B, separate private repo)
  • Per-session demo isolation
  • "Reset now" button or /api/demo/reset endpoint
  • Demo-specific custom branding
  • Playwright E2E coverage for demo mode (manual QA pre-merge sufficient)

Pre-existing CI status (not caused by this branch)

Same baseline as main: 5 pre-existing skipped tests in dlp-vrl-integration.test.ts (Vector binary version) and agent-token.test.ts (bcrypt timeout) — both already worked around in main via describe.skipIf and per-test timeouts.

When ?prefill=demo is present in the URL, the email and password inputs
are populated with demo@demo.local / demo via react-hook-form defaultValues.
Gated only on the URL param — not on VF_DEMO_MODE — so it works on any instance.
Wrap telemetry, service-accounts, team, and users settings pages with a
server-component guard that calls notFound() when VF_DEMO_MODE is true.

All four pages were "use client" components, so each page.tsx is replaced
with a thin async server wrapper and the original client component is kept
as _client.tsx.
Rename VF_DEMO_MODE → NEXT_PUBLIC_VF_DEMO_MODE so Next.js inlines the
value into the client bundle at build time, eliminating the hydration
mismatch where the server rendered with demo-mode on but the client
hydrated with false (non-NEXT_PUBLIC_ vars are not available in the
client bundle).

is-demo-mode.ts now reads process.env.NEXT_PUBLIC_VF_DEMO_MODE directly
instead of going through the server-only env module. Tests updated to
use the new var name, the bare delete replaced with vi.stubEnv("", "")
so vi.unstubAllEnvs() restores it correctly, and the telemetry demo-mode
test gains an assertion that the DB was not queried (the early return is
before the prisma call).
@github-actions github-actions Bot added feature documentation Improvements or additions to documentation labels Apr 25, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 25, 2026

Greptile Summary

This PR adds a NEXT_PUBLIC_VF_DEMO_MODE build-time flag and the corresponding UI behaviors for a public hosted demo at demo.terrifiedbug.com: a top banner, login pre-fill via ?prefill=demo, telemetry short-circuit, four settings pages 404-gated with server-component guards, nav links hidden, and sign-out hidden in all three surfaces. Production is unaffected when the flag is unset. The implementation is clean and consistent — the server-component notFound() pattern is a solid guard that cannot be bypassed client-side.

Confidence Score: 4/5

Safe to merge; one P2 Zod schema inconsistency is worth fixing but is not a blocker.

Only P2 findings present. The implementation is well-structured with no auth bypasses, no hydration mismatches, and correct server-side guards. The single comment is a minor Zod enum strictness issue that would surface only when the env var is set to an empty string.

src/lib/env.ts — the NEXT_PUBLIC_VF_DEMO_MODE Zod enum entry.

Important Files Changed

Filename Overview
src/lib/is-demo-mode.ts Tiny helper that reads NEXT_PUBLIC_VF_DEMO_MODE directly from process.env — correct for both server and client (inlined at build time by Next.js).
src/lib/env.ts Adds NEXT_PUBLIC_VF_DEMO_MODE to the Zod schema; enum restricts to "true"
src/server/services/telemetry-sender.ts Adds isDemoMode() early-return guard before DB read or HTTP send — clean and correct; test verifies DB is never queried.
src/app/(dashboard)/settings/service-accounts/page.tsx Converted to async server component with notFound() guard; demo guard cannot be bypassed client-side.
src/app/(dashboard)/layout.tsx Adds DemoBanner at top level outside SidebarProvider and hides sign-out in all three dropdown locations; NEXT_PUBLIC_ var is build-time inlined so no hydration mismatch risk.
src/app/(auth)/login/page.tsx ?prefill=demo populates form defaults; no auth-path divergence — NextAuth still validates credentials normally.
src/components/app-sidebar.tsx Filters nav items with demoHidden:true when isDemoMode() is true; called once per group render, correct.
src/components/dashboard/demo-banner.tsx Simple banner component rendered only when isDemoMode() is true; no issues.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    BUILD["Build image\nNEXT_PUBLIC_VF_DEMO_MODE=true"]
    PROD["Production image\n(flag unset)"]

    BUILD --> BANNER["DemoBanner rendered\nin dashboard layout"]
    BUILD --> TELEMETRY["sendTelemetryHeartbeat\nreturns immediately"]
    BUILD --> NAV["demoHidden nav items\nfiltered from sidebar"]
    BUILD --> SIGNOUT["Sign-out button\nhidden in 3 locations"]
    BUILD --> GUARD["Settings pages\nnotFound() on server"]

    GUARD --> P404_T["/settings/telemetry → 404"]
    GUARD --> P404_SA["/settings/service-accounts → 404"]
    GUARD --> P404_TM["/settings/team → 404"]
    GUARD --> P404_U["/settings/users → 404"]

    PROD --> NOOP["All behaviors disabled\nno visible change"]

    LOGIN["GET /login?prefill=demo"]
    LOGIN --> PREFILL["Form pre-filled\ndemo@demo.local / demo"]
    PREFILL --> AUTH["NextAuth credentials flow\n(unchanged)"]
    AUTH -->|user exists| OK["Authenticated"]
    AUTH -->|user missing| FAIL["Auth failure\n(prod instance)"]
Loading

Comments Outside Diff (1)

  1. src/lib/env.ts, line 1958-1961 (link)

    P2 Zod enum rejects empty-string env var, causing startup crash

    z.enum(["true", "false"]) only accepts those two exact strings; it does not treat "" the same as undefined. If a deployment defines NEXT_PUBLIC_VF_DEMO_MODE= (empty string — common in Docker Compose when a key is declared but has no value), Zod throws a validation error and the app fails to start. isDemoMode() itself would handle "" gracefully (returns false), but the schema here would crash first.

    Switching to z.string() with a regex or just comparing after coercion avoids this:

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/lib/env.ts
    Line: 1958-1961
    
    Comment:
    **Zod enum rejects empty-string env var, causing startup crash**
    
    `z.enum(["true", "false"])` only accepts those two exact strings; it does **not** treat `""` the same as `undefined`. If a deployment defines `NEXT_PUBLIC_VF_DEMO_MODE=` (empty string — common in Docker Compose when a key is declared but has no value), Zod throws a validation error and the app fails to start. `isDemoMode()` itself would handle `""` gracefully (returns `false`), but the schema here would crash first.
    
    Switching to `z.string()` with a regex or just comparing after coercion avoids this:
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/lib/env.ts
Line: 1958-1961

Comment:
**Zod enum rejects empty-string env var, causing startup crash**

`z.enum(["true", "false"])` only accepts those two exact strings; it does **not** treat `""` the same as `undefined`. If a deployment defines `NEXT_PUBLIC_VF_DEMO_MODE=` (empty string — common in Docker Compose when a key is declared but has no value), Zod throws a validation error and the app fails to start. `isDemoMode()` itself would handle `""` gracefully (returns `false`), but the schema here would crash first.

Switching to `z.string()` with a regex or just comparing after coercion avoids this:

```suggestion
  NEXT_PUBLIC_VF_DEMO_MODE: z
    .string()
    .default("false")
    .transform((v) => v === "true"),
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(demo): use NEXT_PUBLIC_VF_DEMO_MODE ..." | Re-trigger Greptile

@TerrifiedBug TerrifiedBug merged commit c7ac7ef into main Apr 25, 2026
10 checks passed
@TerrifiedBug TerrifiedBug deleted the feat/demo-mode branch April 25, 2026 22:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant