diff --git a/i18n/be/LC_MESSAGES/messages.po b/i18n/be/LC_MESSAGES/messages.po
index db3d814..8962308 100644
--- a/i18n/be/LC_MESSAGES/messages.po
+++ b/i18n/be/LC_MESSAGES/messages.po
@@ -25,8 +25,8 @@ msgstr ""
#: src/snapshots/templates/article-template.html.j2:73
msgid ""
-"This story is a copy of an article from %(site_title). It is delivered to "
+"This story is a copy of an article from {site_title}. It is delivered to "
"you from a trusted archive to assure its availability over time."
msgstr ""
diff --git a/i18n/en/LC_MESSAGES/messages.po b/i18n/en/LC_MESSAGES/messages.po
index 82f1ac0..d3a6f71 100644
--- a/i18n/en/LC_MESSAGES/messages.po
+++ b/i18n/en/LC_MESSAGES/messages.po
@@ -24,12 +24,12 @@ msgstr "How do I know that I can trust this page?"
#: src/snapshots/templates/article-template.html.j2:73
msgid ""
-"This story is a copy of an article from %(site_title). It is delivered to "
+"This story is a copy of an article from {site_title}. It is delivered to "
"you from a trusted archive to assure its availability over time."
msgstr ""
-"This story is a copy of an article from %(site_title). It is delivered to "
+"This story is a copy of an article from {site_title}. It is delivered to "
"you from a trusted archive to assure its availability over time."
#: src/snapshots/templates/article-template.html.j2:76
diff --git a/i18n/es/LC_MESSAGES/messages.po b/i18n/es/LC_MESSAGES/messages.po
index eef3583..2157241 100644
--- a/i18n/es/LC_MESSAGES/messages.po
+++ b/i18n/es/LC_MESSAGES/messages.po
@@ -26,12 +26,12 @@ msgstr "¿Cómo sé que puedo confiar en esta página?"
#: src/snapshots/templates/article-template.html.j2:73
msgid ""
-"This story is a copy of an article from %(site_title). It is delivered to "
+"This story is a copy of an article from {site_title}. It is delivered to "
"you from a trusted archive to assure its availability over time."
msgstr ""
-"Esta historia es una copia de un artículo de %(site_title). Se la entregamos "
+"Esta historia es una copia de un artículo de {site_title}. Se la entregamos "
"desde un archivo confiable para garantizar su disponibilidad a lo largo del "
"tiempo."
diff --git a/i18n/fa/LC_MESSAGES/messages.po b/i18n/fa/LC_MESSAGES/messages.po
index 483d342..f3c20d0 100644
--- a/i18n/fa/LC_MESSAGES/messages.po
+++ b/i18n/fa/LC_MESSAGES/messages.po
@@ -24,8 +24,8 @@ msgstr ""
#: src/snapshots/templates/article-template.html.j2:73
msgid ""
-"This story is a copy of an article from %(site_title). It is delivered to "
+"This story is a copy of an article from {site_title}. It is delivered to "
"you from a trusted archive to assure its availability over time."
msgstr ""
diff --git a/i18n/ka/LC_MESSAGES/messages.po b/i18n/ka/LC_MESSAGES/messages.po
index aa07c51..f4d8635 100644
--- a/i18n/ka/LC_MESSAGES/messages.po
+++ b/i18n/ka/LC_MESSAGES/messages.po
@@ -24,8 +24,8 @@ msgstr ""
#: src/snapshots/templates/article-template.html.j2:73
msgid ""
-"This story is a copy of an article from %(site_title). It is delivered to "
+"This story is a copy of an article from {site_title}. It is delivered to "
"you from a trusted archive to assure its availability over time."
msgstr ""
diff --git a/i18n/messages.pot b/i18n/messages.pot
index b23b85c..3356255 100644
--- a/i18n/messages.pot
+++ b/i18n/messages.pot
@@ -23,8 +23,8 @@ msgstr ""
#: src/snapshots/templates/article-template.html.j2:73
msgid ""
-"This story is a copy of an article from %(site_title). It is delivered to "
+"This story is a copy of an article from {site_title}. It is delivered to "
"you from a trusted archive to assure its availability over time."
msgstr ""
diff --git a/i18n/prs/LC_MESSAGES/messages.po b/i18n/prs/LC_MESSAGES/messages.po
index 5212f79..481721e 100644
--- a/i18n/prs/LC_MESSAGES/messages.po
+++ b/i18n/prs/LC_MESSAGES/messages.po
@@ -24,8 +24,8 @@ msgstr ""
#: src/snapshots/templates/article-template.html.j2:73
msgid ""
-"This story is a copy of an article from %(site_title). It is delivered to "
+"This story is a copy of an article from {site_title}. It is delivered to "
"you from a trusted archive to assure its availability over time."
msgstr ""
diff --git a/i18n/ps/LC_MESSAGES/messages.po b/i18n/ps/LC_MESSAGES/messages.po
index 3fafae2..2bb963d 100644
--- a/i18n/ps/LC_MESSAGES/messages.po
+++ b/i18n/ps/LC_MESSAGES/messages.po
@@ -24,8 +24,8 @@ msgstr ""
#: src/snapshots/templates/article-template.html.j2:73
msgid ""
-"This story is a copy of an article from %(site_title). It is delivered to "
+"This story is a copy of an article from {site_title}. It is delivered to "
"you from a trusted archive to assure its availability over time."
msgstr ""
diff --git a/i18n/ru/LC_MESSAGES/messages.po b/i18n/ru/LC_MESSAGES/messages.po
index 97b6053..44b8f91 100644
--- a/i18n/ru/LC_MESSAGES/messages.po
+++ b/i18n/ru/LC_MESSAGES/messages.po
@@ -25,8 +25,8 @@ msgstr ""
#: src/snapshots/templates/article-template.html.j2:73
msgid ""
-"This story is a copy of an article from %(site_title). It is delivered to "
+"This story is a copy of an article from {site_title}. It is delivered to "
"you from a trusted archive to assure its availability over time."
msgstr ""
diff --git a/src/database.py b/src/database.py
new file mode 100644
index 0000000..d344210
--- /dev/null
+++ b/src/database.py
@@ -0,0 +1,50 @@
+import contextlib
+from typing import Annotated, Iterator, Generator
+
+from fastapi import Depends
+from sqlalchemy import (
+ MetaData, create_engine, Connection,
+)
+from sqlalchemy.orm import sessionmaker, Session
+
+from src.config import settings
+from src.constants import DB_NAMING_CONVENTION
+
+engine = create_engine(
+ str(settings.DATABASE_URL),
+ pool_size=settings.DATABASE_POOL_SIZE,
+ pool_recycle=settings.DATABASE_POOL_TTL,
+ pool_pre_ping=settings.DATABASE_POOL_PRE_PING,
+)
+metadata = MetaData(naming_convention=DB_NAMING_CONVENTION)
+sm = sessionmaker(autocommit=False, expire_on_commit=False, bind=engine)
+
+
+@contextlib.contextmanager
+def get_db_connection() -> Iterator[Connection]:
+ with engine.connect() as connection:
+ try:
+ yield connection
+ except Exception:
+ connection.rollback()
+ raise
+
+
+@contextlib.contextmanager
+def get_db_session() -> Iterator[Session]:
+ session = sm()
+ try:
+ yield session
+ except Exception:
+ session.rollback()
+ raise
+ finally:
+ session.close()
+
+
+def get_db() -> Generator[Session, None]:
+ with get_db_session() as session:
+ yield session
+
+
+DbSession = Annotated[Session, Depends(get_db)]
diff --git a/src/models.py b/src/models.py
new file mode 100644
index 0000000..614f969
--- /dev/null
+++ b/src/models.py
@@ -0,0 +1,34 @@
+from datetime import datetime
+from typing import Any
+
+from sqlalchemy import DateTime, JSON, func
+from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
+from src.database import metadata
+
+
+class CustomBase(DeclarativeBase):
+ type_annotation_map = {
+ datetime: DateTime(timezone=True),
+ dict[str, Any]: JSON,
+ }
+ metadata = metadata
+
+class ActivatedMixin:
+ active: Mapped[bool] = mapped_column(default=True)
+
+
+class DeletedTimestampMixin:
+ deleted_at: Mapped[datetime | None] = mapped_column(nullable=True)
+
+
+class DescriptionMixin:
+ description: Mapped[str]
+
+
+class IdMixin:
+ id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
+
+
+class TimestampMixin:
+ created_at: Mapped[datetime] = mapped_column(default=func.now())
+ updated_at: Mapped[datetime] = mapped_column(default=func.now(), onupdate=func.now())
diff --git a/src/snapshots/models.py b/src/snapshots/models.py
new file mode 100644
index 0000000..17b9f01
--- /dev/null
+++ b/src/snapshots/models.py
@@ -0,0 +1,9 @@
+from sqlalchemy.orm import Mapped
+
+from src.models import CustomBase, IdMixin
+
+
+class Snapshot(CustomBase, IdMixin):
+ __tablename__ = "snapshot"
+
+ url: Mapped[str]