@startuml database-schema !theme plain skinparam backgroundColor #FFFFFF skinparam defaultFontName Arial skinparam linetype ortho skinparam packageStyle rectangle title Database Schema caption Every table also has a RowVersion ROWVERSION column (optimistic concurrency), omitted below for brevity.\nTables marked (*) have no repository/feature slice yet. package "User & Auth" { entity "UserAccount" as User { * UserAccountID: UNIQUEIDENTIFIER <> -- * Username: VARCHAR(64) <> * FirstName: NVARCHAR(128) * LastName: NVARCHAR(128) * Email: VARCHAR(128) <> * CreatedAt: DATETIME UpdatedAt: DATETIME * DateOfBirth: DATE } entity "UserCredential" as Cred { * UserCredentialID: UNIQUEIDENTIFIER <> -- * UserAccountID: UNIQUEIDENTIFIER <> * CreatedAt: DATETIME * Expiry: DATETIME * Hash: NVARCHAR(256) * IsRevoked: BIT RevokedAt: DATETIME } entity "UserVerification" as Verify { * UserVerificationID: UNIQUEIDENTIFIER <> -- * UserAccountID: UNIQUEIDENTIFIER <> * VerificationDateTime: DATETIME } entity "UserProfile" as Profile { * UserProfileID: UNIQUEIDENTIFIER <> -- * UserAccountID: UNIQUEIDENTIFIER <> * Biography: NVARCHAR(MAX) } entity "UserAvatar" as Avatar { * UserAvatarID: UNIQUEIDENTIFIER <> -- * UserProfileID: UNIQUEIDENTIFIER <> * PhotoID: UNIQUEIDENTIFIER <> * ValidFrom: DATETIME2(3) ValidTo: DATETIME2(3) .. At most one row per UserProfileID has ValidTo IS NULL (active avatar) } entity "UserFollow (*)" as Follow { * UserFollowID: UNIQUEIDENTIFIER <> -- * UserAccountID: UNIQUEIDENTIFIER <> * FollowingID: UNIQUEIDENTIFIER <> * CreatedAt: DATETIME } entity "Photo (*)" as Photo { * PhotoID: UNIQUEIDENTIFIER <> -- Hyperlink: NVARCHAR(256) * UploadedByID: UNIQUEIDENTIFIER <> * UploadedAt: DATETIME } } package "Location" { entity "Country" as Country { * CountryID: UNIQUEIDENTIFIER <> -- * CountryName: NVARCHAR(100) * ISO3166_1: CHAR(2) <> } entity "StateProvince" as StateProvince { * StateProvinceID: UNIQUEIDENTIFIER <> -- * StateProvinceName: NVARCHAR(100) * ISO3166_2: CHAR(6) <> * CountryID: UNIQUEIDENTIFIER <> } entity "City" as City { * CityID: UNIQUEIDENTIFIER <> -- * CityName: NVARCHAR(100) * StateProvinceID: UNIQUEIDENTIFIER <> } } package "Brewery" { entity "BreweryPost" as Brewery { * BreweryPostID: UNIQUEIDENTIFIER <> -- * BreweryName: NVARCHAR(256) * PostedByID: UNIQUEIDENTIFIER <> * Description: NVARCHAR(512) * CreatedAt: DATETIME UpdatedAt: DATETIME } entity "BreweryPostLocation" as BreweryLoc { * BreweryPostLocationID: UNIQUEIDENTIFIER <> -- * BreweryPostID: UNIQUEIDENTIFIER <> * AddressLine1: NVARCHAR(256) AddressLine2: NVARCHAR(256) * PostalCode: NVARCHAR(20) * CityID: UNIQUEIDENTIFIER <> Coordinates: GEOGRAPHY } entity "BreweryPostPhoto (*)" as BreweryPhoto { * BreweryPostPhotoID: UNIQUEIDENTIFIER <> -- * BreweryPostID: UNIQUEIDENTIFIER <> * PhotoID: UNIQUEIDENTIFIER <> * LinkedAt: DATETIME } } package "Beer (*)" { entity "BeerStyle (*)" as BeerStyle { * BeerStyleID: UNIQUEIDENTIFIER <> -- * StyleName: NVARCHAR(100) <> Description: NVARCHAR(MAX) } entity "BeerPost (*)" as Beer { * BeerPostID: UNIQUEIDENTIFIER <> -- * Name: NVARCHAR(100) * Description: NVARCHAR(MAX) * ABV: DECIMAL(4,2) * IBU: INT * PostedByID: UNIQUEIDENTIFIER <> * BeerStyleID: UNIQUEIDENTIFIER <> * BrewedByID: UNIQUEIDENTIFIER <> * CreatedAt: DATETIME UpdatedAt: DATETIME } entity "BeerPostPhoto (*)" as BeerPhoto { * BeerPostPhotoID: UNIQUEIDENTIFIER <> -- * BeerPostID: UNIQUEIDENTIFIER <> * PhotoID: UNIQUEIDENTIFIER <> * LinkedAt: DATETIME } entity "BeerPostComment (*)" as BeerComment { * BeerPostCommentID: UNIQUEIDENTIFIER <> -- * Comment: NVARCHAR(250) * BeerPostID: UNIQUEIDENTIFIER <> * CommentedByID: UNIQUEIDENTIFIER <> * Rating: INT * CreatedAt: DATETIME UpdatedAt: DATETIME } } ' Relationships: User & Auth User ||--o{ Cred : "has" User ||--o| Verify : "has" User ||--o| Profile : "has" Profile ||--o{ Avatar : "has (history)" Avatar }o--|| Photo : "refers to" User ||--o{ Follow : "follows (UserAccountID)" User ||--o{ Follow : "followed by (FollowingID)" User ||--o{ Photo : "uploaded" ' Relationships: Location Country ||--o{ StateProvince : "has" StateProvince ||--o{ City : "has" ' Relationships: Brewery User ||--o{ Brewery : "posted" Brewery ||--o| BreweryLoc : "has" City ||--o{ BreweryLoc : "located in" Brewery ||--o{ BreweryPhoto : "has" Photo ||--o{ BreweryPhoto : "linked to" ' Relationships: Beer User ||--o{ Beer : "posted" BeerStyle ||--o{ Beer : "categorizes" Brewery ||--o{ Beer : "brewed" Beer ||--o{ BeerPhoto : "has" Photo ||--o{ BeerPhoto : "linked to" Beer ||--o{ BeerComment : "has" User ||--o{ BeerComment : "commented" note bottom of User Auth reads/writes (registration, login, credential rotation, verification) go through ASP.NET Core Identity's UserManager, backed by Features.Users' DapperUserStore - inline SQL, no stored procedures. Profile/avatar data goes through Features.Users' UserProfileRepository. end note note bottom of Brewery Backed by Features.Breweries' BreweryRepository (Dapper); create/update/delete run inline SQL inside a transaction, with optimistic concurrency via RowVersion end note @enduml