Chat (conversations + messages)¶
A ready threaded-chat module over the SDK primitives (BaseModel /
BaseRepository / pagination / SSE). You inherit the concrete tables,
mount the router, and get conversations, messages and real-time delivery
— without hand-writing the data layer.
The tempest_fastapi_sdk.chat module ships three pieces:
- Abstract tables —
BaseConversationModel,BaseConversationParticipantModel,BaseMessageModel(+make_*factories for tests/scripts). ChatService— the business logic: start a conversation, post a message, list history, list a user's conversations.make_chat_router— the HTTP endpoints, in the same shape asmake_auth_router/make_web_push_router.
No extra
The module uses only the SDK core. No extras to install — import and go.
The tables¶
Like the SDK's other reusable tables, the SDK ships the abstract row
and your project ships the concrete one (so the FK and
__tablename__ live in the application's metadata). Hand-write them in
production:
from tempest_fastapi_sdk.chat import (
BaseConversationModel,
BaseConversationParticipantModel,
BaseMessageModel,
)
from sqlalchemy import ForeignKey, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from uuid import UUID
class ConversationModel(BaseConversationModel):
__tablename__ = "conversations"
class ConversationParticipantModel(BaseConversationParticipantModel):
__tablename__ = "conversation_participants"
__table_args__ = (
UniqueConstraint("conversation_id", "user_id", name="uq_participant"),
)
conversation_id: Mapped[UUID] = mapped_column(
ForeignKey("conversations.id", ondelete="CASCADE"), index=True
)
user_id: Mapped[UUID] = mapped_column(
ForeignKey("users.id", ondelete="CASCADE"), index=True
)
class MessageModel(BaseMessageModel):
__tablename__ = "messages"
conversation_id: Mapped[UUID] = mapped_column(
ForeignKey("conversations.id", ondelete="CASCADE"), index=True
)
sender_id: Mapped[UUID] = mapped_column(
ForeignKey("users.id", ondelete="CASCADE"), index=True
)
Shortcut for tests
In tests and scripts, the factories build the concrete class at runtime:
The service¶
ChatService takes three repositories (and, optionally, an SSEBroker
for real time):
from uuid import UUID
from sqlalchemy.ext.asyncio import AsyncSession
from tempest_fastapi_sdk import BaseRepository
from tempest_fastapi_sdk.chat import ChatService
from src.db.models import ConversationModel, ConversationParticipantModel, MessageModel
def build_chat_service(session: AsyncSession) -> ChatService:
return ChatService(
conversations=BaseRepository(session, model=ConversationModel),
participants=BaseRepository(session, model=ConversationParticipantModel),
messages=BaseRepository(session, model=MessageModel),
)
async def demo(session: AsyncSession, alice: UUID, bob: UUID) -> None:
service = build_chat_service(session)
# The creator joins as a participant automatically.
conversation = await service.start_conversation(alice, [bob], title="Project X")
await service.post_message(conversation.id, alice, "Shall we start?")
await service.post_message(conversation.id, bob, "Let's!")
page = await service.list_messages(conversation.id, page=1, page_size=20)
for message in page["items"]:
print(message.sender_id, message.body)
mine = await service.list_conversations(alice) # [] when there are none
Running demo, the two print calls emit each message in order
(oldest first), with the sender's UUID and body:
list_messages returns the SDK's standard offset-pagination dict
(items already mapped to MessageResponseSchema, total, page,
page_size, pages), ordered oldest-first.
The router¶
make_chat_router takes how to resolve the session, the service and
the authenticated user — like the SDK's other routers:
from collections.abc import AsyncIterator
from uuid import UUID
from fastapi import FastAPI
from sqlalchemy.ext.asyncio import AsyncSession
from tempest_fastapi_sdk.chat import make_chat_router
from src.api.dependencies.resources import db
from src.api.dependencies.services import build_chat_service
sessionmaker = db.get_session_context
async def get_session() -> AsyncIterator[AsyncSession]:
async with sessionmaker() as session:
yield session
def current_user_id() -> UUID:
... # your auth dependency (JWT/session) resolving the user's UUID
app = FastAPI()
app.include_router(
make_chat_router(
service_factory=build_chat_service,
session_factory=get_session,
current_user_id=current_user_id,
)
)
Mounted endpoints (all require authentication):
| Method | Route | Does |
|---|---|---|
POST |
/api/chat/conversations |
Start a conversation (creator becomes a participant) |
GET |
/api/chat/conversations |
List the user's conversations |
POST |
/api/chat/conversations/{id}/messages |
Post a message (participant only) |
GET |
/api/chat/conversations/{id}/messages |
Page the history (participant only) |
GET |
/api/chat/conversations/{id}/stream |
SSE of new messages (participant only) |
Participant guard
Posting, reading and subscribing require the authenticated user to be
a participant of the conversation; otherwise the router responds
403.
Real time via SSE¶
Inject an SSEBroker into the service and every posted message is also
published to the conversation's channel (str(conversation_id)),
reusing the SSE fan-out the SDK already has. Without a broker, the
/stream endpoint responds 404.
from sqlalchemy.ext.asyncio import AsyncSession
from tempest_fastapi_sdk import BaseRepository
from tempest_fastapi_sdk.chat import ChatService
from tempest_fastapi_sdk.sse import SSEBroker
from src.db.models import ConversationModel, ConversationParticipantModel, MessageModel
broker = SSEBroker() # single-process; pass redis=<client> for multi-worker
def build_chat_service(session: AsyncSession) -> ChatService:
return ChatService(
conversations=BaseRepository(session, model=ConversationModel),
participants=BaseRepository(session, model=ConversationParticipantModel),
messages=BaseRepository(session, model=MessageModel),
broker=broker,
)
The client subscribes with an EventSource pointing at
/api/chat/conversations/{id}/stream and receives message events whose
data is the JSON of MessageResponseSchema:
event: message
id: 7d3c5f8a-6b2e-4a19-b0d4-1c2e3f4a5b6c
data: {"id": "7d3c5f8a-...", "conversation_id": "1a2b3c4d-...", "sender_id": "7d3c5f8a-...", "body": "Let's!", "created_at": "2026-07-18T14:32:07Z"}
See the Server-Sent Events » recipe for the client side and the multi-worker Redis bridge.
Recap¶
- Inherit
BaseConversationModel/BaseConversationParticipantModel/BaseMessageModeland point the FKs at yourUserModel. ChatServicecovers start/post/list; it returns schemas, not ORM rows.make_chat_routermounts the endpoints with the participant guard.- Pass an
SSEBrokerto get real-time delivery for free.