Problem
A game can currently only be associated with a single digital store (DigitalStore? — one value from Steam, GOG, Epic, Xbox, PSN, Nintendo, Other). In practice many games are owned on multiple stores (e.g. Steam + Epic giveaway, or Steam + GOG). Users have to pick one or create duplicate entries.
Current model
// Domain entity
public bool IsDigital { get; set; }
public DigitalStore? DigitalStore { get; set; } // single value
// Enum
public enum DigitalStore
{
Steam = 0, Gog = 1, Epic = 2, Xbox = 3,
Psn = 4, Nintendo = 5, Other = 99,
}
Frontend: checkbox "Digital copy" + single <Select> dropdown for store (GameForm.tsx:202).
Proposed solution
Replace the single-value DigitalStore? with a collection of stores (flags enum or join table).
Approach A — Flags enum (recommended for MVP)
Change DigitalStore to a [Flags] enum and store as a bitmask on the Game entity:
[Flags]
public enum DigitalStore
{
None = 0,
Steam = 1,
Gog = 2,
Epic = 4,
Xbox = 8,
Psn = 16,
Nintendo = 32,
Other = 64,
}
Entity change:
// Remove IsDigital — derive from DigitalStore != None
public DigitalStore DigitalStores { get; set; } = DigitalStore.None;
Remove IsDigital property; a game is digital when DigitalStores != DigitalStore.None. Physical + digital ownership is already modeled separately (the Platform field covers the physical side).
Pros: Zero schema migration complexity (single int column), no new entity, consistent with how MovieFormat already works in this codebase.
Cons: Limited to ~7 predefined stores before running into bit-position constraints (currently 7 + None = 8 values, fits in a byte). Adding new stores requires an enum update + EF migration if values shift.
Approach B — Join table
New entity GameDigitalStore with GameId + DigitalStore columns. More flexible for future expansion (free-text store names, per-store acquisition dates/prices) but heavier lift.
Recommendation: Start with Approach A (flags enum). If per-store metadata is needed later, migrate to Approach B.
Changes
Backend
| File |
Change |
Domain/Enums/DigitalStore.cs |
Add [Flags], renumber as powers of 2, add None = 0 |
Domain/Entities/Game.cs |
Replace IsDigital (bool) + DigitalStore? (enum?) with DigitalStores (flags enum, default None) |
Infrastructure/Data/CollectifyDbContext.cs |
Update configuration if any |
Api/Endpoints/GasesEndpoints.cs |
Update DTO, filters, ApplyDto, ToDto — accept DigitalStores as bitmask int. Derive "is digital" from DigitalStores != None for filtering. |
| Migration |
New EF migration for column rename + backfill |
Frontend
| File |
Change |
services/types.ts |
Replace isDigital: boolean + digitalStore?: DigitalStore with digitalStores: number (bitmask). Add DIGITAL_STORE_FLAGS array mirroring MOVIE_FORMAT_FLAGS pattern. |
components/GameForm.tsx |
Replace checkbox + single select with toggle buttons (same UX as MOVIE_FORMAT_FLAGS in MovieForm.tsx) |
components/DetailView.tsx |
Show multiple store pills instead of single "Digital (Steam)" pill |
pages/GamesList.tsx |
Show store icon(s)/label(s) in tertiary line |
components/FiltersPanel.tsx |
Update digital store filter to multi-select checkboxes |
Data migration
Backfill existing rows: if IsDigital == true and DigitalStore is set, copy to corresponding bit in DigitalStores. If IsDigital == true but DigitalStore is null, set DigitalStores = Other.
Out of scope
Verification
dotnet build clean, dotnet test passes
- Add a game with Steam + Epic selected → both stored and displayed
- Edit an existing physical-only game, add digital stores → platform preserved
- Filter games by digital store → correct results
- Existing rows survive migration with stores preserved
Problem
A game can currently only be associated with a single digital store (
DigitalStore?— one value from Steam, GOG, Epic, Xbox, PSN, Nintendo, Other). In practice many games are owned on multiple stores (e.g. Steam + Epic giveaway, or Steam + GOG). Users have to pick one or create duplicate entries.Current model
Frontend: checkbox "Digital copy" + single
<Select>dropdown for store (GameForm.tsx:202).Proposed solution
Replace the single-value
DigitalStore?with a collection of stores (flags enum or join table).Approach A — Flags enum (recommended for MVP)
Change
DigitalStoreto a[Flags]enum and store as a bitmask on theGameentity:Entity change:
Remove
IsDigitalproperty; a game is digital whenDigitalStores != DigitalStore.None. Physical + digital ownership is already modeled separately (thePlatformfield covers the physical side).Pros: Zero schema migration complexity (single int column), no new entity, consistent with how
MovieFormatalready works in this codebase.Cons: Limited to ~7 predefined stores before running into bit-position constraints (currently 7 + None = 8 values, fits in a byte). Adding new stores requires an enum update + EF migration if values shift.
Approach B — Join table
New entity
GameDigitalStorewithGameId+DigitalStorecolumns. More flexible for future expansion (free-text store names, per-store acquisition dates/prices) but heavier lift.Recommendation: Start with Approach A (flags enum). If per-store metadata is needed later, migrate to Approach B.
Changes
Backend
Domain/Enums/DigitalStore.cs[Flags], renumber as powers of 2, addNone = 0Domain/Entities/Game.csIsDigital(bool) +DigitalStore?(enum?) withDigitalStores(flags enum, defaultNone)Infrastructure/Data/CollectifyDbContext.csApi/Endpoints/GasesEndpoints.csApplyDto,ToDto— acceptDigitalStoresas bitmask int. Derive "is digital" fromDigitalStores != Nonefor filtering.Frontend
services/types.tsisDigital: boolean+digitalStore?: DigitalStorewithdigitalStores: number(bitmask). AddDIGITAL_STORE_FLAGSarray mirroringMOVIE_FORMAT_FLAGSpattern.components/GameForm.tsxMOVIE_FORMAT_FLAGSinMovieForm.tsx)components/DetailView.tsxpages/GamesList.tsxcomponents/FiltersPanel.tsxData migration
Backfill existing rows: if
IsDigital == trueandDigitalStoreis set, copy to corresponding bit inDigitalStores. IfIsDigital == truebutDigitalStoreis null, setDigitalStores = Other.Out of scope
Verification
dotnet buildclean,dotnet testpasses