Vicky is a modern self-hosted docs/wiki frontend for Markdown content stored in a GitHub repository.
It gives you:
- a public documentation site
- an admin panel for repository, branding, domain, and theme settings
- an optional AI chat assistant for docs pages
- an in-browser markdown editor that saves directly back to GitHub
- visitor and visit analytics, including per-page analytics
✨ Vicky Example Page: https://docs.fancymenu.net/docs/en-US/home
- GitHub-backed docs storage: pages are read from a configured repository and path, not from this repo
- Public docs UI with tree navigation, search, table of contents, heading anchors, mobile layout, and syntax-highlighted code blocks
- Markdown rendering with GFM support and GitHub-style alert boxes
- Built-in Light and Dark modes with simple accent-color customization
- Optional custom CSS overrides on top of the built-in themes
- Optional OpenRouter-powered AI chat assistant with configurable name, avatar, UI copy, and system prompt template
- Custom domain support with automatic Let's Encrypt HTTPS when using the included production server
- Admin-only editor with live preview and immediate GitHub commits on save
- Built-in visitor and visit analytics, including per-page analytics, for All-time, last 24h, last 7 days, last 30 days, and last 365 days
- Docs content lives in your configured GitHub repository.
- App settings live in
data/wiki-store.jsonby default. - SSL certificates and runtime SSL status live in
data/ssl/by default. - Rendered Markdown HTML cache files live in
data/markdown-cache/by default. - GitHub docs snapshot cache files live in
data/docs-cache/by default. - OpenRouter docs translation cache files live in
data/translation-cache/by default. - Login rate-limit state lives in
data/login-rate-limit.sqliteby default. A legacydata/login-rate-limit.jsonis migrated once and retained untouched. - Visitor and visit analytics live in
data/wiki-analytics.sqliteby default. - A live process owns
data/wiki-store.json.runtime-owner.jsonby default. A launcher that starts a Next.js child also createsdata/wiki-store.json.runtime-owner.json.child.json. These are exclusive runtime ownership artifacts, not application data or backup artifacts.
This repo contains the app itself, not your docs content.
- Node.js
20.9.0or newer - A GitHub repository that contains your markdown docs
- A fine-grained GitHub personal access token with:
Contents: Read and writeMetadata: Read-only
npm ciUnix
cp .env.example .env.localWindows Command Prompt
copy .env.example .env.localWindows PowerShell
Copy-Item .env.example .env.localAUTH_JWT_SECRET: a unique random value of at least 32 charactersADMIN_PASSWORD: a unique password or strong passphrase of at least 14 charactersENCRYPTION_SECRET: a second unique random value of at least 32 charactersVICKY_RUNTIME_TOPOLOGY: keep the explicit supported valuesingle-process-local
Do not reuse a value between these fields, and do not leave example values or common placeholders in place. For each machine-generated secret, openssl rand -base64 48 produces a suitable value. Vicky validates all three values and exits before opening a listener if any value is missing, predictable, reused, or still a placeholder.
npm run dev- Admin login:
http://localhost:3000/admin/login - Docs site:
http://localhost:3000/ - Editor:
http://localhost:3000/editor
If you run the repo from /mnt/<drive>/... inside WSL, npm run dev automatically switches Next.js to a polling-based watcher so hot reload stays reliable.
- Sign in at
/admin/loginwith usernameadminand the password fromADMIN_PASSWORD. - Open
/admin/settings. - In
Repository Settings, configure:- GitHub owner
- GitHub repository
- branch
- docs path
- GitHub token
- Click
Test connection. - Save the settings.
- Optionally configure:
- site title, description, footer, and icons
- Light/Dark theme accent colors
- custom domain and Let's Encrypt email
- AI chat assistant settings, OpenRouter model, and OpenRouter API key
After setup:
/redirects to your configured start page- docs pages are served at
/docs/<path> - the editor is available at
/editor
The AI chat assistant is optional and appears as the floating Ask Docs button on docs pages when enabled.
It supports:
- a configurable assistant name, profile image URL, header subtitle, and welcome message
- a configurable OpenRouter model and encrypted OpenRouter API key
- a system prompt template with
{{assistant_name}}and{{docs_txt}}placeholders - grounding responses in the live
/docs.txtexport of your documentation - optional image uploads when you choose a vision-capable model
AI chat is configured from AI Chat in the admin panel.
Notes:
- keep
{{docs_txt}}in the system prompt template so Vicky can inject the live docs export - use
{{assistant_name}}in the system prompt, header subtitle, or welcome message if you want those values to update automatically with the configured assistant name - leave the profile image URL blank to use the default assistant badge icon
The editor is admin-only and writes directly to your configured GitHub docs repository.
It supports:
- loading existing pages from the docs tree
- creating new pages
- editing title, description, path, markdown content, and commit message
- auto-generating the path from the title until you override it
- Markdown and Preview modes
Ctrl+S/Cmd+Ssaving
Vicky supports:
- GitHub Flavored Markdown
- fenced code blocks with syntax highlighting
- copy buttons on fenced code blocks
- heading anchors
- generated table of contents data
- GitHub-style alerts such as
> [!NOTE],> [!TIP],> [!WARNING] - additional alert aliases:
INFO,SUCCESS,ERROR - automatic normalization of root-relative docs links to
/docs/...
npm run devstarts the Next.js dev servernpm run buildbuilds the app for productionnpm run startruns the included production server (server.mjs)npm run start:nextruns plainnext startnpm run lintruns ESLintnpm run typecheckruns TypeScript checksnpm run testruns the test suite oncenpm run test:watchruns tests in watch mode
Use npm run start if you want Vicky's built-in custom-domain and automatic HTTPS handling. Use npm run start:next only if you explicitly want a plain Next.js server.
Required:
| Variable | Purpose |
|---|---|
AUTH_JWT_SECRET |
Unique 32+ character value that signs admin session tokens |
ADMIN_PASSWORD |
Unique 14+ character password or strong passphrase for /admin/login |
ENCRYPTION_SECRET |
Unique 32+ character value that encrypts stored provider credentials |
VICKY_RUNTIME_TOPOLOGY |
Required topology declaration; currently only single-process-local is supported |
Common optional settings:
| Variable | Purpose | Default |
|---|---|---|
WIKI_STORE_FILE_PATH |
Location of the app settings store | ./data/wiki-store.json |
WIKI_SSL_STORAGE_DIR |
Certificate storage directory | ./data/ssl |
WIKI_MARKDOWN_CACHE_DIR |
Persistent rendered Markdown HTML cache directory | ./data/markdown-cache |
WIKI_DOCS_SNAPSHOT_DIR |
Persistent GitHub docs snapshot cache directory | ./data/docs-cache/snapshots |
WIKI_TRANSLATION_CACHE_DIR |
Persistent docs translation cache directory | ./data/translation-cache |
WIKI_ANALYTICS_DB_PATH |
Persistent visitor analytics SQLite database | ./data/wiki-analytics.sqlite |
AUTH_LOGIN_DB_PATH |
Durable login rate-limit SQLite database | ./data/login-rate-limit.sqlite |
AUTH_LOGIN_MAX_FAILURES |
Failed logins allowed within the rolling window before blocking | 8 |
AUTH_LOGIN_WINDOW_SECONDS |
Rolling failed-login window | 600 |
AUTH_LOGIN_BLOCK_SECONDS |
Block duration after the failed-login threshold | 10800 |
AUTH_LOGIN_MAX_IDENTITIES |
Maximum durable known-client rate-limit identities before new identities share the unknown bucket | 10000 |
AUTH_LOGIN_DB_BUSY_TIMEOUT_MS |
Maximum SQLite lock wait before the bounded process-local fallback is used | 5000 |
AUTH_LOGIN_STORE_FILE_PATH |
Legacy JSON path read once for non-destructive migration; also supplies the SQLite location when AUTH_LOGIN_DB_PATH is unset |
./data/login-rate-limit.json |
PUBLIC_DOCS_CLIENT_MAX_REQUESTS |
Public page, raw, metadata, and tree reads accepted per client during the docs rate window | 180 |
PUBLIC_DOCS_GLOBAL_MAX_REQUESTS |
Public document reads accepted globally during the docs rate window | 5000 |
PUBLIC_DOCS_RATE_WINDOW_SECONDS |
Public document read rate-limit window | 60 |
PUBLIC_DOCS_CLIENT_MAX_CONCURRENCY |
Concurrent public document reads allowed per client | 12 |
PUBLIC_DOCS_GLOBAL_MAX_CONCURRENCY |
Concurrent public document reads allowed per app process | 128 |
VISITOR_ANALYTICS_CLIENT_MAX_REQUESTS |
Page-view requests accepted per client during the analytics rate window | 60 |
VISITOR_ANALYTICS_GLOBAL_MAX_REQUESTS |
Page-view requests accepted globally during the analytics rate window | 1200 |
VISITOR_ANALYTICS_RATE_WINDOW_SECONDS |
Analytics page-view rate-limit window | 60 |
VISITOR_ANALYTICS_CLIENT_MAX_CONCURRENCY |
Concurrent page-view requests allowed per client | 4 |
VISITOR_ANALYTICS_GLOBAL_MAX_CONCURRENCY |
Concurrent page-view requests allowed per app process | 32 |
VISITOR_ANALYTICS_QUEUE_CAPACITY |
Maximum queued or actively written page-view events | 1000 |
VISITOR_ANALYTICS_RETRY_BASE_MS |
Initial delay before retrying a failed analytics write | 1000 |
VISITOR_ANALYTICS_RETRY_MAX_MS |
Maximum delay between failed analytics write attempts | 30000 |
HOST |
Listen host | 0.0.0.0 |
HTTP_PORT |
HTTP listen port | 3000 |
HTTPS_PORT |
HTTPS listen port | 443 |
LETS_ENCRYPT_STAGING |
Use Let's Encrypt staging CA for test runs | false |
AUTH_TRUST_PROXY_HEADERS |
Enable one proxy-overwritten client IP from an allowlisted peer | false |
AUTH_TRUSTED_PROXY_IPS |
Exact comma-separated proxy socket IP allowlist for client-IP forwarding | unset |
VICKY_TRUST_PROXY_ORIGIN_HEADERS |
Trust one proxy-overwritten public host/protocol pair | false |
VICKY_DIRECT_REQUEST_PROTOCOL |
Explicit http or https origin protocol for plain Next direct deployments |
unset |
Runtime file and directory overrides must point into a dedicated storage directory. On POSIX hosts Vicky enforces and verifies 0700 directories and 0600 sensitive files; it refuses to change permissions on filesystem roots, OS temp/home/project roots, and broad system directories such as /var/lib, /usr/local, /Users, or C:\ProgramData. Configure a dedicated child such as /var/lib/vicky, /usr/local/vicky, or C:\ProgramData\Vicky instead. The complete layout is classified before any directory is created, chmodded, or written. Windows deployments must apply equivalent access restrictions through the storage volume's ACLs because Windows does not implement POSIX mode bits.
Vicky supports exactly one application process, on one host, using one local persistent runtime volume. Configure the deployment for one replica and use VICKY_RUNTIME_TOPOLOGY=single-process-local. Multi-process clusters, overlapping rolling deployments, multi-host replicas, serverless instances, and active/active failover are not supported.
Every supported launcher performs a storage preflight and exclusively creates an immutable owner file beside WIKI_STORE_FILE_PATH before Next.js can accept requests. With the default store this is data/wiki-store.json.runtime-owner.json. A launcher never replaces that file during handoff: it exclusively creates data/wiki-store.json.runtime-owner.json.child.json, and the spawned Next.js child remains behind a startup gate until both immutable records identify its inherited random lease ID and PID. The assignment is also bound to the exact owner-record contents. This prevents a killed launcher parent from leaving an unrecorded live server and prevents a handoff from overwriting changed or missing ownership. A second process using the same runtime volume exits instead of independently running translation jobs, cache generations, rate and concurrency limits, analytics ingestion, or TLS/ACME coordination. Invoking the Next CLI directly is also rejected.
The owner file is a local process lock, not a distributed lease, and Vicky does not claim multi-host guarantees. Replicas with separate filesystems cannot see one another, so the deployment platform must enforce a replica count of one. Do not place the SQLite databases on a network filesystem with incomplete locking semantics, and do not distribute different copies of the runtime directory behind one ingress.
Graceful shutdown removes the child-assignment file, when present, and then the owner file. After any ungraceful stop, startup fails closed for every existing ownership artifact—even a well-formed same-host record whose PID appears dead. PID checks cannot make stale-file replacement atomic, so Vicky never automatically reclaims or deletes these files. First verify that every former Vicky launcher, custom server, and Next.js child is terminated, then remove only the exact owner and child-assignment paths printed by the startup error before restarting. Both files must be absent. Do not restore or copy either file as application data.
The enforced topology is also the boundary for process-local work: active translation jobs and their upload queue, accepted analytics writes awaiting SQLite, in-flight provider/cache work, admission counters, and ACME challenges do not move to another process. A restart can interrupt that transient work; the durable settings, sessions, login limiter, analytics already committed to SQLite, certificates, and persistent caches remain on the local runtime volume.
HTTP_PORT falls back to PORT if HTTP_PORT is not set.
Client identity follows one strict policy for login and AI rate limiting, public-docs admission, analytics admission, and visitor identity/event deduplication. npm run start normalizes the socket peer and passes it to the app in a private header authenticated by the same random per-process request-context token used for direct origin data. Client-supplied private headers are ignored by plain Next and overwritten by the included server. Current Next.js NextRequest objects do not expose a direct ip property, so plain npm run start:next cannot safely distinguish Next's synthesized XFF value from a client-supplied one and falls back to unknown/global controls.
Failed-login state uses SQLite WAL transactions so app processes on the same host sharing the configured database cannot lose concurrent attempts. The storage volume must provide reliable local-style SQLite file locking; do not place this database on a network filesystem with incomplete locking semantics. Threshold activation, success clearing, and sliding-window updates are serialized; blocked-status checks are read-only and do not refresh timestamps. Known identities and their pending attempts are bounded and stale entries are pruned opportunistically. When the configured identity capacity is full, new addresses conservatively share the explicit unknown bucket. A temporary SQLite error retains bounded process-local protection and emits throttled diagnostics rather than disabling the limiter.
On first use, Vicky checks AUTH_LOGIN_STORE_FILE_PATH for the former JSON state. Valid entries are canonicalized and imported in one transaction, including merged IPv4-mapped forms; a migration marker prevents cleared attempts from being imported again. The JSON file is never deleted or rewritten. Malformed or oversized legacy state is retained, logged with bounded detail, and activates one temporary global safety block instead of silently discarding possibly active protection. Stop every older JSON-based Vicky process before starting the upgraded version so no legacy writer can add attempts after the one-time migration. If AUTH_LOGIN_DB_PATH is unset and a legacy path is configured, the database is placed beside it by replacing a .json suffix with .sqlite or appending .sqlite.
Forwarded client-IP trust requires both AUTH_TRUST_PROXY_HEADERS=true and an exact match between the authenticated socket/framework peer and one entry in AUTH_TRUSTED_PROXY_IPS. The allowlist accepts comma-separated exact IPv4 or IPv6 addresses only, not hostnames or CIDR ranges; any invalid or empty entry makes the whole allowlist fail closed. IPv4-mapped IPv6 is normalized to IPv4. A trusted ingress must remove client-supplied x-forwarded-for and x-real-ip, then overwrite either header with one exact client address. Chains and address-plus-port forms are rejected; if both headers are present, both must normalize to the same address. Resolution order is trusted forwarded client, authenticated included-server socket, framework direct IP when a runtime genuinely supplies one, then unknown. Keep the Vicky listener unreachable except through the listed ingress peers; the application allowlist complements, but does not replace, network isolation.
Public origins used for canonical/OpenGraph metadata, plaintext docs links and cache keys, OpenRouter attribution, localization jobs, same-origin analytics checks, and icon redirects follow one policy. A configured custom domain is canonical and resolves to HTTPS. Without one, npm run start supplies an authenticated internal Host and socket protocol to the app. Plain next start deployments can set VICKY_DIRECT_REQUEST_PROTOCOL to exactly http or https; when it is unset, localhost/loopback authorities default to HTTP and other validated authorities default to HTTPS. Host input is accepted only as a single valid DNS name, IPv4 address, or bracketed IPv6 address with an optional valid port. Credentials, paths, queries, fragments, whitespace, control characters, invalid ports, and ambiguous numeric host forms are rejected. Authentication middleware uses relative login redirects, so request authority is not involved there.
VICKY_TRUST_PROXY_ORIGIN_HEADERS is deliberately separate from the client-IP pair AUTH_TRUST_PROXY_HEADERS/AUTH_TRUSTED_PROXY_IPS. Enabling origin trust does not enable client-IP trust, and enabling client-IP trust does not change canonical origin selection. Vicky never consumes the RFC Forwarded header and never guesses the first or last value in a forwarding chain. When origin-header trust is enabled, x-forwarded-host and x-forwarded-proto must both contain exactly one valid value; comma-separated, partial, or malformed pairs are ignored in favor of the validated direct request context. The included server also removes an untrusted or invalid pair before Next.js can consume it internally.
Public page-view analytics accepts only small UTF-8 JSON bodies, rejects conflicting browser Origin/Sec-Fetch-Site signals, and records only pages present in the configured docs tree. Page paths and titles are resolved from server-side docs caches, client titles are ignored, and event IDs are deduplicated with bounded in-memory state before SQLite's durable per-visitor deduplication. Per-client controls use the trusted client-IP settings above; when no trustworthy address is available, the global limits remain active without collapsing all visitors into one client quota. Unknown-address visits receive independent anonymous identities and do not share event-ID dedup keys, so visitor counts are necessarily approximate until a trustworthy address is available. The analytics rate, concurrency, queue, and retry limits apply to the one supported app process; ingress rate limiting remains useful as defense in depth but does not make multiple Vicky replicas safe.
Public document reads accept only bounded canonical paths that already exist in the cached GitHub docs tree. Unknown slugs are negatively cached and never become speculative .md or .mdx GitHub file requests. The public docs rate and concurrency limits apply to the one supported app process; when no trustworthy client address is available, only the global limits apply. Ingress rate limiting remains useful as defense in depth but does not make multiple Vicky replicas safe.
For the full list of optional runtime settings, check .env.example.
Standard production flow:
npm run build
npm run startSet the deployment replica count to one, prevent overlapping process starts during rollout, and mount one durable local-style runtime volume for every persisted path listed below. Startup fails before a listener opens if the topology declaration, runtime path permissions, or exclusive owner lease is invalid.
Both npm run start and npm run start:next validate the required secrets, supported topology, storage paths, and exclusive ownership before accepting requests. Use the npm command instead of invoking next start directly so the plain Next.js server is validated before its process is created. Automated tests receive deterministic fallback credentials only through the test runner; setting NODE_ENV=test in a normal process does not bypass startup validation.
npm run start uses the included server.mjs server, which:
- starts the Next.js app
- serves HTTP
- enables HTTPS automatically when a custom domain and Let's Encrypt email are configured
- reserves plaintext
/.well-known/acme-challenge/*requests for HTTP-01 validation - redirects configured custom-domain HTTP traffic only while a matching, currently valid certificate is active
- returns a cache-disabled
503maintenance response for configured custom-domain HTTP traffic while initial issuance, expired-certificate replacement, or outage recovery is pending - keeps localhost and other non-custom HTTP hosts available for local/default-host access
- retries failed issuance and renewal with capped exponential backoff while the process is running
- supplies authenticated admins with SSL runtime status from a private on-disk snapshot
- watches the settings store so domain/SSL changes apply quickly
- persists runtime SSL status to disk
The optional external SSL health endpoint defaults to /.well-known/vicky/ssl-status, but it is disabled and returns 404 unless SSL_STATUS_BEARER_TOKEN is set. When enabled, it requires Authorization: Bearer <token> using constant-time credential comparison and returns only sanitized health fields. It never returns filesystem paths, the configured domain, host/port settings, refresh reasons, or raw certificate/ACME errors. Use a long randomly generated token, access the endpoint only through HTTPS or a trusted private network, and change SSL_STATUS_ENDPOINT_PATH if your ingress requires a custom route. The authenticated /api/admin/domain-status route does not depend on this external endpoint.
HTTP_PORT and HTTPS_PORT must be different values.
If you run Vicky behind a reverse proxy:
- forward
/.well-known/acme-challenge/*to Vicky unchanged - do not replace Vicky's fail-closed custom-domain
503response with a plaintext application fallback - preserve the original
Hostheader - keep DNS pointed at the proxy/public ingress
- set
AUTH_TRUST_PROXY_HEADERS=trueand list every exact proxy socket address inAUTH_TRUSTED_PROXY_IPSonly when the proxy removes client-suppliedx-forwarded-for/x-real-ip, overwrites one client address, the Vicky listener is isolated from other peers, and the proxy never appends a forwarding chain - set
VICKY_TRUST_PROXY_ORIGIN_HEADERS=trueonly when the proxy removes client-supplied forwarding headers, overwrites bothx-forwarded-hostandx-forwarded-protowith one client-facing value each, and the Vicky port cannot be reached except through that proxy - do not append forwarding chains for origin headers; Vicky intentionally ignores comma-separated first-hop/last-hop values
For a plain npm run start:next deployment without a reverse proxy, leave AUTH_TRUST_PROXY_HEADERS=false, AUTH_TRUSTED_PROXY_IPS empty, and VICKY_TRUST_PROXY_ORIGIN_HEADERS=false. If no custom domain is configured, set VICKY_DIRECT_REQUEST_PROTOCOL to the direct public protocol when its value differs from the localhost/HTTPS defaults. The included npm run start server derives the direct protocol and authenticated client socket address itself and does not need this setting. Plain Next uses global-only public-docs and analytics admission when no trustworthy IP is available; login and AI brute-force protection deliberately retain conservative global unknown buckets.
For direct HTTP-01 validation without a reverse proxy, you usually want:
HTTP_PORT=80HTTPS_PORT=443
Persist these paths across deployments:
data/wiki-store.jsondata/ssl/data/markdown-cache/data/docs-cache/data/translation-cache/data/wiki-analytics.sqlite, including its-waland-shmcompanions when presentdata/login-rate-limit.sqlite, including its-waland-shmcompanions when presentdata/login-rate-limit.jsonwhen retained as a legacy migration source
Public endpoints:
GET /api/public/settingsGET /api/public/icon/16GET /api/public/icon/32GET /api/public/icon/180POST /api/ai/chatGET /api/docs/treeGET /api/docs/pageGET /api/docs/page-metadataGET /api/docs/rawGET /api/docs/raw/<path>GET /api/docs/searchPOST /api/docs/visitGET /docs.txt
Auth endpoints:
POST /api/auth/loginPOST /api/auth/logoutGET /api/auth/me
Admin endpoints:
GET|PATCH /api/admin/settingsPOST /api/admin/test-connectionGET|POST /api/admin/docsPOST /api/admin/docs/refreshGET /api/admin/domain-statusGET|POST|DELETE /api/admin/markdown-cacheGET|POST /api/admin/moderatorsPATCH|DELETE /api/admin/moderators/<id>GET /api/admin/performancePOST /api/admin/translations/requestPOST /api/admin/translations/statusGET /api/admin/visitors
Vicky uses Google's Material Symbols Outlined icon font for UI icons via @fontsource/material-symbols-outlined.
- Source: https://github.com/google/material-design-icons
- License: Apache-2.0
Vicky uses Circle Flags for language flag icons via @iconify-json/circle-flags.
- Source: https://github.com/HatScripts/circle-flags
- License: MIT
Vicky Copyright © 2026 Keksuccino.
Vicky is licensed under MIT. See LICENSE.md.