Skip to content
GitHub

Configuration

Create application.yaml in your project root. Top-level keys are core settings; each extension reads its own section (the section name is the provider’s config_key):

application.yaml
app_name: my-app
debug: false
env: development # development | staging | production | test
logging:
level: INFO
format: json # text | json
# lexigram-web (config_key: "web")
web:
server:
host: "0.0.0.0"
port: 8000
cors:
enabled: true
allow_origins: ["https://myapp.com"]
# lexigram-sql (config_key: "db")
db:
backend:
url: "${DATABASE_URL}"
pool:
min_size: 2
max_size: 10
# lexigram-cache (config_key: "cache")
cache:
backends:
- name: memory
type: memory # memory | redis | memcached
default: true
from lexigram import LexigramConfig
# Auto-discovers application.yaml in the project root
config = LexigramConfig.from_yaml()
# Or from a specific path
config = LexigramConfig.from_yaml("path/to/application.yaml")

Application loads configuration for you when you don’t pass one — it calls LexigramConfig.from_env_profile() by default.

LexigramConfig has typed top-level fields:

FieldTypeDefaultDescription
app_namestr"lexigram-app"Application name
debugboolFalseDebug mode
envEnvironmentdevelopmentDeployment environment
loggingLoggingConfigStructured logging settings
moduleslist[str][]Enabled modules

All extension sections (web:, db:, cache:, …) are accessed via config.get_section().


There are two complementary mechanisms.

Use ${VAR} for secrets and deployment values, with optional defaults via ${VAR:default}:

application.yaml
db:
backend:
url: "${DATABASE_URL:sqlite+aiosqlite:///./dev.db}"
auth:
secret_key: "${LEX_AUTH__SECRET_KEY}"

Any configuration key can be overridden by an environment variable using the LEX_ prefix and double underscores for nesting. Env vars win over YAML:

Terminal window
LEX_WEB__SERVER__PORT=9000 # web.server.port = 9000
LEX_SQL__BACKEND__URL=postgresql+asyncpg://... # db backend url
LEX_AI_LLM__PROVIDERS__0__API_KEY=sk-... # list items use numeric indices
VariablePurposeDefault
LEX_PROFILEActive configuration profile(none)
LEX_DEBUGEnable debug modefalse
LEX_QUIETSuppress startup bannerfalse
LEX_ENVDeployment environmentdevelopment

Lexigram merges a profile-specific YAML over the base config. Set LEX_PROFILE to activate it:

application.yaml # Base config (always loaded)
application.development.yaml # Merged when LEX_PROFILE=development
application.staging.yaml # Merged when LEX_PROFILE=staging
application.production.yaml # Merged when LEX_PROFILE=production
application.test.yaml # Merged when LEX_PROFILE=test
application.development.yaml
debug: true
logging:
level: DEBUG
format: text
db:
backend:
url: "sqlite+aiosqlite:///./dev.db"
application.production.yaml
debug: false
logging:
level: WARNING
format: json
cache:
backends:
- name: redis
type: redis
default: true
redis_url: "${REDIS_URL}"
from lexigram import LexigramConfig
# Reads LEX_PROFILE from the environment
config = LexigramConfig.from_env_profile()
# Explicit profile
config = LexigramConfig.from_env_profile("production")
# With a custom base path
config = LexigramConfig.from_env_profile("staging", base_path="./config")

validate_for_environment() checks environment-specific constraints (for example, debug=True in production):

from lexigram.contracts.core.config import Environment
issues = config.validate_for_environment(Environment.PRODUCTION)

A provider declares config_key and config_model to automatically receive its typed config section — no manual parsing:

from dataclasses import dataclass
from lexigram import Provider
from lexigram.contracts.core.di import ContainerRegistrarProtocol
@dataclass
class BillingConfig:
stripe_key: str = ""
currency: str = "usd"
class BillingProvider(Provider):
name = "billing"
config_key = "billing" # reads "billing:" from application.yaml
config_model = BillingConfig # coerces it into BillingConfig
async def register(self, container: ContainerRegistrarProtocol) -> None:
cfg = self.config or BillingConfig() # self.config is a typed BillingConfig
container.singleton(StripeClient, StripeClient(cfg.stripe_key))

Before calling register(), the framework reads the matching section via LexigramConfig.get_section(config_key, config_model) and assigns it to provider.config. Built-in providers use the same mechanism:

Providerconfig_key
WebProvider"web"
DatabaseProvider"db"
CacheProvider"cache"
AuthProvider"auth"

config = LexigramConfig.from_yaml()
# Typed top-level access
config.app_name # "my-app"
config.debug # False
config.environment # Environment.DEVELOPMENT
# Section access (extension config)
db_config = config.get_section("db", DatabaseConfig)
rag_config = config.get_section("ai_rag", RAGConfig) # dotted paths also supported
# Existence + serialization (secrets redacted by default)
config.has_section("web") # True
config.to_dict() # {"app_name": "...", "auth": {"secret_key": "***"}}
config.to_dict(redact_secrets=False) # full values