10 KiB
Database Schema
This document describes the database schema, stored procedures, and data access patterns for The Biergarten App.
Overview
The Biergarten App uses SQL Server 2022 with a SQL-first approach:
- All queries via stored procedures
- No ORM (no Entity Framework)
- Database-driven validation and integrity
- Version-controlled migrations using DbUp
See the database-schema diagram for visual representation.
Schema Overview
The database is organized into several logical domains:
User Management
Core user account and authentication tables.
UserAccount
Primary user profile table.
| Column | Type | Constraints | Description |
|---|---|---|---|
| UserAccountId | INT | PK, IDENTITY | Primary key |
| Username | NVARCHAR(30) | UNIQUE, NOT NULL | Unique username |
| NVARCHAR(255) | UNIQUE, NOT NULL | Email address | |
| FirstName | NVARCHAR(50) | NOT NULL | First name |
| LastName | NVARCHAR(50) | NOT NULL | Last name |
| Bio | NVARCHAR(500) | NULL | User bio |
| CreatedAt | DATETIME2 | NOT NULL | Account creation timestamp |
| UpdatedAt | DATETIME2 | NOT NULL | Last update timestamp |
| LastLoginAt | DATETIME2 | NULL | Last successful login |
Indexes:
- Unique index on
Username - Unique index on
Email - Index on
CreatedAtfor sorting
UserCredential
Authentication credentials with password hashes.
| Column | Type | Constraints | Description |
|---|---|---|---|
| UserCredentialId | INT | PK, IDENTITY | Primary key |
| UserAccountId | INT | FK → UserAccount | User reference |
| PasswordHash | VARBINARY(32) | NOT NULL | Argon2id hash (256-bit) |
| PasswordSalt | VARBINARY(16) | NOT NULL | Random salt (128-bit) |
| IsActive | BIT | NOT NULL | Active credential flag |
| CredentialRotatedAt | DATETIME2 | NULL | Last password change |
| CredentialExpiresAt | DATETIME2 | NULL | Password expiration |
| CredentialRevokedAt | DATETIME2 | NULL | Revocation timestamp |
| CreatedAt | DATETIME2 | NOT NULL | Creation timestamp |
Notes:
- Users can have multiple credentials (rotation support)
- Only one active credential per user
- Expired/revoked credentials retained for audit
Password Hashing:
- Algorithm: Argon2id
- Memory: 64MB
- Iterations: 4
- Parallelism: CPU core count
- Salt: Cryptographically random 128-bit
- Hash: 256-bit output
UserVerification
Email verification and account activation.
| Column | Type | Constraints | Description |
|---|---|---|---|
| UserVerificationId | INT | PK, IDENTITY | Primary key |
| UserAccountId | INT | FK → UserAccount | User reference |
| IsVerified | BIT | NOT NULL, DEFAULT 0 | Verification status |
| VerifiedAt | DATETIME2 | NULL | Verification timestamp |
| VerificationToken | NVARCHAR(255) | NULL | Email token |
| TokenExpiresAt | DATETIME2 | NULL | Token expiration |
UserAvatar
User profile pictures.
| Column | Type | Constraints | Description |
|---|---|---|---|
| UserAvatarId | INT | PK, IDENTITY | Primary key |
| UserAccountId | INT | FK → UserAccount | User reference |
| PhotoId | INT | FK → Photo | Image reference |
| IsActive | BIT | NOT NULL | Active avatar flag |
| CreatedAt | DATETIME2 | NOT NULL | Upload timestamp |
UserFollow
Social following relationships.
| Column | Type | Constraints | Description |
|---|---|---|---|
| UserFollowId | INT | PK, IDENTITY | Primary key |
| FollowerUserId | INT | FK → UserAccount | User who follows |
| FollowedUserId | INT | FK → UserAccount | User being followed |
| CreatedAt | DATETIME2 | NOT NULL | Follow timestamp |
Constraints:
- Unique constraint on (FollowerUserId, FollowedUserId)
- Check constraint: FollowerUserId ≠ FollowedUserId
Location Data
Geographic reference data for breweries and users.
Country
ISO 3166-1 country codes.
| Column | Type | Constraints | Description |
|---|---|---|---|
| CountryId | INT | PK, IDENTITY | Primary key |
| CountryCode | CHAR(2) | UNIQUE, NOT NULL | ISO 3166-1 alpha-2 |
| CountryName | NVARCHAR(100) | NOT NULL | Country name |
StateProvince
States, provinces, and regions (ISO 3166-2).
| Column | Type | Constraints | Description |
|---|---|---|---|
| StateProvinceId | INT | PK, IDENTITY | Primary key |
| CountryId | INT | FK → Country | Country reference |
| StateProvinceCode | NVARCHAR(10) | NOT NULL | ISO 3166-2 code |
| StateProvinceName | NVARCHAR(100) | NOT NULL | State/province name |
City
Cities and municipalities.
| Column | Type | Constraints | Description |
|---|---|---|---|
| CityId | INT | PK, IDENTITY | Primary key |
| StateProvinceId | INT | FK → StateProvince | State/province ref |
| CityName | NVARCHAR(100) | NOT NULL | City name |
| Latitude | DECIMAL(9,6) | NULL | Latitude coordinate |
| Longitude | DECIMAL(9,6) | NULL | Longitude coordinate |
Content
Brewery and beer content tables.
BreweryPost
Brewery information posts.
| Column | Type | Constraints | Description |
|---|---|---|---|
| BreweryPostId | INT | PK, IDENTITY | Primary key |
| UserAccountId | INT | FK → UserAccount | Author |
| BreweryName | NVARCHAR(200) | NOT NULL | Brewery name |
| Description | NVARCHAR(MAX) | NULL | Description |
| CreatedAt | DATETIME2 | NOT NULL | Creation timestamp |
| UpdatedAt | DATETIME2 | NOT NULL | Last update |
BreweryPostLocation
Geospatial data for breweries.
| Column | Type | Constraints | Description |
|---|---|---|---|
| BreweryPostLocationId | INT | PK, IDENTITY | Primary key |
| BreweryPostId | INT | FK → BreweryPost | Brewery reference |
| CityId | INT | FK → City | City reference |
| Address | NVARCHAR(255) | NULL | Street address |
| Location | GEOGRAPHY | NULL | Spatial data |
Spatial Index: On Location column for geospatial queries.
BeerPost
Individual beer posts.
| Column | Type | Constraints | Description |
|---|---|---|---|
| BeerPostId | INT | PK, IDENTITY | Primary key |
| BreweryPostId | INT | FK → BreweryPost | Brewery ref |
| UserAccountId | INT | FK → UserAccount | Author |
| BeerName | NVARCHAR(200) | NOT NULL | Beer name |
| Description | NVARCHAR(MAX) | NULL | Description |
| ABV | DECIMAL(4,2) | NULL | Alcohol by volume |
| IBU | INT | NULL | International Bitterness Units |
| BeerStyleId | INT | FK → BeerStyle | Style category |
| CreatedAt | DATETIME2 | NOT NULL | Creation timestamp |
Photo
Image metadata.
| Column | Type | Constraints | Description |
|---|---|---|---|
| PhotoId | INT | PK, IDENTITY | Primary key |
| Url | NVARCHAR(500) | NOT NULL | Image URL |
| CloudinaryPublicId | NVARCHAR(255) | NULL | Cloudinary ID |
| Width | INT | NULL | Image width |
| Height | INT | NULL | Image height |
| Format | NVARCHAR(10) | NULL | Image format |
| CreatedAt | DATETIME2 | NOT NULL | Upload timestamp |
Stored Procedures
All data access is performed through stored procedures for:
- Performance optimization
- Centralized business logic
- Enhanced security
- Better query plan caching
User Management Procedures
USP_RegisterUser
Creates a new user account with credentials.
Parameters:
@Username- Unique username@Email- Email address@FirstName- First name@LastName- Last name@PasswordHash- Argon2id hash@PasswordSalt- Random salt
Returns: UserAccountId of created user
Logic:
- Validates username and email uniqueness
- Creates UserAccount record
- Creates UserCredential record
- Creates UserVerification record (unverified)
- Returns user ID
USP_GetUserAccountByUsername
Retrieves user account and credential by username.
Parameters:
@Username- Username to look up
Returns: User account details + active credential
USP_RotateUserCredential
Updates user password (credential rotation).
Parameters:
@UserAccountId- User ID@NewPasswordHash- New password hash@NewPasswordSalt- New salt
Logic:
- Deactivates current credential
- Creates new active credential
- Updates CredentialRotatedAt timestamp
Location Procedures
USP_CreateCountry
Inserts a new country or retrieves existing.
USP_CreateStateProvince
Inserts a state/province or retrieves existing.
USP_CreateCity
Inserts a city or retrieves existing.
Data Seeding
The Database.Seed project populates development/test databases with:
Location Data
- 3 countries (US, Canada, Mexico)
- 50 US states + DC
- 13 Canadian provinces/territories
- 32 Mexican states
- Major cities for each region
Test Users
- Admin account
- Sample user accounts with verified emails
Sample Data (Future)
- Breweries
- Beer styles
- Sample posts
Migrations
DbUp Migration Strategy
Migration Files: src/Core/Database/Database.Migrations/scripts/
Naming Convention: ###-DescriptiveName.sql
001-CreateUserTables.sql002-CreateLocationTables.sql003-CreateContentTables.sql
Execution:
- Migrations run on container startup
- Tracked in
SchemaVersionstable - Idempotent (safe to run multiple times)
- Sequential execution order
Development Mode:
Set CLEAR_DATABASE=true to drop and recreate (dev/test only).
Adding New Migrations
- Create new SQL file in
scripts/folder - Use next sequential number
- Write idempotent SQL
- Test locally before committing
- Rebuild migration container
Example Migration:
-- 004-AddUserBio.sql
IF NOT EXISTS (
SELECT * FROM sys.columns
WHERE object_id = OBJECT_ID('UserAccount')
AND name = 'Bio'
)
BEGIN
ALTER TABLE UserAccount
ADD Bio NVARCHAR(500) NULL;
END
GO
Query Performance
Indexes
Strategic indexes for common queries:
- Username lookup (unique index)
- Email lookup (unique index)
- User followers (composite index)
- Brewery location (spatial index)
- Created date sorting
###Execution Plans
Stored procedures allow SQL Server to:
- Cache execution plans
- Optimize query performance
- Reduce compilation overhead
- Enable query hints when needed
Backup & Recovery
Development
No backup strategy (fresh database from migrations).
Production (Future)
- Automated daily backups
- Point-in-time recovery
- Geo-redundant storage
- Backup retention policy
Monitoring
Queries to Monitor
- User login frequency
- Slow-running procedures
- Lock contention
- Index fragmentation
Tools
- SQL Server Management Studio (SSMS)
- JetBrains Rider