Skip to main content
The application uses two SQLAlchemy models backed by a SQLite database file (risk_monitor.db). Tables are created automatically on startup via Base.metadata.create_all(bind=engine).

Business

Table name: businesses
class Business(Base):
    """A business entity that can be evaluated for risk."""

    __tablename__ = "businesses"

    id = Column(Integer, primary_key=True, index=True)
    name = Column(String(255), nullable=False)
    industry = Column(String(100), nullable=True)
    country = Column(String(100), nullable=True)
    created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))

    risk_evaluations = relationship(
        "RiskEvaluation",
        back_populates="business",
        order_by="desc(RiskEvaluation.evaluated_at)",
    )

Columns

ColumnTypeNullableDescription
idIntegerNoPrimary key, auto-incremented
nameString(255)NoBusiness display name, required on creation
industryString(100)YesIndustry category; used by the risk engine for modifiers
countryString(100)YesCountry of operation; used by the risk engine for modifiers
created_atDateTimeNoUTC timestamp set automatically on insert

Relationship

risk_evaluations is a one-to-many relationship to RiskEvaluation, ordered by evaluated_at descending. Accessing business.risk_evaluations[0] always returns the most recent evaluation.

RiskEvaluation

Table name: risk_evaluations
class RiskEvaluation(Base):
    """A single risk evaluation result for a business."""

    __tablename__ = "risk_evaluations"

    id = Column(Integer, primary_key=True, index=True)
    business_id = Column(Integer, ForeignKey("businesses.id"), nullable=False)
    risk_score = Column(Float, nullable=False)
    risk_level = Column(String(20), nullable=False)
    factors = Column(Text, nullable=True)
    evaluated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))

    business = relationship("Business", back_populates="risk_evaluations")

Columns

ColumnTypeNullableDescription
idIntegerNoPrimary key, auto-incremented
business_idIntegerNoForeign key to businesses.id
risk_scoreFloatNoFinal computed score, 0.0–100.0 rounded to 1 decimal place
risk_levelString(20)NoOne of: low, medium, high, critical
factorsTextYesJSON string containing the score component breakdown
evaluated_atDateTimeNoUTC timestamp set automatically on insert

Relationship

business is a many-to-one back-reference to Business.

The factors JSON text pattern

SQLite has no native JSON column type. The factors column stores a serialised JSON string produced by json.dumps() in the risk engine:
factors = {
    "base_score": round(base_score, 1),
    "industry_modifier": round(industry_modifier, 1),
    "country_modifier": round(country_modifier, 1),
    "noise": round(noise, 1),
    "final_score": final_score,
}

return {
    ...
    "factors": json.dumps(factors),
}
Route handlers parse the string back to a dict with json.loads() before passing it to templates:
factors = json.loads(latest_eval.factors) if latest_eval.factors else None
In a production PostgreSQL setup, the factors column would be a native jsonb column. This would allow querying individual factor values directly in SQL without application-level parsing.

Parsed factors structure

{
  "base_score": 42.3,
  "industry_modifier": 22.7,
  "country_modifier": 0.0,
  "noise": -1.4,
  "final_score": 63.6
}

Database setup

The database engine and session factory are defined in app/database.py:
SQLALCHEMY_DATABASE_URL = "sqlite:///./risk_monitor.db"

engine = create_engine(
    SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
check_same_thread=False is required for SQLite when the session is used across threads, which FastAPI’s dependency injection can trigger. Each request gets its own session via the get_db dependency, which ensures the session is closed after the response is returned.
Tests use a separate test_risk_monitor.db database so seed data in the main database is never affected by test runs.

Architecture overview

Request flow, HTMX partials pattern, and design decisions

Risk engine

How risk scores are computed from the industry and country fields

Build docs developers (and LLMs) love