Loading Secrets

How to initialize Redenv and load secrets into your application.

The Redenv client provides two methods for loading secrets: init() and load().

MethodReturnsUse Where
init()SecretsOnly in config/redenv.py (app startup)
load()SecretsEverywhere else (programmatic access)

Both methods fetch, decrypt, cache secrets, and populate os.environ.

Info

Fun fact: init() and load() are identical under the hood — both return Secrets. The different names make your code's intent clearer: "initialize at startup" vs "load secrets for use".

Step 1: Initialize and Validate at Startup#

Call init() only in the file where you create the client. Since init() returns the Secrets object, you can validate required secrets immediately:

config/redenv.py
import os
from redenv import Redenv

redenv = Redenv({
    "project": os.environ["REDENV_PROJECT"],
    "token_id": os.environ["REDENV_TOKEN_ID"],
    "token": os.environ["REDENV_TOKEN_KEY"],
    "upstash": {
        "url": os.environ["UPSTASH_REDIS_URL"],
        "token": os.environ["UPSTASH_REDIS_TOKEN"],
    },
    "environment": os.getenv("ENVIRONMENT", "development"),
})

async def initialize():
    # Initialize and validate at startup
    secrets = await redenv.init()  
    secrets.require("DATABASE_URL", "JWT_SECRET", "STRIPE_KEY")  
    return secrets
config/redenv.py
import os
from redenv import RedenvSync

redenv = RedenvSync({
    "project": os.environ["REDENV_PROJECT"],
    "token_id": os.environ["REDENV_TOKEN_ID"],
    "token": os.environ["REDENV_TOKEN_KEY"],
    "upstash": {
        "url": os.environ["UPSTASH_REDIS_URL"],
        "token": os.environ["UPSTASH_REDIS_TOKEN"],
    },
    "environment": os.getenv("ENVIRONMENT", "development"),
})

# Initialize and validate at startup
secrets = redenv.init()  
secrets.require("DATABASE_URL", "JWT_SECRET", "STRIPE_KEY")  

Tip

Fail fast! By validating in config/redenv.py, your app crashes immediately on startup if secrets are missing — not later when a route tries to use them.

Step 2: Use load() Everywhere Else#

In all other files, use load() to get programmatic access to secrets:

services/database.py
from config.redenv import redenv

async def connect_database():
    secrets = await redenv.load()  

    # Programmatic access (recommended)
    db_url = secrets["DATABASE_URL"]
    port = secrets.get("PORT", 5432, cast=int)

    # Validate required secrets
    secrets.require("DATABASE_URL", "DATABASE_PASSWORD")
services/database.py
from config.redenv import redenv

def connect_database():
    secrets = redenv.load()  

    # Programmatic access (recommended)
    db_url = secrets["DATABASE_URL"]
    port = secrets.get("PORT", 5432, cast=int)

    # Validate required secrets
    secrets.require("DATABASE_URL", "DATABASE_PASSWORD")

Info

load() is idempotent. Calling it multiple times won't cause extra network requests—it serves from cache within the configured TTL.

Why Programmatic Access?#

We highly recommend using load() over direct os.environ access:

Featureos.environsecrets object
Type casting Manual parsing cast=int, cast=bool, etc.
Validation Runtime errors .require() fails fast
Scoping Manual filtering .scope("AWS_")
Masking Exposed in logs Auto-masked
Raw access .raw for unexpanded values
secrets = await redenv.load()

# Type-safe casting
port = secrets.get("PORT", 3000, cast=int)
debug = secrets.get("DEBUG", cast=bool)
config = secrets.get("APP_CONFIG", cast=dict)

# Fail fast if required secrets are missing
secrets.require("DATABASE_URL", "STRIPE_KEY", "JWT_SECRET")

# Scoped access for modules
aws_config = secrets.scope("AWS_")  # AWS_KEY → KEY
secrets = redenv.load()

# Type-safe casting
port = secrets.get("PORT", 3000, cast=int)
debug = secrets.get("DEBUG", cast=bool)
config = secrets.get("APP_CONFIG", cast=dict)

# Fail fast if required secrets are missing
secrets.require("DATABASE_URL", "STRIPE_KEY", "JWT_SECRET")

# Scoped access for modules
aws_config = secrets.scope("AWS_")  # AWS_KEY → KEY

Serverless Functions#

For serverless environments (AWS Lambda), you can skip init() and use load() directly:

handler.py
from config.redenv import redenv

async def handler(event, context):
    # First call fetches from Redis, subsequent calls use cache
    secrets = await redenv.load()

    secrets.require("API_KEY")
    return {"statusCode": 200, "body": "ok"}
handler.py
from config.redenv import redenv

def handler(event, context):
    # First call fetches from Redis, subsequent calls use cache
    secrets = redenv.load()

    secrets.require("API_KEY")
    return {"statusCode": 200, "body": "ok"}

Info

In serverless, load() handles both initialization and access. The first invocation warms the cache; subsequent calls in the same instance are instant.

Environment Population#

By default, both init() and load() populate os.environ:

import os

secrets = redenv.load()  # or await redenv.load()

# These are equivalent:
secrets["API_KEY"] == os.environ["API_KEY"]  # True

Controlling Override Behavior#

config/redenv.py
redenv = Redenv({
    # ...
    "env": {
        "override": False,  # Preserve existing env vars (useful for local .env overrides)
    },
})

Multiple Environments#

config/redenv.py
import os
from redenv import Redenv

base_options = {
    "project": os.environ["REDENV_PROJECT"],
    "token_id": os.environ["REDENV_TOKEN_ID"],
    "token": os.environ["REDENV_TOKEN_KEY"],
    "upstash": {
        "url": os.environ["UPSTASH_REDIS_URL"],
        "token": os.environ["UPSTASH_REDIS_TOKEN"],
    },
}

prod_secrets = Redenv({
    **base_options,
    "environment": "production",
})

staging_secrets = Redenv({
    **base_options,
    "environment": "staging",
})

Warning

Multiple environments will both try to populate os.environ. Use env: { "override": False } or rely solely on programmatic access.