Docs/Reference

Config reference: .demovie/config.json

npx demovie init writes this file; edit it freely. The JSON Schema ships with the package, and $schema points at it (../node_modules/demovie/schema/config.schema.json when demovie is installed in the project, otherwise https://unpkg.com/demovie@<version>/schema/config.schema.json), so editors validate and autocomplete it.

  • Any string of the form "$env:NAME" resolves from the environment or .demovie/.env (gitignored).
  • Secrets must only appear as $env: references.
  • In CI, DEMOVIE_APP_URL, DEMOVIE_APP_START and DEMOVIE_APP_HEADERS override app (see CI).
KeyTypeDefaultDescription
versionnumberconfig format version
projectobjectthe app demovie works on
project.namestringproduct name used in videos
project.framework"nextjs" | "generic"nextjs: routes from the file system; generic: crawl app.url
project.rootstring"."the app folder, relative to the folder that holds .demovie/
project.nextjsobject | nullwhat init detected (Next.js only)
project.nextjs.versionstring | nulldetected Next.js version
project.nextjs.router"app" | "pages" | "both"which routers the app uses
project.nextjs.appDirstring | nullApp Router folder, e.g. src/app
project.nextjs.pagesDirstring | nullPages Router folder, e.g. pages
project.nextjs.basePathstring | nullnext.config basePath
project.nextjs.i18nobject | nullnext.config i18n (Pages Router)
project.nextjs.i18n.localesstring[]
project.nextjs.i18n.defaultLocalestring | null
project.nextjs.outputstring | nullnext.config output, e.g. "standalone"
appobjecthow to reach and start the app
app.urlstringwhere the running app answers
app.startobject | nullnullhow up and capture start the app; null when you start it yourself
app.start.commandstringstarts the app, e.g. pnpm dev
app.start.cwdstring"."folder to run it in, relative to the project
app.start.envrecord<string, string>{}extra environment variables
app.start.readyPathstring"/"path polled until the app answers
app.start.timeoutMsinteger120000how long to wait for readyPath
app.reuseRunningbooleantrueuse the app if it already answers at url
app.headersrecord<string, string>{}extra HTTP headers; secrets as "$env:NAME" references
demoobject{}demo mode: seeded data, frozen clock, hidden and masked content
demo.seedstring | nullnulloptional seed command; runs before start/capture
demo.nowstring | nullnullfrozen page clock for captures
demo.timezonestring"UTC"browser timezone for captures
demo.localestring"en-US"browser locale for captures
demo.colorScheme"light" | "dark" | "both""light"color schemes to capture
demo.hidestring[][]CSS selectors hidden before screenshots (cookie banners, chat)
demo.blockRequestsstring[]see exampleURL patterns (* wildcards) aborted during capture
demo.maskobject{}redaction of personal data in captures
demo.mask.patterns("email" | "phone" | "secret")[]["email","phone","secret"]kinds of text replaced before screenshots
demo.mask.selectorsstring[][]CSS selectors whose text is masked entirely
demo.mask.allowstring[][]values never masked, e.g. *@harborly.demo
demo.mask.replacement"fictional" | "dots""fictional"fictional look-alikes of similar length, or •••
demo.mask.enabledbooleantrueturn redaction off only for fully fictional apps
authobject{}how captures log in to protected pages
auth.strategy"none" | "form" | "storageState" | "script""none"form: fill the login page; storageState: a session saved by auth record; script: your own code
auth.loginPathstring | nullnulllogin page path (form)
auth.usernameEnvstring"DEMOVIE_USER"env var holding the demo username (form)
auth.passwordEnvstring"DEMOVIE_PASSWORD"env var holding the demo password (form)
auth.successPathstring | nullnullpath reached after a successful login
auth.scriptstring | nullnullauth script path (script); default .demovie/auth.ts
captureobject{}screenshots and element maps
capture.viewportsrecord<string, object>see examplenamed viewports
capture.defaultViewportsstring[]["desktop"]viewports captured when --viewport isn't given
capture.routesobject{}which routes capture visits
capture.routes.includestring[]["**"]route globs to capture
capture.routes.excludestring[]["/api/**"]route globs to skip
capture.routes.paramsrecord<string, string[]>{}values for dynamic routes, e.g. { "/app/projects/[id]": ["prj_launch"] }
capture.fullPagestring[][]route globs that also get a full-page screenshot
capture.waitForrecord<string, string>{}per-route CSS selector to wait for, e.g. { "/app": "main" }
capture.concurrencyinteger3pages captured in parallel
capture.crawlobject{}route discovery for generic apps
capture.crawl.maxPagesinteger50most pages a generic-mode crawl visits
capture.crawl.maxDepthinteger3most links followed from app.url
videoobject{}defaults for demovie new
video.fpsinteger30frame rate of new videos
video.formats("16:9" | "9:16" | "1:1" | "4:5")[]["16:9","9:16"]formats of new videos
video.style"clean" | "bold" | "soft" | "editorial" | "terminal""clean"style preset of new videos
audioobject{}default audio providers
audio.musicobject{}background music
audio.music.provider"synth" | "elevenlabs" | "file" | "none""synth"synth: free and local; elevenlabs: your key; file: a track imported with add --licensed
audio.voiceobject{}voiceover
audio.voice.provider"none" | "elevenlabs" | "openai""none"voiceover provider; uses your API key after you approve the cost
audio.voice.voiceIdstring | nullnullprovider voice id (default: the provider's)
audio.voice.modelstring | nullnullprovider model id (default: the provider's)
agents("claude" | "codex" | "cursor")[][]agents init set up the skill and MCP for

Example (the Harborly fixture)#

json
{
  "$schema": "https://unpkg.com/demovie@0.1.0/schema/config.schema.json",
  "version": 1,
  "project": {
    "name": "Harborly",
    "framework": "nextjs",
    "root": ".",
    "nextjs": {
      "version": "16.3.8",
      "router": "app",
      "appDir": "src/app",
      "pagesDir": null,
      "basePath": null,
      "output": null
    }
  },
  "app": {
    "url": "http://localhost:3000",
    "start": {
      "command": "pnpm dev",
      "cwd": ".",
      "env": {
        "DEMO_MODE": "1"
      },
      "readyPath": "/",
      "timeoutMs": 120000
    },
    "reuseRunning": true,
    "headers": {}
  },
  "demo": {
    "seed": "pnpm demo:seed",
    "now": "2026-09-15T10:30:00.000Z",
    "timezone": "UTC",
    "locale": "en-US",
    "colorScheme": "light",
    "hide": [".cookie-banner", "#intercom-container"],
    "blockRequests": [
      "*google-analytics.com*",
      "*googletagmanager.com*",
      "*posthog.com*",
      "*segment.io*",
      "*hotjar.com*"
    ],
    "mask": {
      "patterns": ["email", "phone", "secret"],
      "selectors": [],
      "allow": ["*@harborly.demo"],
      "replacement": "fictional",
      "enabled": true
    }
  },
  "auth": {
    "strategy": "form",
    "loginPath": "/login",
    "usernameEnv": "DEMOVIE_USER",
    "passwordEnv": "DEMOVIE_PASSWORD",
    "successPath": "/app",
    "script": null
  },
  "capture": {
    "viewports": {
      "desktop": {
        "width": 1440,
        "height": 900,
        "deviceScaleFactor": 2
      },
      "mobile": {
        "width": 390,
        "height": 844,
        "deviceScaleFactor": 3,
        "isMobile": true
      }
    },
    "defaultViewports": ["desktop"],
    "routes": {
      "include": ["**"],
      "exclude": ["/api/**"],
      "params": {
        "/app/projects/[id]": ["prj_launch"]
      }
    },
    "fullPage": ["/", "/pricing"],
    "waitFor": {
      "/app": "[data-testid=velocity-chart] svg",
      "/app/reports": "main svg",
      "/app/projects/[id]": "[data-testid=project-velocity] svg"
    },
    "concurrency": 3,
    "crawl": {
      "maxPages": 50,
      "maxDepth": 3
    }
  },
  "video": {
    "fps": 30,
    "formats": ["16:9", "9:16"],
    "style": "clean"
  },
  "audio": {
    "music": {
      "provider": "synth"
    },
    "voice": {
      "provider": "none",
      "voiceId": null,
      "model": null
    }
  },
  "agents": ["claude", "codex", "cursor"]
}
↑ ↓ to move↵ to openesc to close