# CORS Middleware Configuration for FastAPI in Open Notebook: Production vs Development

> Configure FastAPI CORS for Open Notebook development and production. Learn to manage CORS origins with environment variables for secure and open access.

- Repository: [Luis Novo/open-notebook](https://github.com/lfnovo/open-notebook)
- Tags: best-practices
- Published: 2026-07-06

---

**Open Notebook configures FastAPI CORS through the `CORS_ORIGINS` environment variable, defaulting to wildcard (`*`) in development for unrestricted access while requiring explicit origin lists in production for security.**

Open Notebook is an open-source knowledge management platform built on FastAPI. Its CORS middleware configuration provides a secure yet developer-friendly approach to cross-origin requests, using environment-driven settings that automatically adapt to development and production deployments.

## How CORS Configuration Works in Open Notebook

The CORS implementation is centralized in [`api/main.py`](https://github.com/lfnovo/open-notebook/blob/main/api/main.py) and follows a parse-load-register pattern that handles both happy-path requests and error responses.

### Parsing the Environment Variable

The `_parse_cors_origins` function in [`api/main.py`](https://github.com/lfnovo/open-notebook/blob/main/api/main.py) (lines 54-60) converts the raw environment string into a list of allowed origins:

```python
def _parse_cors_origins(raw: str) -> list[str]:
    """Parse CORS_ORIGINS env value into a list of origins."""
    value = raw.strip()
    if value == "*":
        return ["*"]
    return [origin.strip() for origin in value.split(",") if origin.strip()]

```

This implementation supports comma-separated lists (e.g., `https://app.com,https://admin.com`) and treats a single asterisk as a wildcard that permits **any** origin.

### Loading Values at Startup

At module import time (lines 62-66), the application evaluates the environment and sets flags for runtime behavior:

```python
_cors_origins_raw = os.getenv("CORS_ORIGINS")
CORS_ALLOWED_ORIGINS = _parse_cors_origins(_cors_origins_raw or "*")
CORS_IS_DEFAULT_WILDCARD = _cors_origins_raw is None

```

If `CORS_ORIGINS` is unset, the system defaults to `["*"]`, enabling permissive cross-origin access suitable for local development. The `CORS_IS_DEFAULT_WILDCARD` flag tracks whether this fallback is active to trigger security warnings.

### Middleware Registration

The CORSMiddleware is registered in [`api/main.py`](https://github.com/lfnovo/open-notebook/blob/main/api/main.py) (lines 37-44) with credentials, methods, and headers fully permitted:

```python
app.add_middleware(
    CORSMiddleware,
    allow_origins=CORS_ALLOWED_ORIGINS,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

```

FastAPI processes middlewares in reverse order of registration. By adding CORSMiddleware **last**, it executes **first** on each request, ensuring CORS headers are injected before authentication or other processing layers.

### Handling CORS on Error Responses

To ensure CORS headers appear even on authentication failures or server errors, Open Notebook implements a custom exception handler (lines 49-63) that reflects the request's origin when allowed:

```python
@app.exception_handler(StarletteHTTPException)
async def custom_http_exception_handler(request: Request, exc: StarletteHTTPException):
    return JSONResponse(
        status_code=exc.status_code,
        content={"detail": exc.detail},
        headers={**(exc.headers or {}), **_cors_headers(request)},
    )

```

The helper `_cors_headers` (lines 76-94) mirrors Starlette's behavior, ensuring preflight and actual error responses both carry the appropriate `Access-Control-Allow-Origin` headers.

## Development vs Production Settings

The behavior differs significantly based on the `CORS_ORIGINS` environment variable:

| Setting | Typical Value | Effect |
|---|---|---|
| **Variable unset** | `None` (default) | `CORS_ALLOWED_ORIGINS = ["*"]` → any origin may call the API. A warning is logged at startup. |
| **Explicit origins** | `https://notebook.example.com` | Only listed origins receive the `Access-Control-Allow-Origin` header. No startup warning. |
| **Explicit wildcard** | `*` | Same as default but intention is clear; startup warning is suppressed because the variable is present. |

**Production Warning**: When `CORS_ORIGINS` is not set, Open Notebook logs a warning at startup (lines 212-221):

```python
if CORS_IS_DEFAULT_WILDCARD:
    logger.warning(
        "CORS_ORIGINS is not set — API accepts cross-origin requests from any "
        "origin (default: '*'). For production deployments, set CORS_ORIGINS to "
        "your frontend origin(s)"
    )

```

## Configuration Examples

### Production Docker Compose

Restrict CORS to your specific frontend domain:

```yaml

# docker-compose.yml

services:
  api:
    image: open-notebook/api:latest
    environment:
      - CORS_ORIGINS=https://notebook.example.com

```

### Local Development

Omit the variable to enable unrestricted access:

```bash

# .env (development)

# No CORS_ORIGINS entry → wildcard mode

OPEN_NOTEBOOK_ENCRYPTION_KEY=dev-secret

```

```bash
uv run python -m api.main

# Log output: "CORS_ORIGINS is not set — API accepts cross-origin requests from any origin"

```

### Runtime Inspection

Verify the effective configuration programmatically:

```python
from api.main import CORS_ALLOWED_ORIGINS, CORS_IS_DEFAULT_WILDCARD

print("Allowed origins:", CORS_ALLOWED_ORIGINS)
print("Using default wildcard?", CORS_IS_DEFAULT_WILDCARD)

```

## Key Implementation Files

- **[`api/main.py`](https://github.com/lfnovo/open-notebook/blob/main/api/main.py)**: Central FastAPI app creation, CORS parsing, middleware registration, and custom exception handling.
- **[`api/auth.py`](https://github.com/lfnovo/open-notebook/blob/main/api/auth.py)**: Password authentication middleware; shows how CORS preflight (`OPTIONS`) requests bypass auth checks.
- **`.env.example`**: Documents the `CORS_ORIGINS` variable and other required settings.

## Summary

- Open Notebook uses the `CORS_ORIGINS` environment variable to control FastAPI CORS middleware, defaulting to wildcard (`*`) when unset.
- The `_parse_cors_origins` function supports comma-separated origin lists and explicit wildcards.
- Registering CORSMiddleware last ensures it executes first, guaranteeing headers are present before authentication processing.
- A custom exception handler ensures CORS headers are included even on HTTP error responses.
- Always set `CORS_ORIGINS` to specific HTTPS origins in production to prevent cross-site request forgery and information leakage.

## Frequently Asked Questions

### How do I enable CORS for multiple subdomains in production?

Set `CORS_ORIGINS` to a comma-separated list of your trusted origins. For example: `CORS_ORIGINS=https://app.example.com,https://admin.example.com,https://api.example.com`. The `_parse_cors_origins` function splits on commas and trims whitespace, allowing you to authorize multiple frontends or subdomains from a single environment variable.

### Why does Open Notebook warn about the default CORS configuration?

The warning appears when `CORS_ORIGINS` is unset because the system defaults to `["*"]`, which permits any website to make cross-origin requests to your API. According to the source code in [`api/main.py`](https://github.com/lfnovo/open-notebook/blob/main/api/main.py), this default is convenient for local development but exposes production deployments to cross-site request forgery (CSRF) and unauthorized data access.

### Does the CORS middleware handle OPTIONS preflight requests?

Yes. Because `CORSMiddleware` is registered with `allow_methods=["*"]` and `allow_headers=["*"]`, it automatically handles `OPTIONS` preflight requests with appropriate `Access-Control-Allow-Methods` and `Access-Control-Allow-Headers` responses. In [`api/auth.py`](https://github.com/lfnovo/open-notebook/blob/main/api/auth.py), authentication checks typically skip these preflight requests to ensure browsers can verify permissions before sending credentials.

### What happens if I set CORS_ORIGINS to an empty string?

An empty string evaluates to falsy, triggering the same wildcard default as an unset variable. The `_parse_cors_origins` function receives an empty string (via `or "*"`), returns `["*"]`, and sets `CORS_IS_DEFAULT_WILDCARD = True`, resulting in the permissive development mode with a startup warning. To restrict access, provide at least one valid origin URL.