From 8f5e87ed4a212ee04a4f8c038f6cc6fa24190d25 Mon Sep 17 00:00:00 2001
From: Richard R
Date: Tue, 17 Feb 2026 18:26:03 -0700
Subject: [PATCH] feat: Replace `NEXT_PUBLIC_NODE_ENV` with explicit feature
flags for granular control over DOCX conversion, destructive actions, TTS
defaults, and DeepInfra model visibility.
---
.env.example | 13 ++-
docs-site/docs/deploy/vercel-deployment.md | 41 ++++----
.../docs/reference/environment-variables.md | 96 ++++++++++++-------
src/components/DocumentUploader.tsx | 13 +--
src/components/SettingsModal.tsx | 10 +-
src/lib/dexie.ts | 10 +-
src/types/config.ts | 6 +-
7 files changed, 110 insertions(+), 79 deletions(-)
diff --git a/.env.example b/.env.example
index 216d86e..f1b041c 100644
--- a/.env.example
+++ b/.env.example
@@ -1,7 +1,3 @@
-# (Optional) Client feature flags (does not work in Docker containers due to being build-time only)
-NEXT_PUBLIC_NODE_ENV=development
-NEXT_PUBLIC_ENABLE_AUDIOBOOK_EXPORT=
-NEXT_PUBLIC_ENABLE_WORD_HIGHLIGHT=
# Local / OpenAI TTS API Configuration (default)
# Suggest using https://github.com/remsky/Kokoro-FastAPI
@@ -74,3 +70,12 @@ WHISPER_CPP_BIN=/whisper.cpp/build/bin/whisper-cli
# (Optional) Override ffmpeg binary path used for audiobook processing
FFMPEG_BIN=
+
+# (Optional) Client feature flags (does not work in Docker containers due to being build-time only)
+# NEXT_PUBLIC_ENABLE_DOCX_CONVERSION=true
+# NEXT_PUBLIC_ENABLE_DESTRUCTIVE_DELETE_ACTIONS=true
+# NEXT_PUBLIC_DEFAULT_TTS_PROVIDER=custom-openai
+# NEXT_PUBLIC_DEFAULT_TTS_MODEL=kokoro
+# NEXT_PUBLIC_SHOW_ALL_DEEPINFRA_MODELS=true
+# NEXT_PUBLIC_ENABLE_AUDIOBOOK_EXPORT=true
+# NEXT_PUBLIC_ENABLE_WORD_HIGHLIGHT=false
diff --git a/docs-site/docs/deploy/vercel-deployment.md b/docs-site/docs/deploy/vercel-deployment.md
index 00a61bd..89a91a9 100644
--- a/docs-site/docs/deploy/vercel-deployment.md
+++ b/docs-site/docs/deploy/vercel-deployment.md
@@ -18,6 +18,8 @@ This guide covers deploying OpenReader WebUI to Vercel with external Postgres an
Recommended production setup (auth enabled):
```bash
+API_BASE=https://api.deepinfra.com/v1/openai
+API_KEY=your_deepinfra_key
POSTGRES_URL=postgres://...
USE_EMBEDDED_WEED_MINI=false
S3_ACCESS_KEY_ID=...
@@ -27,34 +29,29 @@ S3_REGION=us-east-1
S3_PREFIX=openreader
BASE_URL=https://your-app.vercel.app
AUTH_SECRET=...
-NEXT_PUBLIC_NODE_ENV=production
-# Optional client/runtime feature overrides:
-# NEXT_PUBLIC_ENABLE_AUDIOBOOK_EXPORT=false
-# NEXT_PUBLIC_ENABLE_WORD_HIGHLIGHT=true
+# Optional client/runtime feature defaults:
+NEXT_PUBLIC_ENABLE_DOCX_CONVERSION=false
+NEXT_PUBLIC_ENABLE_DESTRUCTIVE_DELETE_ACTIONS=false
+NEXT_PUBLIC_DEFAULT_TTS_PROVIDER=deepinfra
+NEXT_PUBLIC_DEFAULT_TTS_MODEL=hexgrad/Kokoro-82M
+NEXT_PUBLIC_SHOW_ALL_DEEPINFRA_MODELS=false
+NEXT_PUBLIC_ENABLE_AUDIOBOOK_EXPORT=true
+NEXT_PUBLIC_ENABLE_WORD_HIGHLIGHT=false
# Optional (non-AWS S3-compatible providers):
# S3_ENDPOINT=https://...
# S3_FORCE_PATH_STYLE=true
```
-:::info `NEXT_PUBLIC_*` feature flags
-- `NEXT_PUBLIC_ENABLE_AUDIOBOOK_EXPORT=false`: hides audiobook export UI entry points.
-- `NEXT_PUBLIC_ENABLE_WORD_HIGHLIGHT=true`: enables word-highlight UI and timestamp alignment requests.
-:::
+:::info Production Configuration & Feature Flags
+We recommend setting these defaults for a production-like environment:
-:::warning `NEXT_PUBLIC_NODE_ENV` behavior
-Use `NEXT_PUBLIC_NODE_ENV=production` on Vercel unless you explicitly want dev-oriented client behavior.
-
-With `production`:
-- Footer is shown in the app shell
-- DOCX upload/conversion option is hidden
-- Default provider/model behavior is production-oriented
-- DeepInfra model picker is restricted without an API key
-- Privacy modal shows hosted-service/operator wording
-- Dev-only destructive document actions are hidden
-
-With unset/non-`production`, the inverse dev behavior applies.
-
-Full details: [Environment Variables](../reference/environment-variables#next_public_node_env).
+- `NEXT_PUBLIC_ENABLE_DOCX_CONVERSION=false`: Disables DOCX upload (requires external tools anyway)
+- `NEXT_PUBLIC_ENABLE_DESTRUCTIVE_DELETE_ACTIONS=false`: Hides destructive "Delete All" actions
+- `NEXT_PUBLIC_DEFAULT_TTS_PROVIDER=deepinfra`: Points default TTS to a scalable provider
+- `NEXT_PUBLIC_DEFAULT_TTS_MODEL=hexgrad/Kokoro-82M`: Uses a high-quality default model
+- `NEXT_PUBLIC_SHOW_ALL_DEEPINFRA_MODELS=false`: Restricts usage to free models if no key is provided
+- `NEXT_PUBLIC_ENABLE_AUDIOBOOK_EXPORT=true`: (Optional) Controls audiobook export UI
+- `NEXT_PUBLIC_ENABLE_WORD_HIGHLIGHT=false`: (Optional) Controls word highlighting UI (requires timestamp backend)
:::
:::warning Auth recommendation
diff --git a/docs-site/docs/reference/environment-variables.md b/docs-site/docs/reference/environment-variables.md
index 1c85aa3..50866d2 100644
--- a/docs-site/docs/reference/environment-variables.md
+++ b/docs-site/docs/reference/environment-variables.md
@@ -9,7 +9,11 @@ This is the single reference page for OpenReader WebUI environment variables.
| Variable | Area | Default | When to set |
| --- | --- | --- | --- |
-| `NEXT_PUBLIC_NODE_ENV` | Runtime mode | treated as `development` unless `production` | Set `production` for production client behavior |
+| `NEXT_PUBLIC_ENABLE_DOCX_CONVERSION` | Client feature flags | `true` unless set to `false` | Set `false` to hide DOCX support |
+| `NEXT_PUBLIC_ENABLE_DESTRUCTIVE_DELETE_ACTIONS` | Client feature flags | `true` unless set to `false` | Set `false` to hide destructive actions |
+| `NEXT_PUBLIC_DEFAULT_TTS_PROVIDER` | Client feature flags | `custom-openai` | Override default TTS provider |
+| `NEXT_PUBLIC_DEFAULT_TTS_MODEL` | Client feature flags | `kokoro` | Override default TTS model |
+| `NEXT_PUBLIC_SHOW_ALL_DEEPINFRA_MODELS` | Client feature flags | `true` unless set to `false` | Set `false` to restrict DeepInfra models |
| `NEXT_PUBLIC_ENABLE_AUDIOBOOK_EXPORT` | Client feature flags | `true` unless set to `false` | Set `false` to hide audiobook export UI |
| `NEXT_PUBLIC_ENABLE_WORD_HIGHLIGHT` | Client feature flags | `false` unless set to `true` | Set `true` to enable word highlight + alignment |
| `API_BASE` | TTS provider | none | Point to your OpenAI-compatible TTS base URL |
@@ -50,41 +54,7 @@ This is the single reference page for OpenReader WebUI environment variables.
| `WHISPER_CPP_BIN` | Word timing | unset | Set to enable `whisper.cpp` timestamps |
| `FFMPEG_BIN` | Audio runtime | auto-detected (`ffmpeg-static`) | Override ffmpeg binary path |
-## Client Runtime and Feature Flags
-### NEXT_PUBLIC_NODE_ENV
-
-Controls development vs production behavior in client/server code paths.
-
-- Typical values: `development`, `production`
-- OpenReader `isDev` checks rely on this variable directly
-- If this is not `production`, OpenReader treats the client as development mode
-- In deployed environments, set `NEXT_PUBLIC_NODE_ENV=production` explicitly for predictable production behavior
-- Affects:
- - Footer visibility in the app shell
- - DOCX upload/conversion availability in upload UI
- - Default provider/model behavior for first-run TTS config
- - DeepInfra model picker restrictions when no API key is set
- - Dev-only destructive document actions in settings
-
-### NEXT_PUBLIC_ENABLE_AUDIOBOOK_EXPORT
-
-Controls whether audiobook export UI/actions are shown in the client.
-
-- Default behavior: enabled unless explicitly set to `false`
-- Applies in both development and production
-- Affects export entry points in PDF/EPUB pages and document settings UI
-
-### NEXT_PUBLIC_ENABLE_WORD_HIGHLIGHT
-
-Controls word-by-word highlighting UI and timestamp-alignment behavior.
-
-- Default behavior: disabled unless set to `true`
-- Applies in both development and production
-- Requires working timestamp generation (for example `WHISPER_CPP_BIN`)
-- Affects:
- - Word-highlight toggles in document settings
- - Alignment requests during TTS playback
## TTS Provider and Request Behavior
@@ -367,3 +337,59 @@ Absolute path or executable name for the ffmpeg binary used by audiobook/process
- Resolution order: `FFMPEG_BIN` -> `ffmpeg-static`
- Example: `/var/task/node_modules/ffmpeg-static/ffmpeg`
+
+## Client Runtime and Feature Flags
+
+### NEXT_PUBLIC_ENABLE_DOCX_CONVERSION
+
+ Controls whether the experimental DOCX-to-PDF conversion and upload feature is enabled.
+
+ - Default: `true` (enabled)
+ - Set `false` to hide DOCX support in the upload UI
+
+ ### NEXT_PUBLIC_ENABLE_DESTRUCTIVE_DELETE_ACTIONS
+
+ Controls whether the "Delete all user docs" and other bulk-delete buttons are shown in Settings.
+
+ - Default: `true` (enabled)
+ - Set `false` to hide destructive actions (recommended for production)
+
+ ### NEXT_PUBLIC_DEFAULT_TTS_PROVIDER
+
+ Sets the default TTS provider for new users.
+
+ - Default: `custom-openai`
+ - Example values: `deepinfra`, `openai`, `custom-openai`
+
+ ### NEXT_PUBLIC_DEFAULT_TTS_MODEL
+
+ Sets the default TTS model for new users.
+
+ - Default: `kokoro`
+ - Example values: `hexgrad/Kokoro-82M`, `tts-1`
+
+ ### NEXT_PUBLIC_SHOW_ALL_DEEPINFRA_MODELS
+
+ Controls whether the DeepInfra model list shows all models or just the free tier when no API key is set.
+
+ - Default: `true` (show all)
+ - Set `false` to restrict to free tier models when no API key is provided
+
+### NEXT_PUBLIC_ENABLE_AUDIOBOOK_EXPORT
+
+Controls whether audiobook export UI/actions are shown in the client.
+
+- Default behavior: enabled unless explicitly set to `false`
+- Applies in both development and production
+- Affects export entry points in PDF/EPUB pages and document settings UI
+
+### NEXT_PUBLIC_ENABLE_WORD_HIGHLIGHT
+
+Controls word-by-word highlighting UI and timestamp-alignment behavior.
+
+- Default behavior: disabled unless set to `true`
+- Applies in both development and production
+- Requires working timestamp generation (for example `WHISPER_CPP_BIN`)
+- Affects:
+ - Word-highlight toggles in document settings
+ - Alignment requests during TTS playback
diff --git a/src/components/DocumentUploader.tsx b/src/components/DocumentUploader.tsx
index df7cce1..0a714d2 100644
--- a/src/components/DocumentUploader.tsx
+++ b/src/components/DocumentUploader.tsx
@@ -6,7 +6,8 @@ import { UploadIcon } from '@/components/icons/Icons';
import { useDocuments } from '@/contexts/DocumentContext';
import { uploadDocxAsPdf } from '@/lib/client-documents';
-const isDev = process.env.NEXT_PUBLIC_NODE_ENV !== 'production' || process.env.NODE_ENV == null;
+const enableDocx = process.env.NEXT_PUBLIC_ENABLE_DOCX_CONVERSION !== 'false';
+
interface DocumentUploaderProps {
className?: string;
@@ -14,8 +15,8 @@ interface DocumentUploaderProps {
}
export function DocumentUploader({ className = '', variant = 'default' }: DocumentUploaderProps) {
- const {
- addPDFDocument: addPDF,
+ const {
+ addPDFDocument: addPDF,
addEPUBDocument: addEPUB,
addHTMLDocument: addHTML,
refreshDocuments,
@@ -38,7 +39,7 @@ export function DocumentUploader({ className = '', variant = 'default' }: Docume
await addEPUB(file);
} else if (file.type === 'text/plain' || file.type === 'text/markdown' || file.name.endsWith('.md')) {
await addHTML(file);
- } else if (isDev && file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
+ } else if (enableDocx && file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
// Preserve prior UX: show "Converting DOCX..." state rather than generic uploading.
setIsUploading(false);
setIsConverting(true);
@@ -65,7 +66,7 @@ export function DocumentUploader({ className = '', variant = 'default' }: Docume
'application/epub+zip': ['.epub'],
'text/plain': ['.txt'],
'text/markdown': ['.md'],
- ...(isDev ? {
+ ...(enableDocx ? {
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx']
} : {})
},
@@ -111,7 +112,7 @@ export function DocumentUploader({ className = '', variant = 'default' }: Docume
{isDragActive ? 'Drop your file(s) here' : 'Drop your file(s) here, or click to select'}
- {isDev ? 'PDF, EPUB, TXT, MD, or DOCX files are accepted' : 'PDF, EPUB, TXT, or MD files are accepted'}
+ {enableDocx ? 'PDF, EPUB, TXT, MD, or DOCX files are accepted' : 'PDF, EPUB, TXT, or MD files are accepted'}
{error && {error}
}
>
diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx
index 05b28d2..d043fd6 100644
--- a/src/components/SettingsModal.tsx
+++ b/src/components/SettingsModal.tsx
@@ -40,7 +40,9 @@ import { deleteDocuments, mimeTypeForDoc, uploadDocuments } from '@/lib/client-d
import { cacheStoredDocumentFromBytes, clearDocumentCache } from '@/lib/document-cache';
import { clearAllDocumentPreviewCaches, clearInMemoryDocumentPreviewCache } from '@/lib/document-preview-cache';
-const isDev = process.env.NEXT_PUBLIC_NODE_ENV !== 'production' || process.env.NODE_ENV == null;
+const enableDestructiveDelete = process.env.NEXT_PUBLIC_ENABLE_DESTRUCTIVE_DELETE_ACTIONS !== 'false';
+const showAllDeepInfra = process.env.NEXT_PUBLIC_SHOW_ALL_DEEPINFRA_MODELS !== 'false';
+
const themes = THEMES.map(id => ({
id,
@@ -108,7 +110,7 @@ export function SettingsModal({ className = '' }: { className?: string }) {
];
case 'deepinfra':
// In production without an API key, limit to free tier model
- if (!isDev && !localApiKey) {
+ if (!showAllDeepInfra && !localApiKey) {
return [
{ id: 'hexgrad/Kokoro-82M', name: 'hexgrad/Kokoro-82M' }
];
@@ -561,7 +563,7 @@ export function SettingsModal({ className = '' }: { className?: string }) {
type="password"
value={localApiKey}
onChange={(e) => handleInputChange('apiKey', e.target.value)}
- placeholder={!isDev && localTTSProvider === 'deepinfra' ? "Deepinfra free or use your API key" : "Using environment variable"}
+ placeholder={!showAllDeepInfra && localTTSProvider === 'deepinfra' ? "Deepinfra free or use your API key" : "Using environment variable"}
className="w-full rounded-lg bg-background py-1.5 px-3 text-foreground shadow-sm focus:outline-none focus:ring-2 focus:ring-accent"
/>
@@ -782,7 +784,7 @@ export function SettingsModal({ className = '' }: { className?: string }) {
>
Clear cache
- {isDev && (
+ {enableDestructiveDelete && (