Skip to main content

Configuration & Environment

This project centralizes application configuration and infrastructure management using Pydantic-based settings. This approach ensures type safety, environment variable validation, and consistent access to core services like the database and SMTP server across the entire application.

Centralized Configuration with Settings​

The core of the configuration system is the Settings class located in backend/app/core/config.py. It inherits from Pydantic's BaseSettings, which automatically maps environment variables to class attributes.

Environment Variable Loading​

The application is configured to look for a .env file located one level above the backend directory, as defined in the model_config:

class Settings(BaseSettings):
model_config = SettingsConfigDict(
# Use top level .env file (one level above ./backend/)
env_file="../.env",
env_ignore_empty=True,
extra="ignore",
)

Dynamic and Computed Fields​

The Settings class uses Pydantic's @computed_field to derive complex configuration strings from simpler environment variables. A primary example is the construction of the database connection string:

@computed_field
@property
def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn:
return PostgresDsn.build(
scheme="postgresql+psycopg",
username=self.POSTGRES_USER,
password=self.POSTGRES_PASSWORD,
host=self.POSTGRES_SERVER,
port=self.POSTGRES_PORT,
path=self.POSTGRES_DB,
)

This ensures that the SQLALCHEMY_DATABASE_URI is always consistent with the individual POSTGRES_* variables provided in the environment.

Database Infrastructure​

The application uses SQLModel (built on SQLAlchemy) for database interactions. The connection infrastructure is managed in backend/app/core/db.py and backend/app/api/deps.py.

Engine and Session Management​

The SQLAlchemy engine is initialized globally in backend/app/core/db.py using the URI from the settings:

from sqlmodel import create_engine
from app.core.config import settings

engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI))

For API requests, database sessions are managed via FastAPI's dependency injection system in backend/app/api/deps.py. The get_db generator ensures that a session is opened for the request and automatically closed afterward:

def get_db() -> Generator[Session, None, None]:
with Session(engine) as session:
yield session

SessionDep = Annotated[Session, Depends(get_db)]

Developers use SessionDep in API routes to gain access to the database without manually managing connection lifecycles.

Database Initialization​

On startup, the application can initialize the database via the init_db function in backend/app/core/db.py. This function uses settings to create the initial superuser if it does not already exist:

def init_db(session: Session) -> None:
user = session.exec(
select(User).where(User.email == settings.FIRST_SUPERUSER)
).first()
if not user:
user_in = UserCreate(
email=settings.FIRST_SUPERUSER,
password=settings.FIRST_SUPERUSER_PASSWORD,
is_superuser=True,
)
user = crud.create_user(session=session, user_create=user_in)

Email and SMTP Configuration​

Email functionality is integrated through the emails library and configured via SMTP settings.

Enabling Email​

The Settings class includes a helper property, emails_enabled, which determines if the necessary variables (SMTP_HOST and EMAILS_FROM_EMAIL) are present:

@computed_field
@property
def emails_enabled(self) -> bool:
return bool(self.SMTP_HOST and self.EMAILS_FROM_EMAIL)

Sending Emails​

The utility function send_email in backend/app/utils.py consumes these settings to dispatch messages. It handles various authentication methods (TLS/SSL) based on the configuration:

def send_email(*, email_to: str, subject: str = "", html_content: str = "") -> None:
assert settings.emails_enabled, "no provided configuration for email variables"
message = emails.Message(
subject=subject,
html=html_content,
mail_from=(settings.EMAILS_FROM_NAME, settings.EMAILS_FROM_EMAIL),
)
smtp_options = {"host": settings.SMTP_HOST, "port": settings.SMTP_PORT}
if settings.SMTP_TLS:
smtp_options["tls"] = True
# ... additional logic for SSL and Auth ...
response = message.send(to=email_to, smtp=smtp_options)

Security and Environment Validation​

The project enforces strict security checks to prevent accidental deployment with default credentials.

Default Secret Enforcement​

In backend/app/core/config.py, a model_validator checks critical secrets like SECRET_KEY, POSTGRES_PASSWORD, and FIRST_SUPERUSER_PASSWORD. If these are set to the default value "changethis" in a non-local environment, the application will fail to start:

def _check_default_secret(self, var_name: str, value: str | None) -> None:
if value == "changethis":
message = f'The value of {var_name} is "changethis", for security, please change it...'
if self.ENVIRONMENT == "local":
warnings.warn(message, stacklevel=1)
else:
raise ValueError(message)

Environment-Specific Logic​

The ENVIRONMENT setting (which can be local, staging, or production) is used throughout the app to toggle features. For instance, in backend/app/main.py, Sentry error tracking is only initialized if the environment is not local:

if settings.SENTRY_DSN and settings.ENVIRONMENT != "local":
sentry_sdk.init(dsn=str(settings.SENTRY_DSN), enable_tracing=True)