feat: Replace NEXT_PUBLIC_NODE_ENV with explicit feature flags for granular control over DOCX conversion, destructive actions, TTS defaults, and DeepInfra model visibility.

This commit is contained in:
Richard R 2026-02-17 18:26:03 -07:00
parent eeeceb8e54
commit 8f5e87ed4a
7 changed files with 110 additions and 79 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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'}
</p>
<p className="text-xs sm:text-sm text-muted">
{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'}
</p>
{error && <p className="mt-2 text-sm text-red-500">{error}</p>}
</>

View file

@ -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"
/>
</div>
@ -782,7 +784,7 @@ export function SettingsModal({ className = '' }: { className?: string }) {
>
Clear cache
</Button>
{isDev && (
{enableDestructiveDelete && (
<div className="flex w-full gap-2">
<Button
onClick={() => {

View file

@ -76,7 +76,7 @@ type OpenReaderDB = Dexie & {
export const db = new Dexie(DB_NAME) as OpenReaderDB;
const isDev = process.env.NEXT_PUBLIC_NODE_ENV !== 'production' || process.env.NODE_ENV == null;
type DexieOpenStatus = 'opening' | 'opened' | 'blocked' | 'stalled' | 'error';
@ -113,8 +113,8 @@ function inferProviderAndBaseUrl(raw: RawConfigMap): { provider: string; baseUrl
const cachedBaseUrl = raw.baseUrl;
let inferredProvider = raw.ttsProvider || '';
if (!isDev && !raw.ttsProvider) {
inferredProvider = 'deepinfra';
if (!raw.ttsProvider) {
inferredProvider = process.env.NEXT_PUBLIC_DEFAULT_TTS_PROVIDER || 'custom-openai';
} else if (!inferredProvider) {
if (cachedBaseUrl) {
const baseUrlLower = cachedBaseUrl.toLowerCase();
@ -194,8 +194,8 @@ function buildAppConfigFromRaw(raw: RawConfigMap): AppConfigRow {
(provider === 'openai'
? 'tts-1'
: provider === 'deepinfra'
? 'hexgrad/Kokoro-82M'
: APP_CONFIG_DEFAULTS.ttsModel),
? 'hexgrad/Kokoro-82M'
: APP_CONFIG_DEFAULTS.ttsModel),
ttsInstructions: raw.ttsInstructions ?? APP_CONFIG_DEFAULTS.ttsInstructions,
savedVoices,
pdfHighlightEnabled:

View file

@ -1,6 +1,6 @@
import type { DocumentListState } from '@/types/documents';
const isDev = process.env.NEXT_PUBLIC_NODE_ENV !== 'production' || process.env.NODE_ENV == null;
const wordHighlightEnabledByDefault = process.env.NEXT_PUBLIC_ENABLE_WORD_HIGHLIGHT === 'true';
export type ViewType = 'single' | 'dual' | 'scroll';
@ -48,8 +48,8 @@ export const APP_CONFIG_DEFAULTS: AppConfigValues = {
footerMargin: 0,
leftMargin: 0,
rightMargin: 0,
ttsProvider: isDev ? 'custom-openai' : 'deepinfra',
ttsModel: isDev ? 'kokoro' : 'hexgrad/Kokoro-82M',
ttsProvider: process.env.NEXT_PUBLIC_DEFAULT_TTS_PROVIDER || 'custom-openai',
ttsModel: process.env.NEXT_PUBLIC_DEFAULT_TTS_MODEL || 'kokoro',
ttsInstructions: '',
savedVoices: {},
smartSentenceSplitting: true,