From 9bb066291f6a1d2434e85f344a31fda0d9ac0ce0 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 18:29:38 +0000 Subject: [PATCH 1/5] docs: improve Config docs (ENG-10642) - reflex-docgen: merge docstring Attributes sections across the MRO so inherited fields keep their base-class descriptions; drop the extra_fields workaround that listed every Config field twice on the api-reference Config page. - Add an Environment Variable column to the generated Config fields table so every REFLEX_* override (e.g. REFLEX_ENV_FILE) is listed and searchable; cross-link it from the Environment Variables page. - Remove the low-value "Key Configuration Areas" section from the Config docstring. - Rewrite the advanced-onboarding configuration page: correct the env var example (REFLEX_FRONTEND_PORT, not FRONTEND_PORT) and document env_file/python-dotenv, the app module, ports/hosts/URLs, CORS, deploy_url, path prefixes, and plugins (config and env string forms). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012ZK7KxuaTb2HePTSBFFpJp --- docs/advanced_onboarding/configuration.md | 105 +++++++++++++++++- docs/app/agent_files/_plugin.py | 52 +++++---- docs/app/reflex_docs/pages/docs/apiref.py | 17 +-- docs/app/reflex_docs/pages/docs/env_vars.py | 2 +- docs/app/reflex_docs/pages/docs/source.py | 22 +++- .../reflex-base/src/reflex_base/config.py | 15 +-- .../reflex-docgen/src/reflex_docgen/_class.py | 27 ++++- .../units/docgen/test_class_and_component.py | 43 +++++++ 8 files changed, 228 insertions(+), 55 deletions(-) diff --git a/docs/advanced_onboarding/configuration.md b/docs/advanced_onboarding/configuration.md index d538c8b7c75..bfcc8308bf4 100644 --- a/docs/advanced_onboarding/configuration.md +++ b/docs/advanced_onboarding/configuration.md @@ -23,17 +23,20 @@ config = rx.Config( ) ``` -See the [config reference](https://reflex.dev/docs/api-reference/config/) for all the parameters available. +See the [config reference](/docs/api-reference/config/) for all the parameters available. ## Environment Variables -You can override the configuration file by setting environment variables. -For example, to override the `frontend_port` setting, you can set the `FRONTEND_PORT` environment variable. +Any config parameter can be overridden by setting an environment variable with the `REFLEX_` prefix and the parameter name in uppercase. Environment variables take precedence over values set in `rxconfig.py`. + +For example, to override the `frontend_port` setting: ```bash -FRONTEND_PORT=3001 uv run reflex run +REFLEX_FRONTEND_PORT=3001 uv run reflex run ``` +The [config reference](/docs/api-reference/config/) lists the environment variable corresponding to each config parameter. Reflex also honors additional environment variables that are not config parameters — see [environment variables](/docs/api-reference/environment-variables/) for those. + ## Command Line Arguments Finally, you can override the configuration file and environment variables by passing command line arguments to `uv run reflex run`. @@ -44,6 +47,100 @@ uv run reflex run --frontend-port 3001 See the [CLI reference](/docs/api-reference/cli) for all the arguments available. +## Loading a .env File + +Set `env_file` (or the `REFLEX_ENV_FILE` environment variable) to load environment variables from a dotenv-format file before the config is read. This requires the `python-dotenv` package to be installed. + +```python +config = rx.Config( + app_name="my_app_name", + env_file=".env", +) +``` + +Multiple files can be passed separated by `os.pathsep` (`:` on Linux/macOS, `;` on Windows); when several files set the same variable, the first file in the list takes precedence. Values from an env file override variables already present in the environment. + +Because the env file is loaded before config overrides are applied, it can set any `REFLEX_*` variable, e.g. `REFLEX_FRONTEND_PORT=3001`. + +## The App Module + +`app_name` tells Reflex where your app lives: by default it imports the module `.` (the layout created by `reflex init`) and expects it to define a module-level variable named `app`, an instance of `rx.App`. + +Set `app_module_import` to load the app from a different module, e.g. a `src` layout or a package entry point: + +```python +config = rx.Config( + app_name="my_app_name", + # Equivalent to `from mypkg.main import app`. + app_module_import="mypkg.main", +) +``` + +The imported module must still define `app` at module level. + +## Ports, Hosts and URLs + +- `frontend_port` (default `3000`) and `backend_port` (default `8000`) control which ports the frontend and backend listen on. In dev mode, if a port is taken, the next available port is used. +- `backend_host` (default `0.0.0.0`) is the address the backend server binds to. +- `api_url` (default `http://localhost:8000`) is the URL the user's **browser** uses to reach the backend. Whenever the backend is not reachable at localhost from the browser — in production, or behind a reverse proxy — set `api_url` to the public backend URL. +- `deploy_url` (default `http://localhost:3000`) is the public URL where the **frontend** is hosted. Reflex uses it wherever an absolute frontend URL is needed — most notably for the links in the generated `sitemap.xml`. It is also the origin browsers will present when connecting to the backend, which matters for CORS (below). + +## CORS + +The backend only accepts cross-origin requests from origins listed in `cors_allowed_origins`. The default, `["*"]`, allows any origin — convenient in development, but in production restrict it to the origins your frontend is actually served from (typically the origin of `deploy_url`): + +```python +config = rx.Config( + app_name="my_app_name", + api_url="https://api.example.com", + deploy_url="https://example.com", + cors_allowed_origins=["https://example.com"], +) +``` + +The setting applies to both regular HTTP endpoints and the WebSocket connection. As an environment variable, pass a comma-separated list: + +```bash +REFLEX_CORS_ALLOWED_ORIGINS="https://example.com,https://www.example.com" uv run reflex run +``` + +## Path Prefixes + +By default the frontend is served from the root of its domain and backend routes are mounted at the root of the backend server. Two settings change that, e.g. when the app shares a domain with other services behind a reverse proxy: + +- `frontend_path` serves the frontend under a sub-path: `frontend_path="/app"` serves the frontend at `http://localhost:3000/app`. +- `backend_path` mounts all backend routes under a prefix: `backend_path="/api"` mounts the event WebSocket and the `/ping`, `/_upload`, `/_health`, and `/_all_routes` endpoints under `/api`. The prefix is automatically included in the backend URLs baked into the frontend. Routes are registered at startup, so changing it requires a full `reflex run` restart. + +Both values are normalized to start with a `/`. + +## Plugins + +Plugins extend the Reflex compiler. Add plugin instances via the `plugins` parameter, and disable plugins that are enabled by default (like the sitemap plugin) with `disable_plugins`: + +```python +config = rx.Config( + app_name="my_app_name", + plugins=[ + rx.plugins.SitemapPlugin(), + rx.plugins.TailwindV4Plugin(), + ], + # Or turn off a default-enabled plugin: + # disable_plugins=[rx.plugins.SitemapPlugin], +) +``` + +Plugins can also be specified in the environment as fully qualified import paths, separated by `:`. Plugins specified this way are instantiated without arguments, so plugins that require constructor arguments must be configured in `rxconfig.py`. + +- `REFLEX_PLUGINS` **replaces** the `plugins` list from `rxconfig.py`. +- `REFLEX_EXTRA_PLUGINS` **appends** to the configured plugins, skipping any that are already configured or disabled. +- `REFLEX_DISABLE_PLUGINS` lists plugin classes to disable. + +```bash +REFLEX_EXTRA_PLUGINS="reflex.plugins.SitemapPlugin" uv run reflex run +``` + +See the [plugins reference](/docs/api-reference/plugins/) for the available plugins and how to write your own. + ## Customizable App Data Directory The `REFLEX_DIR` environment variable can be set, which allows users to set the location where Reflex writes helper tools like Bun and NodeJS. diff --git a/docs/app/agent_files/_plugin.py b/docs/app/agent_files/_plugin.py index 758d836a2fa..8771990f83f 100644 --- a/docs/app/agent_files/_plugin.py +++ b/docs/app/agent_files/_plugin.py @@ -492,6 +492,7 @@ def generate_api_reference_markdown_content( class_fields: Sequence[tuple[str, str, str]], fields: Sequence[tuple[str, str, str]], methods: Sequence[tuple[str, str]], + env_var_prefix: str | None = None, ) -> str: """Generate markdown content for a dynamic API reference page. @@ -502,6 +503,8 @@ def generate_api_reference_markdown_content( class_fields: The class field rows as name, type, description. fields: The field rows as name, type, description. methods: The method rows as signature, description. + env_var_prefix: If set, add a column listing the environment variable + (prefix + field name in uppercase) that overrides each field. Returns: The generated markdown content. @@ -520,16 +523,26 @@ def generate_api_reference_markdown_content( ("Class Fields", class_fields), ("Fields", fields), ): - table = _markdown_table( - ["Prop", "Description"], - [ + if env_var_prefix is not None and heading == "Fields": + headers = ["Prop", "Environment Variable", "Description"] + rows = [ ( f"`{name}: {type_display}`", + f"`{env_var_prefix}{name.upper()}`", field_description, ) for name, type_display, field_description in field_rows - ], - ) + ] + else: + headers = ["Prop", "Description"] + rows = [ + ( + f"`{name}: {type_display}`", + field_description, + ) + for name, type_display, field_description in field_rows + ] + table = _markdown_table(headers, rows) if table: lines.extend([f"## {heading}", "", *table, ""]) @@ -554,7 +567,7 @@ def generate_class_api_reference_markdown( url_path: Path, title: str, cls: type, - extra_fields: Sequence[object] = (), + env_var_prefix: str | None = None, ) -> tuple[Path, str]: """Generate a dynamic class API reference markdown asset. @@ -562,18 +575,15 @@ def generate_class_api_reference_markdown( url_path: The public markdown asset path. title: The page title. cls: The class to document. - extra_fields: Extra docgen fields to include. + env_var_prefix: If set, list the environment variable that overrides + each field (prefix + field name in uppercase). Returns: The public path and generated markdown content. """ - from reflex_docgen import FieldDocumentation, generate_class_documentation + from reflex_docgen import generate_class_documentation doc = generate_class_documentation(cls) - fields = ( - *doc.fields, - *(field for field in extra_fields if isinstance(field, FieldDocumentation)), - ) return ( url_path, generate_api_reference_markdown_content( @@ -586,12 +596,13 @@ def generate_class_api_reference_markdown( ), fields=tuple( (field.name, field.type_display, field.description or "") - for field in fields + for field in doc.fields ), methods=tuple( (method.name + method.signature, method.description or "") for method in doc.methods ), + env_var_prefix=env_var_prefix, ), ) @@ -654,7 +665,7 @@ def env_var_docstring(name: str) -> str: "Reflex provides a number of environment variables that can be used to configure the behavior of your application.", "These environment variables can be set in your shell environment or in a `.env` file.", "", - "This page documents all available environment variables in Reflex.", + "This page documents the environment variables that are not config parameters. Environment variables that override `rx.Config` parameters (e.g. `REFLEX_FRONTEND_PORT`) are listed in the [config reference](/docs/api-reference/config/).", "", "## Environment Variables", "", @@ -676,13 +687,12 @@ def generate_dynamic_api_reference_files() -> tuple[tuple[Path, str], ...]: import reflex as rx from reflex.istate.manager import StateManager from reflex.utils.imports import ImportVar - from reflex_docgen import generate_class_documentation modules = [ rx.App, rx.Component, rx.ComponentState, - (rx.Config, rx.config.BaseConfig), + rx.Config, rx.event.Event, rx.event.EventHandler, rx.event.EventSpec, @@ -692,20 +702,18 @@ def generate_dynamic_api_reference_files() -> tuple[tuple[Path, str], ...]: ImportVar, rx.Var, ] + # Classes whose fields can be overridden via prefixed environment variables; + # the fields table gets an extra column listing each generated env var name. + env_var_prefixes = {rx.Config: "REFLEX_"} files = [] for module in modules: - extra_fields: list[object] = [] - if isinstance(module, tuple): - module, *extra_modules = module - for extra_module in extra_modules: - extra_fields.extend(generate_class_documentation(extra_module).fields) slug = module.__name__.lower() files.append( generate_class_api_reference_markdown( url_path=Path(f"api-reference/{slug}.md"), title=_format_title(slug), cls=module, - extra_fields=tuple(extra_fields), + env_var_prefix=env_var_prefixes.get(module), ) ) files.append(generate_environment_variables_markdown()) diff --git a/docs/app/reflex_docs/pages/docs/apiref.py b/docs/app/reflex_docs/pages/docs/apiref.py index 548a030571e..5ea4bd09ec9 100644 --- a/docs/app/reflex_docs/pages/docs/apiref.py +++ b/docs/app/reflex_docs/pages/docs/apiref.py @@ -1,7 +1,6 @@ import reflex as rx from reflex.istate.manager import StateManager from reflex.utils.imports import ImportVar -from reflex_docgen import generate_class_documentation from reflex_docs.templates.docpage import docpage @@ -11,7 +10,7 @@ rx.App, rx.Component, rx.ComponentState, - (rx.Config, rx.config.BaseConfig), + rx.Config, rx.event.Event, rx.event.EventHandler, rx.event.EventSpec, @@ -24,20 +23,16 @@ rx.Var, ] +# Classes whose fields can be overridden via prefixed environment variables; +# the fields table gets an extra column listing each generated env var name. +env_var_prefixes = {rx.Config: "REFLEX_"} + from .env_vars import env_vars_doc pages = [] for module in modules: - if isinstance(module, tuple): - module, *extra_modules = module - extra_fields = () - for extra_module in extra_modules: - extra_doc = generate_class_documentation(extra_module) - extra_fields = extra_fields + extra_doc.fields - else: - extra_fields = None name = module.__name__.lower() - docs = generate_docs(name, module, extra_fields=extra_fields) + docs = generate_docs(name, module, env_var_prefix=env_var_prefixes.get(module)) title = name.replace("_", " ").title() page_data = docpage(f"/api-reference/{name}/", title)(docs) # Keep the short sidebar/nav label (e.g. "App"), but emit a descriptive HTML diff --git a/docs/app/reflex_docs/pages/docs/env_vars.py b/docs/app/reflex_docs/pages/docs/env_vars.py index 1d34426c9f4..322a728a8c7 100644 --- a/docs/app/reflex_docs/pages/docs/env_vars.py +++ b/docs/app/reflex_docs/pages/docs/env_vars.py @@ -150,7 +150,7 @@ def env_vars_page(): Reflex provides a number of environment variables that can be used to configure the behavior of your application. These environment variables can be set in your shell environment or in a `.env` file. - This page documents all available environment variables in Reflex. + This page documents the environment variables that are not config parameters. Environment variables that override `rx.Config` parameters (e.g. `REFLEX_FRONTEND_PORT`) are listed in the [config reference](/docs/api-reference/config/). """ ), h2_comp(text="Environment Variables"), diff --git a/docs/app/reflex_docs/pages/docs/source.py b/docs/app/reflex_docs/pages/docs/source.py index 694931f6d21..f18c9414510 100644 --- a/docs/app/reflex_docs/pages/docs/source.py +++ b/docs/app/reflex_docs/pages/docs/source.py @@ -23,7 +23,10 @@ def format_field(field: FieldDocumentation) -> rx.Component: def format_fields( headers: list[str], fields: tuple[FieldDocumentation, ...], + env_var_prefix: str | None = None, ) -> rx.Component: + if env_var_prefix is not None: + headers = [headers[0], "Environment Variable", *headers[1:]] return ( rx.table.root( rx.table.header( @@ -40,6 +43,18 @@ def format_fields( rx.table.cell( format_field(field), ), + *( + [ + rx.table.cell( + rx.code( + f"{env_var_prefix}{field.name.upper()}", + class_name="code-style", + ), + ) + ] + if env_var_prefix is not None + else [] + ), rx.table.cell( render_markdown(field.description or ""), class_name="font-small text-secondary-11", @@ -85,10 +100,9 @@ def format_methods(methods: tuple[MethodDocumentation, ...]) -> rx.Component: def generate_docs( title: str, cls: type, - extra_fields: tuple[FieldDocumentation, ...] | None = None, + env_var_prefix: str | None = None, ) -> rx.Component: doc = generate_class_documentation(cls) - fields = doc.fields + (extra_fields or ()) return rx.box( h1_comp(text=title.title()), @@ -107,10 +121,10 @@ def generate_docs( ( rx.box( h2_comp(text="Fields"), - format_fields(["Prop", "Description"], fields), + format_fields(["Prop", "Description"], doc.fields, env_var_prefix), overflow="auto", ) - if fields + if doc.fields else rx.fragment() ), rx.box( diff --git a/packages/reflex-base/src/reflex_base/config.py b/packages/reflex-base/src/reflex_base/config.py index be02f00a937..d9d35a2a6f0 100644 --- a/packages/reflex-base/src/reflex_base/config.py +++ b/packages/reflex-base/src/reflex_base/config.py @@ -158,7 +158,7 @@ class BaseConfig: backend_port: The port to run the backend on. NOTE: When running in dev mode, the next available port will be used if this is taken. backend_path: The path prefix for backend routes. For example, "/api" mounts the event websocket, /ping, /_upload, /_health, and /_all_routes under /api, and is automatically included in URLs baked into the frontend. Changing this requires a full `reflex run` restart — routes are registered at startup. api_url: The backend url the frontend will connect to. This must be updated if the backend is hosted elsewhere, or in production. - deploy_url: The url the frontend will be hosted on. + deploy_url: The url the frontend will be hosted on. Used to build absolute frontend URLs, e.g. links in the generated sitemap.xml. backend_host: The url the backend will be hosted on. db_url: The database url used by rx.Model. async_db_url: The async database url used by rx.Model. @@ -176,7 +176,7 @@ class BaseConfig: redis_lock_expiration: Maximum expiration lock time for redis state manager. redis_lock_warning_threshold: Maximum lock time before warning for redis state manager. redis_token_expiration: Token expiration time for redis state manager. - env_file: Path to file containing key-values pairs to override in the environment; Dotenv format. + env_file: Path to file containing key-values pairs to load into the environment; Dotenv format. Multiple files may be separated by os.pathsep. Requires the python-dotenv package. state_auto_setters: Whether to automatically create setters for state base vars. default_color_mode: The default color mode for the app: "system" (follow the OS preference), "light", or "dark". Applies to the built-in color mode switcher and `color_mode_cond` without requiring a radix theme. show_built_with_reflex: Whether to display the sticky "Built with Reflex" badge on all pages. @@ -319,16 +319,7 @@ class Config(BaseConfig): REFLEX_FRONTEND_PORT=3001 reflex run ``` - ## Key Configuration Areas - - - **App Settings**: `app_name`, `loglevel`, `telemetry_enabled` - - **Server**: `frontend_port`, `backend_port`, `api_url`, `cors_allowed_origins` - - **Database**: `db_url`, `async_db_url`, `redis_url` - - **Frontend**: `frontend_packages`, `react_strict_mode`, `frontend_compression_formats` - - **State Management**: `state_manager_mode`, `state_auto_setters` - - **Plugins**: `plugins`, `disable_plugins` - - See the [configuration docs](https://reflex.dev/docs/advanced-onboarding/configuration) for complete details on all available options. + See the [configuration docs](https://reflex.dev/docs/advanced-onboarding/configuration) for a guided overview of the most commonly tweaked settings. """ # Track whether the app name has already been validated for this Config instance. diff --git a/packages/reflex-docgen/src/reflex_docgen/_class.py b/packages/reflex-docgen/src/reflex_docgen/_class.py index ae3bba1ac94..2a462999bd9 100644 --- a/packages/reflex-docgen/src/reflex_docgen/_class.py +++ b/packages/reflex-docgen/src/reflex_docgen/_class.py @@ -114,6 +114,31 @@ def _attributes_from_sections(sections: list[Any]) -> dict[str, str]: } +def _attributes_from_mro(cls: type, sections: list[Any]) -> dict[str, str]: + """Merge Attributes sections from the class and its base classes. + + Fields are inherited from base classes, so their descriptions must be too + (e.g. ``Config`` inherits every documented field from ``BaseConfig``). Bases + are merged in reverse MRO order so a class closer to ``cls`` wins, and + ``cls``'s own docstring (already parsed as ``sections``) wins over all bases. + + Args: + cls: The class whose MRO to walk. + sections: The parsed docstring sections of ``cls`` itself. + + Returns: + A mapping from attribute name to description string. + """ + attrs: dict[str, str] = {} + for base in reversed(cls.__mro__[1:]): + # Only parse docstrings defined on the base itself; object and + # undocumented bases contribute nothing. + if base is not object and base.__dict__.get("__doc__"): + attrs.update(_attributes_from_sections(_parse_docstring_sections(base))) + attrs.update(_attributes_from_sections(sections)) + return attrs + + def _description_from_sections(sections: list[Any]) -> str | None: """Join the prose body of a docstring, excluding the Attributes section. @@ -556,7 +581,7 @@ def generate_class_documentation(cls: type) -> ClassDocumentation: try: sections = _parse_docstring_sections(cls) description = _description_from_sections(sections) - docstring_attrs = _attributes_from_sections(sections) + docstring_attrs = _attributes_from_mro(cls, sections) if dataclasses.is_dataclass(cls): fields = _get_dataclass_fields(cls, docstring_attrs) diff --git a/tests/units/docgen/test_class_and_component.py b/tests/units/docgen/test_class_and_component.py index 6353da70bbf..691a9cb64e8 100644 --- a/tests/units/docgen/test_class_and_component.py +++ b/tests/units/docgen/test_class_and_component.py @@ -203,6 +203,49 @@ def test_private_marker_in_doc(): assert "hidden" not in field_names +@dataclasses.dataclass +class _SubclassOfSampleDataclass(_SampleDataclass): + """A subclass whose docstring documents only its own field. + + Attributes: + extra: A field defined on the subclass. + name: Overridden description for name. + """ + + extra: int = 0 + + +def test_inherited_fields_get_base_docstring_descriptions(): + """Fields inherited from a base class keep the base docstring descriptions. + + Regression: only the class's own docstring was consulted, so inherited fields + had no description; the docs site worked around it by appending the base + class's fields, listing every field twice on the Config API reference page. + """ + doc = generate_class_documentation(_SubclassOfSampleDataclass) + fields_by_name = {f.name: f for f in doc.fields} + + # Each field appears exactly once. + assert len(doc.fields) == len(fields_by_name) + # Inherited field: description comes from the base class docstring. + assert fields_by_name["items"].description == "A list of items." + # The subclass docstring wins for fields it re-documents. + assert fields_by_name["name"].description == "Overridden description for name." + # Field defined and documented on the subclass itself. + assert fields_by_name["extra"].description == "A field defined on the subclass." + + +def test_config_fields_documented_once_with_descriptions(): + """Every rx.Config field is listed once and carries its BaseConfig description.""" + import reflex as rx + + doc = generate_class_documentation(rx.Config) + names = [f.name for f in doc.fields] + assert len(names) == len(set(names)) + undocumented = [f.name for f in doc.fields if not f.description] + assert not undocumented, f"Config fields missing descriptions: {undocumented}" + + def test_dataclass_methods(): """Methods: name, signature, and truncated description are correct.""" doc = generate_class_documentation(_SampleDataclass) From 202768aad867fd4be5884b0ea4b7ec3d07f64a30 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 23:56:16 +0000 Subject: [PATCH 2/5] docs: api_url only needs setting when backend address differs from frontend The frontend substitutes its own domain when api_url points at localhost, so a backend reachable at the same address as the frontend works without setting api_url; clarify this on the onboarding page and in the Config docstring. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012ZK7KxuaTb2HePTSBFFpJp --- docs/advanced_onboarding/configuration.md | 2 +- packages/reflex-base/src/reflex_base/config.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/advanced_onboarding/configuration.md b/docs/advanced_onboarding/configuration.md index bfcc8308bf4..68f2eb5306a 100644 --- a/docs/advanced_onboarding/configuration.md +++ b/docs/advanced_onboarding/configuration.md @@ -82,7 +82,7 @@ The imported module must still define `app` at module level. - `frontend_port` (default `3000`) and `backend_port` (default `8000`) control which ports the frontend and backend listen on. In dev mode, if a port is taken, the next available port is used. - `backend_host` (default `0.0.0.0`) is the address the backend server binds to. -- `api_url` (default `http://localhost:8000`) is the URL the user's **browser** uses to reach the backend. Whenever the backend is not reachable at localhost from the browser — in production, or behind a reverse proxy — set `api_url` to the public backend URL. +- `api_url` (default `http://localhost:8000`) is the URL the user's **browser** uses to reach the backend. You typically don't need to set it: when `api_url` points at localhost, the frontend substitutes the domain the app was served from, so a backend reachable at the same address as the frontend (e.g. behind a reverse proxy or load balancer) is found automatically. Set `api_url` only when the backend is listening on a different address than the frontend, e.g. `https://api.example.com`. - `deploy_url` (default `http://localhost:3000`) is the public URL where the **frontend** is hosted. Reflex uses it wherever an absolute frontend URL is needed — most notably for the links in the generated `sitemap.xml`. It is also the origin browsers will present when connecting to the backend, which matters for CORS (below). ## CORS diff --git a/packages/reflex-base/src/reflex_base/config.py b/packages/reflex-base/src/reflex_base/config.py index d9d35a2a6f0..c2534dae91c 100644 --- a/packages/reflex-base/src/reflex_base/config.py +++ b/packages/reflex-base/src/reflex_base/config.py @@ -157,7 +157,7 @@ class BaseConfig: frontend_path: The path to run the frontend on. For example, "/app" will run the frontend on http://localhost:3000/app backend_port: The port to run the backend on. NOTE: When running in dev mode, the next available port will be used if this is taken. backend_path: The path prefix for backend routes. For example, "/api" mounts the event websocket, /ping, /_upload, /_health, and /_all_routes under /api, and is automatically included in URLs baked into the frontend. Changing this requires a full `reflex run` restart — routes are registered at startup. - api_url: The backend url the frontend will connect to. This must be updated if the backend is hosted elsewhere, or in production. + api_url: The backend url the frontend will connect to. Only needs to be set when the backend is listening on a different address than the frontend. deploy_url: The url the frontend will be hosted on. Used to build absolute frontend URLs, e.g. links in the generated sitemap.xml. backend_host: The url the backend will be hosted on. db_url: The database url used by rx.Model. From f5c971de066555ad8360dd281ca960f77719033c Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Tue, 28 Jul 2026 15:02:44 -0700 Subject: [PATCH 3/5] render_markdown helper automatically uses textwrap.dedent avoid accidentally rendering inline triple-quoted strings as code block when passing them to `render_markdown` --- docs/app/reflex_docs/pages/docs/component.py | 4 +-- .../src/reflex_site_shared/docs/markdown.py | 5 ++++ .../reflex_site_shared/docs/test_markdown.py | 27 +++++++++++++++++++ 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/docs/app/reflex_docs/pages/docs/component.py b/docs/app/reflex_docs/pages/docs/component.py index 5c58b21e77f..9fd40ab9076 100644 --- a/docs/app/reflex_docs/pages/docs/component.py +++ b/docs/app/reflex_docs/pages/docs/component.py @@ -885,9 +885,7 @@ def component_docs( return rx.box( h2_comp(text=comp_display_name), - rx.box( - render_markdown(textwrap.dedent(doc.description or "")), class_name="pb-2" - ), + rx.box(render_markdown(doc.description or ""), class_name="pb-2"), props, children, triggers, diff --git a/packages/reflex-site-shared/src/reflex_site_shared/docs/markdown.py b/packages/reflex-site-shared/src/reflex_site_shared/docs/markdown.py index ad7b983a064..f7206c6f8e7 100644 --- a/packages/reflex-site-shared/src/reflex_site_shared/docs/markdown.py +++ b/packages/reflex-site-shared/src/reflex_site_shared/docs/markdown.py @@ -5,6 +5,7 @@ import json import sys +import textwrap import types from pathlib import Path @@ -893,6 +894,7 @@ def render_markdown( *, virtual_filepath: str = "", filename: str = "", + dedent: bool = True, ) -> rx.Component: """Render a Markdown string into Reflex components. @@ -900,10 +902,13 @@ def render_markdown( text: Markdown source to render. virtual_filepath: Stable document identity used by executable blocks. filename: Optional source filename included in execution errors. + dedent: If True, remove common leading whitespace from the source. Returns: The rendered component tree. """ + if dedent: + text = textwrap.dedent(text) doc = parse_document(text) transformer = ReflexDocTransformer( virtual_filepath=virtual_filepath, diff --git a/tests/units/reflex_site_shared/docs/test_markdown.py b/tests/units/reflex_site_shared/docs/test_markdown.py index 47b3d59fa09..6df87548772 100644 --- a/tests/units/reflex_site_shared/docs/test_markdown.py +++ b/tests/units/reflex_site_shared/docs/test_markdown.py @@ -192,6 +192,33 @@ def demo_only_example(): assert "Demo only output" in rendered +def test_render_markdown_dedent_kwarg_controls_whitespace_normalization() -> None: + """Allow callers to opt out of dedenting already formatted markdown.""" + source = """ ```python exec + import reflex as rx + + def answer_component(): + return rx.text(str(6 * 7)) + ``` + + ```python eval + answer_component() + ``` +""" + + dedented = str(render_markdown(source, virtual_filepath="tests/dedent-true.md")) + not_dedented = str( + render_markdown( + source, + virtual_filepath="tests/dedent-false.md", + dedent=False, + ) + ) + + assert "42" in dedented + assert "42" not in not_dedented + + def test_toc_helpers_extract_heading_levels() -> None: """Extract heading levels and labels from Markdown source.""" source = "# One\n\n## Two\n\nBody text.\n\n### Three\n" From 87b2ee731a6e3a6b24d04c96d1e72cf824c6b0db Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Tue, 28 Jul 2026 15:03:42 -0700 Subject: [PATCH 4/5] filter internal env vars by name starting with "__" the `internal` flag is not preserved through the `env_var` -> `EnvVar` translation, so we can't key off of it. Instead, all internal env vars get "__" prepended to the name, so just use that to make the determination. --- docs/app/reflex_docs/pages/docs/env_vars.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/app/reflex_docs/pages/docs/env_vars.py b/docs/app/reflex_docs/pages/docs/env_vars.py index 322a728a8c7..9ab106ad1dc 100644 --- a/docs/app/reflex_docs/pages/docs/env_vars.py +++ b/docs/app/reflex_docs/pages/docs/env_vars.py @@ -69,7 +69,7 @@ def generate_env_var_table(cls, include_internal: bool = False) -> rx.Component: env_vars = [ (name, var) for name, var in env_vars - if not getattr(var, "internal", False) + if not getattr(var, "name", "").startswith("__") ] env_vars.sort(key=lambda x: x[0]) From 22273e4a54431b9c189d3d7e26afc00ec73b1f2b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Jul 2026 22:23:03 +0000 Subject: [PATCH 5/5] docs: stack table descriptions below fields on narrow containers The Config fields table (with its env var column) and the Environment Variables table were too wide for normal displays, forcing horizontal scrolling just to read descriptions. Using Tailwind container queries, the description column now stays in-row only when the container is wide enough; below that each entry renders as two lines, with the description spanning the full table width on the second line. The radix row divider is suppressed between the two lines of one logical row so entries stay visually grouped. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012ZK7KxuaTb2HePTSBFFpJp --- docs/app/reflex_docs/pages/docs/env_vars.py | 76 ++++------ docs/app/reflex_docs/pages/docs/source.py | 150 +++++++++++++++----- 2 files changed, 143 insertions(+), 83 deletions(-) diff --git a/docs/app/reflex_docs/pages/docs/env_vars.py b/docs/app/reflex_docs/pages/docs/env_vars.py index 9ab106ad1dc..66188458906 100644 --- a/docs/app/reflex_docs/pages/docs/env_vars.py +++ b/docs/app/reflex_docs/pages/docs/env_vars.py @@ -3,6 +3,8 @@ from __future__ import annotations import inspect +import itertools +from functools import partial from typing import Any, List, Optional, Tuple import reflex as rx @@ -11,6 +13,8 @@ from reflex_docs.docgen_pipeline import render_markdown from reflex_docs.templates.docpage import docpage, h1_comp, h2_comp +from .source import stacked_description_header, stacked_description_rows + class EnvVarDocs: """Documentation for Reflex environment variables.""" @@ -74,62 +78,40 @@ def generate_env_var_table(cls, include_internal: bool = False) -> rx.Component: env_vars.sort(key=lambda x: x[0]) + def env_var_rows(name: str, var: Any) -> tuple[rx.Component, rx.Component]: + type_name = str( + var.type_.__name__ if hasattr(var.type_, "__name__") else str(var.type_) + ) + return stacked_description_rows( + [ + (rx.code(var.name, class_name="code-style"), "@4xl:w-[20%]"), + (rx.code(type_name, class_name="code-style"), "@4xl:w-[15%]"), + ( + rx.code(str(var.default), class_name="code-style"), + "@4xl:w-[15%]", + ), + ], + partial(render_markdown, cls.get_env_var_docstring(name) or ""), + "4xl", + description_cell_class="@4xl:w-[50%]", + ) + return rx.box( rx.table.root( - rx.table.header( - rx.table.row( - rx.table.column_header_cell( - "Name", - class_name="font-small text-secondary-12 text-normal w-[20%] justify-start pl-4 font-bold", - ), - rx.table.column_header_cell( - "Type", - class_name="font-small text-secondary-12 text-normal w-[15%] justify-start pl-4 font-bold", - ), - rx.table.column_header_cell( - "Default", - class_name="font-small text-secondary-12 text-normal w-[15%] justify-start pl-4 font-bold", - ), - rx.table.column_header_cell( - "Description", - class_name="font-small text-secondary-12 text-normal w-[50%] justify-start pl-4 font-bold", - ), - ) + stacked_description_header( + ["Name", "Type", "Default", "Description"], + "4xl", ), rx.table.body( - *[ - rx.table.row( - rx.table.cell( - rx.code(var.name, class_name="code-style"), - class_name="w-[20%]", - ), - rx.table.cell( - rx.code( - str( - var.type_.__name__ - if hasattr(var.type_, "__name__") - else str(var.type_) - ), - class_name="code-style", - ), - class_name="w-[15%]", - ), - rx.table.cell( - rx.code(str(var.default), class_name="code-style"), - class_name="w-[15%]", - ), - rx.table.cell( - render_markdown(cls.get_env_var_docstring(name) or ""), - class_name="font-small text-secondary-11 w-[50%]", - ), - ) - for name, var in env_vars - ], + *itertools.chain.from_iterable( + env_var_rows(name, var) for name, var in env_vars + ), ), width="100%", overflow_x="visible", class_name="w-full", ), + class_name="@container", ) diff --git a/docs/app/reflex_docs/pages/docs/source.py b/docs/app/reflex_docs/pages/docs/source.py index f18c9414510..27b0916f932 100644 --- a/docs/app/reflex_docs/pages/docs/source.py +++ b/docs/app/reflex_docs/pages/docs/source.py @@ -1,3 +1,6 @@ +import itertools +from collections.abc import Callable, Sequence + import reflex as rx from reflex_docgen import ( FieldDocumentation, @@ -12,6 +15,8 @@ "font-small text-secondary-12 text-normal w-auto justify-start pl-4 font-bold" ) +description_cell_class_name = "font-small text-secondary-11" + def format_field(field: FieldDocumentation) -> rx.Component: type_str = field.type_display @@ -20,49 +25,120 @@ def format_field(field: FieldDocumentation) -> rx.Component: return rx.code(field.name, ": ", type_str, class_name="code-style") +def stacked_description_rows( + leading_cells: Sequence[tuple[rx.Component, str]], + description: Callable[[], rx.Component], + breakpoint: str, + description_cell_class: str = "", +) -> tuple[rx.Component, rx.Component]: + """Build a table row whose description column stacks on narrow containers. + + On containers at least as wide as the Tailwind ``breakpoint``, the + description renders as the last cell of a single row. Below that, it + drops to a second full-width row so the table never needs horizontal + scrolling just to read descriptions. The nearest ancestor with the + ``@container`` class defines the measured width. + + Args: + leading_cells: The non-description cells as (content, extra cell classes). + description: Description factory, called once per rendered copy. + breakpoint: Tailwind container breakpoint (e.g. ``"4xl"``) above which + the description stays in-row. + description_cell_class: Extra classes for the in-row description cell. + + Returns: + The main row and the narrow-only description row. + """ + return ( + rx.table.row( + *[ + rx.table.cell( + content, + # When stacked, the description row below carries the divider. + class_name=f"{extra_class} @max-{breakpoint}:shadow-none".strip(), + ) + for content, extra_class in leading_cells + ] + + [ + rx.table.cell( + description(), + class_name=f"{description_cell_class_name} {description_cell_class} hidden @{breakpoint}:table-cell", + ), + ] + ), + rx.table.row( + rx.table.cell( + description(), + col_span=len(leading_cells), + class_name=description_cell_class_name, + ), + class_name=f"@{breakpoint}:hidden", + ), + ) + + +def stacked_description_header( + headers: list[str], + breakpoint: str, + header_class: str = table_header_class_name, +) -> rx.Component: + """Build a table header whose last (description) column hides when stacked. + + Args: + headers: All column headers; the last one is the description column. + breakpoint: Tailwind container breakpoint matching the body rows. + header_class: Base classes for every header cell. + + Returns: + The table header component. + """ + return rx.table.header( + rx.table.row( + *[ + rx.table.column_header_cell(header, class_name=header_class) + for header in headers[:-1] + ] + + [ + rx.table.column_header_cell( + headers[-1], + class_name=f"{header_class} hidden @{breakpoint}:table-cell", + ), + ] + ) + ) + + def format_fields( headers: list[str], fields: tuple[FieldDocumentation, ...], env_var_prefix: str | None = None, ) -> rx.Component: + # A table with the extra env var column needs more room to fit the + # description in-row, so it stacks at a wider container breakpoint. + breakpoint = "4xl" if env_var_prefix is not None else "2xl" if env_var_prefix is not None: headers = [headers[0], "Environment Variable", *headers[1:]] - return ( - rx.table.root( - rx.table.header( - rx.table.row(*[ - rx.table.column_header_cell( - header, class_name=table_header_class_name - ) - for header in headers - ]) - ), - rx.table.body( - *[ - rx.table.row( - rx.table.cell( - format_field(field), - ), - *( - [ - rx.table.cell( - rx.code( - f"{env_var_prefix}{field.name.upper()}", - class_name="code-style", - ), - ) - ] - if env_var_prefix is not None - else [] - ), - rx.table.cell( - render_markdown(field.description or ""), - class_name="font-small text-secondary-11", - ), - ) - for field in fields - ], - ), + + def field_rows(field: FieldDocumentation) -> tuple[rx.Component, rx.Component]: + leading_cells = [(format_field(field), "")] + if env_var_prefix is not None: + leading_cells.append(( + rx.code( + f"{env_var_prefix}{field.name.upper()}", + class_name="code-style", + ), + "", + )) + return stacked_description_rows( + leading_cells, + lambda: render_markdown(field.description or ""), + breakpoint, + ) + + return rx.table.root( + stacked_description_header(headers, breakpoint), + rx.table.body( + *itertools.chain.from_iterable(field_rows(field) for field in fields), ), ) @@ -114,6 +190,7 @@ def generate_docs( h2_comp(text="Class Fields"), format_fields(["Prop", "Description"], doc.class_fields), overflow="auto", + class_name="@container", ) if doc.class_fields else rx.fragment() @@ -123,6 +200,7 @@ def generate_docs( h2_comp(text="Fields"), format_fields(["Prop", "Description"], doc.fields, env_var_prefix), overflow="auto", + class_name="@container", ) if doc.fields else rx.fragment()