Skip to content

Managing Secrets and .env

The application now reads three values out of the environment: DATABASE_URL, DEBUG, and JWT_SECRET_KEY. The first two are mostly local dev-convenience settings; the third is a real secret.

.env Is Not a Secret Store

.env makes working in dev easier.

pydantic-settings reads it on startup, populates the Settings model, and the application moves on.

Looks similar every Python project that uses environment variables, and using it locally avoids polluting the shell with project-specific exports.

What it's not:

  • It's not where production secrets live.
    • Production secrets come from the deployment platform (a managed secrets service, the platform's environment-variable feature, or a sidecar that mounts a secrets volume).
  • It's not in version control.
    • .env is gitignored from the very first commit, no exceptions. Once a secret hits the git history, it has to be rotated, even if you git rm it in the next commit.
  • It's not an authoritative documentation file.
    • A separate .env.example is what describes the variables the app needs.

.gitignore and .env.example

Both files belong at the project root.

.gitignore
.env
.env.example
# Copy this file to `.env` and fill in real values. `.env` is gitignored.

DATABASE_URL=postgresql+psycopg://release_tracker:release_tracker@localhost:5432/release_tracker
DEBUG=true

# Generate with: uv run python -c "import secrets; print(secrets.token_urlsafe(32))"
JWT_SECRET_KEY=replace-this-with-a-32+-char-random-value

.env.example documents the variables, gives placeholder values for the non-secret ones, and shows how to generate the secret value.

New contributors run cp .env.example .env and generate a JWT_SECRET_KEY.

What Counts as a Secret

A useful threshold: if a leak would force a rotation, it's a secret. Apply this to the release tracker:

  • JWT_SECRET_KEY:

    Secret. Anyone who has it can mint tokens for any user.

  • DATABASE_URL:

    Usually a secret in production, because it embeds the database password. The local-dev value (release_tracker:release_tracker@localhost) is harmless, but the production form (with a managed-Postgres password) needs the same handling as the JWT key.

  • DEBUG:

    Not a secret, but production should always have it false. A true flag in production exposes verbose tracebacks and changes log levels in ways an attacker finds useful.

Production Patterns

Once the application is in a deployment platform, .env is no longer relevant. The platform supplies environment variables directly.



Validation at Startup

The Settings field declared alongside the authentication config already does a lot of the work:

src/release_tracker/config.py
class Settings(BaseSettings):
    database_url: str = DEFAULT_DATABASE_URL
    debug: bool = False
    jwt_secret_key: SecretStr = Field(min_length=32)
    ...

To read the actual value, code calls .get_secret_value():

src/release_tracker/security.py
return jwt.encode(
    payload,
    get_settings().jwt_secret_key.get_secret_value(),
    algorithm=JWT_ALGORITHM,
)

The explicit method name forces a developer to think about the call site every time the secret leaves the settings object.

Tests Set the Secret Themselves

Tests can't depend on .env, since CI runs in a clean shell. The pattern from the authentication conftest.py sets a fixed test value before importing anything that touches settings:

tests/conftest.py
import os

os.environ.setdefault(
    "JWT_SECRET_KEY",
    "test-secret-key-for-pytest-only-32-bytes",
)

from release_tracker import crud  # imports happen after the env var is set

os.environ.setdefault only writes the variable if it isn't already set, so a CI environment that supplies its own JWT_SECRET_KEY overrides the test default.

The import order matters: the os.environ.setdefault call has to run before any module that calls get_settings(), otherwise Pydantic raises a validation error at import time and pytest never starts.

Don't reuse the test key in production

The test-secret-key-for-pytest-only-32-bytes value is intentionally weak and committed to source control. It exists only to make the test suite runnable without external setup. A production JWT_SECRET_KEY is generated with secrets.token_urlsafe(32) (or stronger), stored in the deployment platform's secrets system, and never written to a file in version control.