Phase 1: auth, room CRUD, WebSocket chat, PWA frontend

Invite-only FastAPI + SQLAlchemy(async) + Postgres backend (session-cookie
auth via CLI-provisioned accounts, open-room CRUD, single-instance /ws/chat)
and a React + Vite PWA frontend (login, room list, chat view). Backend tests
pass against a local Postgres DB. See README.md and backend/README.md for
setup, and ARCHITECTURE.md for the full phased design.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-13 20:01:17 -06:00
co-authored by Claude Sonnet 5
parent 8ac35062dc
commit 99aa029c0d
77 changed files with 9042 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
from app.models.base import Base
from app.models.membership import RoomMembership, RoomRole
from app.models.message import Message
from app.models.room import Room
from app.models.user import User
__all__ = ["Base", "User", "Room", "RoomMembership", "RoomRole", "Message"]
+5
View File
@@ -0,0 +1,5 @@
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
pass
+31
View File
@@ -0,0 +1,31 @@
import enum
import uuid
from datetime import datetime
from sqlalchemy import DateTime, Enum, ForeignKey, PrimaryKeyConstraint, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base
class RoomRole(str, enum.Enum):
owner = "owner"
admin = "admin"
member = "member"
class RoomMembership(Base):
__tablename__ = "room_memberships"
__table_args__ = (PrimaryKeyConstraint("room_id", "user_id"),)
room_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("rooms.id"))
user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"))
role: Mapped[RoomRole] = mapped_column(
Enum(RoomRole, name="room_role"), default=RoomRole.member, nullable=False
)
joined_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
room = relationship("Room", back_populates="memberships")
user = relationship("User")
+23
View File
@@ -0,0 +1,23 @@
import uuid
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, Text, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base
class Message(Base):
__tablename__ = "messages"
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
room_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("rooms.id"), index=True, nullable=False)
user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"), nullable=False)
content: Mapped[str] = mapped_column(Text, nullable=False)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), index=True, nullable=False
)
edited_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
user = relationship("User")
+25
View File
@@ -0,0 +1,25 @@
import uuid
from datetime import datetime
from sqlalchemy import Boolean, DateTime, ForeignKey, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base
class Room(Base):
__tablename__ = "rooms"
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
name: Mapped[str] = mapped_column(String(100), unique=True, index=True, nullable=False)
description: Mapped[str | None] = mapped_column(Text)
is_private: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
owner_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"), nullable=False)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
owner = relationship("User")
memberships = relationship(
"RoomMembership", back_populates="room", cascade="all, delete-orphan"
)
+21
View File
@@ -0,0 +1,21 @@
import uuid
from datetime import datetime
from sqlalchemy import Boolean, DateTime, String, func
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base
class User(Base):
__tablename__ = "users"
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
username: Mapped[str] = mapped_column(String(50), unique=True, index=True, nullable=False)
email: Mapped[str] = mapped_column(String(255), unique=True, index=True, nullable=False)
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
is_bot: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
is_site_admin: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)