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)