ENGINEERING · CROSS-CUTTING SUBSYSTEM · 10-05-2026
Runtime configuration system — the configurations table, its caches, every consumer service, the admin edit flow, and the canonical key registry.
| Metric | Value |
|---|---|
| Total config keys | 222 |
| Categories | 11 (ANOMALY, CRYPTO, GLOBAL, ITEMS, LIMITS, PRICING, SALE, STRIPE, SYSTEM, TRADEBOT, USER) |
| Consumer modules cited | 20+ across tradeit-backend + tradeit-admin-backend |
| ConfigurationService implementations | 2 — tradeit-backend (JS, L1+L2) and tradeit-admin-backend (TS/Prisma, L2 only) |
| L1 cache TTL | 60s in-process Map (backend only) |
| L2 cache TTL | 180s Redis (configuration:<key>) — both services |
| Admin edit cache invalidation | NONE — admin writes do NOT publish DEL or pub/sub; stale window up to 3 min |
MAX(updated_at) | 2026-04-29 08:24:04 UTC (analytics replica is DMS-stale since 2026-04-30) |
Indexes on configurations | PRIMARY on id only — no unique index on key |
steamarbitrage.configurations MySQL tableConfigurationService runtime layers and their Redis namespace configuration:* / configurationCategory:*Configuration.vue view)configurationKeys enum (canonical named keys).env*) — owned by deployment / dotenv-vaultpricing.* schema) — separate subjectnumBots but the rules live in botsServiceThe line between "platform config" and "feature flag" is fuzzy — this table holds both. Anything an admin/ops user can flip without a deploy lands here.
There are two independent service implementations that read/write the same MySQL table and share the same Redis key namespace:
tradeit-backend ConfigurationService (JS, two-layer cache)server/service/configurationService.jsthis.cacheConfiguration[key] plain JS object, 60s TTL per key (Date.now() + 60 * 1000)configuration:<key> (per-key) and configurationCategory:<category>:<0|1> (category fetch), both 180sgetValueByKey: L1 hit → L2 hit (and re-populate L1) → MySQL (and populate both layers)updateValueByKey(key, value) is called by anomaly circuit breakers. Clears L1 immediately, writes to MySQL, then Redis.del(...)s the L2 key inside a setTimeout(1000) deferred block.tradeit-admin-backend ConfigurationService (TS/Prisma, Redis-only)src/services/configurationService.tsMainService (an EventEmitter base) — emits created / updated / deleted events that activityListeners.ts writes into the audit trailgetValueByKey reads Redis (configuration:<key>), falls through to Prisma findUnique, then Redis setex for 180supdate) writes via Prisma and emits the audit event — but does not publish a Redis DEL or invalidation messageBoth services therefore share the L2 cache key namespace but have no coordinated invalidation. See §9 and §11.
tradeit-backend runs on every backend EC2 instance — multiple Node.js processes per host, each with its own L1.tradeit-admin-backend runs as a single deployment fronting the Vue admin SPA.
graph LR
AdminUI["tradeit-admin Vue / Configuration.vue"] -->|"PATCH /configurations/:id"| AdminBE["tradeit-admin-backend / controllers/configuration.ts"]
AdminBE -->|"prisma.configurations.update"| MySQL[("MySQL steamarbitrage.configurations")]
AdminBE -->|"emit updated -> activityListeners"| ActivityLog[("admin_activities")]
AdminBE -.->|"NO INVALIDATION"| Redis[("Redis configuration:key + configurationCategory:cat:flag")]
Backend["tradeit-backend ConfigurationService"] -->|"L1 miss -> GET configuration:key"| Redis
Redis -.->|"L2 miss after 180s TTL"| Backend
Backend -->|"SELECT * FROM configurations"| MySQL
Backend -->|"SETEX configuration:key 180"| Redis
Bootstrap["GET /api/v2/configuration"] -->|"getGlobalConfigs useHashKey=true"| Backend
Consumers["20+ consumer modules"] -->|"getValueByKey / getValuesByKeys"| Backend
Breaker["userLimitationService.js / tradeOutSummaryService.js"] -->|"updateValueByKey on anomaly"| Backend
Backend -->|"DEL configuration:key + UPDATE row"| MySQL
Backend -->|"DEL configuration:key"| Redis
The dashed NO INVALIDATION edge from AdminBE to Redis is the most important detail in the diagram. Admin edits land in MySQL and the audit log but do not touch the Redis cache used by tradeit-backend. Backend instances continue to serve the previous value until the per-key 180s TTL expires (or 60s if the L1 entry was already populated from the stale Redis value).
tradeit-backend (public/frontend)| Method | Path | Purpose |
|---|---|---|
| GET | /api/v2/configuration | Frontend bootstrap — returns GLOBAL category (hash-keyed), country, intercomJwt, isAdmin, csgoCollections, csgoTypeMap |
| GET | /api/v2/configuration/stats | Server status stats (not config values) |
Source: server/routes/configuration.js + server/controllers/configuration.js.
tradeit-admin-backend (admin CRUD)| Method | Path | Purpose |
|---|---|---|
| GET | /configurations | List all rows |
| POST | /configurations | Create new key |
| PATCH | /configurations/:id | Update value (validates against stored type) |
| DELETE | /configurations/:id | Remove a key |
All four guarded by [isLoggedIn, isAdmin]. Source: routes/configurations.ts:11-14, mounted at routes/index.ts:75.
Two services in tradeit-backend write to configurations via updateValueByKey without an admin in the loop:
server/service/userLimitationService.js:357 — sets tradesDisabled=1 when limit anomalies tripserver/service/userLimitationService.js:377 — sets disableOtherGames=1 for non-CSGO surgesserver/service/userLimitationService.js:398 — sets largeTradesDisabled=1 for large-trade anomaliesserver/service/tradeOutSummaryService.js:70 — sets tradesDisabled=1 from outflow summary checksThese are self-flipping kill switches: code disables a feature, ops admin manually re-enables it after investigation. They DO correctly invalidate L1 and L2 (DEL configuration:<key>) — but the category cache configurationCategory:GLOBAL:* is not invalidated.
| Key | TTL | Set by | Purpose |
|---|---|---|---|
configuration:<key> | 180s | tradeit-backend & tradeit-admin-backend getValueByKey | Per-key string cache |
configurationCategory:<category>:0 | 180s | tradeit-backend getConfigsByCategory(cat, false) | Full category fetch, plain keys |
configurationCategory:<category>:1 | 180s | tradeit-backend getConfigsByCategory(cat, true) | Full category fetch, hash-keyed (compact frontend payload) |
Key gotcha: per-key DEL on updateValueByKey clears configuration:<key> only — it does not touch the category caches. A flipped tradesDisabled does not propagate into a fresh GLOBAL bootstrap fetch until the category TTL expires. Frontend-visible flags surfaced through GET /api/v2/configuration have effective worst-case staleness of 180s (category cache) on top of the per-key invalidation.
No pub/sub channel exists for cross-instance invalidation. Each tradeit-backend process maintains its own L1 — invalidation is per-process via the local delete this.cacheConfiguration[key].
CREATE TABLE steamarbitrage.configurations (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
category VARCHAR(15) NULL,
`key` VARCHAR(255) NULL,
value VARCHAR(2000) NULL,
type VARCHAR(100) NULL, -- 'string' | 'int' | 'tinyint'
key_hash VARCHAR(10) NULL, -- short hash alias for compact frontend payloads
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
PRIMARY KEY (id)
);
SHOW INDEX FROM steamarbitrage.configurations returns only the PRIMARY KEY on id. Despite the unique-by-name read pattern (getByKey, getByKeyAndCategory), there is no MySQL-enforced UNIQUE on key or (category, key). App-layer uniqueness is enforced by tradeit-admin-backend/repositories/configurationRepo.ts:36.
| Category | Row count | Use |
|---|---|---|
| ANOMALY | 12 | Inventory anomaly thresholds |
| CRYPTO | 1 | Crypto payment controls |
| GLOBAL | 77 | Frontend-surfaced platform settings |
| ITEMS | 1 | Item-level flags |
| LIMITS | 3 | Hard hourly outflow caps |
| PRICING | 15 | Pricing algorithm parameters |
| SALE | 3 | Sale/listing controls |
| STRIPE | 2 | Stripe level-0 verification |
| SYSTEM | 98 | Backend runtime toggles, bot fleet, kill switches |
| TRADEBOT | 1 | Tradebot-specific limits |
| USER | 9 | User tier limits and gates |
key_hash columnkey_hash is a 6-8 char hex string stored alongside each row, used by getConfigsByCategory(category, useHashKey=true) to produce a compact response (frontend never sees the full key name). Some rows have key_hash = NULL (e.g., all of LIMITS, instantSellPriceAdjustPercent) — those rows will not surface in hash-keyed payloads. Hash is stored, not computed at runtime; admin-backend does not auto-fill it on insert/update — must be set manually or via SQL seed.
N/A — reason: Configuration values are not indexed in OpenSearch. The configurations table is the source of truth and Redis is the only secondary store. There is no full-text search requirement; admin UI does in-memory filtering on the full getAll response.
The cache invalidation gap is the only async behavior worth calling out — and it is itself the absence of an async path.
update in tradeit-admin-backend writes MySQL, emits an in-process MainService.emit('updated', ...) for admin_activities audit logging, then returns. No Redis DEL. No pub/sub publish. No HTTP callback to tradeit-backend.tradeit-backend has no subscriber for config invalidation messages. Each process drains its L1 on the 60s timer and re-reads from L2 (which is also stale until 180s TTL expires).Worst case for a GLOBAL admin edit landing on a logged-in user's screen:
configuration:<key> cache stays stale until T+180s (no DEL).configurationCategory:GLOBAL:1 stays stale until its own T+180s window expires.End-to-end visibility of an admin edit on the /api/v2/configuration bootstrap response: up to 180s in the typical case, longer if the category cache was warmed just before the edit.
The internal circuit-breaker writes (updateValueByKey from userLimitationService / tradeOutSummaryService) DO correctly DEL the per-key cache, so a self-flip propagates to other backend instances within 60s — but the category cache remains stale up to 180s here too.
Every key in the live configurations table grouped by category. Sensitive values are marked REDACTED. Live values pulled from analytics replica snapshot 2026-04-29. Most numeric currency values are stored in cents (the platform's internal money unit).
| Key | Type | Description |
|---|---|---|
highDailyInflowValue | int | Daily inflow threshold ratio for anomaly trip |
highDailyInPrice | int | Daily inbound-price threshold |
highDailyInVolume | int | Daily inbound-volume threshold |
highDailyOutflowValue | int | Daily outflow threshold ratio |
highHourlyInflowValue | int | Hourly inflow threshold |
highHourlyInPrice | int | Hourly inbound-price threshold |
highHourlyInVolume | int | Hourly inbound-volume threshold |
highHourlyOutflowValue | int | Hourly outflow threshold |
lowHourlyInflowValue | int | Hourly low-inflow trip |
lowHourlyOutflowValue | int | Hourly low-outflow trip |
lowWeeklyInVolume | int | Weekly low-inbound-volume trip |
outPriceDropLast | int | Last-trade outbound price drop threshold |
| Key | Type | Description |
|---|---|---|
maxActiveCryptoCharges | int | Max concurrent NowPayments charges per user |
GET /api/v2/configurationTwo-column listing for compactness. Type column shows the stored type field; (null) = type is NULL in DB (admin-backend skips type-cast for these).
| Key | Type |
|---|---|
admins | string |
affiliateBaseDiscountPercent | int |
availableLoginWithCodeCountries | string |
blackfriday | tinyint |
directBuyFromStackItemIds | string |
disableOtherGames | int |
easterCouponCode | string |
enableBalanceListingsRevenueWithdraw | tinyint |
enableBankListingsRevenueWithdraw | tinyint |
enableBundle | (null) |
enableCryptoListingsRevenueWithdraw | tinyint |
enableCryptoTopup | tinyint |
enableCsgoStoreDirectCartPopup | int |
enableDeficitIndication | int |
enableFirstTradeBonus | int |
enableGeoIpLangCurrencyAuto | (null) |
enableGoogleCustomerReviews | int |
enableInvestEnter | tinyint |
enableInvestExit | tinyint |
enableLocalCurrency | int |
enableLockedStoreBalanceRevenueWithdraw | tinyint |
enableLoginWithCode | int |
enableLootbear | int |
enableRealtimeCartChecker | tinyint |
enableRussiaPayment | int |
enableSkinSet | tinyint |
enableSteamItem | tinyint |
enableStoreBalanceRevenueWithdraw | tinyint |
enableTradeLocked | tinyint |
firstTradeBonusThreshold | string |
firstTradeMaxBonus | int |
guessGameRoundTimeSecond | int |
haloSkinMarkupStorePercent | int |
isDisableWithdrawLimited | int |
isEmailVerificationEnabled | int |
isInstantSellDisabled | tinyint |
isSaleDisabled | tinyint |
isSellBalanceDisabled | tinyint |
maxDiscountPercent | int |
| Key | Type |
|---|---|
maxItemCountInCollection | int |
maxMultipleOfPrice | int |
maxTagCountInCollection | int |
maxTopupPercent | int |
maxTopupStoreBalanceDaily | int |
minimumCryptoTopupAmount | string |
minInstantSellPrice | int |
minSalePrice | int |
monnectProcessingFeePercent | int |
nowPaymentsProcessingFeePercent | int |
numberOfChickenClick | int |
pricingIncludeStoreItemStats | int |
promotionImage | string |
promotionText | string |
qaAllowedUserIds | (null) |
recommendedSalePercent | int |
saleFeePercent | int |
shouldShowGleamGiveAwayBanner | int |
showTwitch | int |
siteMessage | string |
skinIqLinkDisplay | int |
skinIqTicketCostPerRound | int |
steamCInvCacheTTLMinutes | int |
storeDailyPurchaseLimit | int |
storeDiscountPercent | int |
storePriceMarkup | string |
stripeFeePercent | string |
stripeFixedFeeUSD | string |
topupCardBonusPercent | int |
topupCryptoBonusPercent | int |
topupKinguinGiftcardBonusPercent | int |
tradeLockBaseFee | tinyint |
tradeLockChangeOverTime | tinyint |
withdrawCryptoMin | tinyint |
withdrawCryptoProcessingFeePercent | tinyint |
withdrawLimitedPercent | int |
withdrawStripeMin | tinyint (REDACTED — Stripe min) |
withdrawStripeProcessingFeePercent | tinyint (REDACTED — Stripe fee) |
Note: a handful of GLOBAL rows have type = NULL (e.g., enableBundle, enableGeoIpLangCurrencyAuto, qaAllowedUserIds). The TS update validation in tradeit-admin-backend/controllers/configuration.ts will pass any value through for these rows because the type-cast switch falls to default.
| Key | Type | Description |
|---|---|---|
minDopplerItemsStuckPrices | int | Threshold for the doppler-stuck-prices alert |
| Key | Type | Description |
|---|---|---|
hourlyLimitHardOutValue | int | Hard ceiling on hourly outflow value (cents) |
hourlyLimitSoftOutValue | int | Soft warning threshold on hourly outflow |
largeTradeValue | int | Threshold above which a trade is "large" (cents) |
| Key | Type | Description |
|---|---|---|
adjustByLiveToStablePriceRatio | tinyint | Toggle live-to-stable price adjustment |
avg7DStableBotPriceModifier | string | Modifier for 7-day stable bot price |
blockDepositByAvg7DStablePrice | tinyint | Block deposits when 7d stable price guard trips |
blockDepositByPrevMonthPrice | tinyint | Block deposits when prev-month price guard trips |
cardIdVerificationAmount | int | Card-payment ID verification trigger amount |
decreaseDepositPriceForCheapItems | tinyint | Toggle cheap-item deposit haircut |
liveToStableModifier | string | Live-to-stable conversion modifier |
pricingMaxBotPlayerPrice | int | Max bot-player price ceiling |
pricingMaxBotPrice | int | Max bot price ceiling |
pricingMaxWantedMaxStock | int | Max stock cap for the wanted-list pricing path |
pricingTableVersion | string | Active pricing table version (e.g., items_market_v5) |
settledMaxStockEnabled | tinyint | Toggle the settled-max-stock pricing path |
tradePriceMarkup | string | Markup applied to trade prices |
wantedMaxStockLowCapacityRatio | string | Low-capacity ratio for wanted-list cap |
weeklyInOutRatioWeight | string | Weight applied to weekly in/out ratio in pricing model |
| Key | Type | Description |
|---|---|---|
instantSellPriceAdjustPercent | string | Instant-sell price adjustment percent |
limitSalePriceUpdateMinutes | int | Cooldown between user sale-price edits |
minDivisorOfPrice | int | Minimum sale-price divisor relative to base |
| Key | Type | Description |
|---|---|---|
enableLevel0Check | tinyint | Toggle Stripe level-0 verification |
level0CheckLimit | int | Amount above which level-0 check triggers (cents) |
Two-column listing for compactness.
| Key | Type |
|---|---|
affiliateWithdrawDaily | int |
allowGeneratePromptLocales | int |
botDangerousPercentage | string |
botDisabled | string |
botsGoodSizeLimit | int |
botsVeryGoodSizeLimit | int |
botWarningPercentage | string |
cashListingsFee | int |
couponBalanceMax | int |
couponBalanceMaxDaily | int |
couponBalanceMaxPerUser | int |
couponStoreBalanceMaxDaily | int |
couponStoreBalanceMaxPerUser | int |
enable24HrTradeSurgeCheck | int |
enable6HrTradeSurgeCheck | int |
enableAIContentGeneration | int |
enableCSGOCasesStockCheck | tinyint |
enableCSGOStockCheck | tinyint |
enableCsMoneyPriceCheck | tinyint |
enableDopplerPriceCrash | int |
enableListingHack | int |
enablePricingSyncChecker | int |
enableTradeUrlInventory | tinyint |
haloSkinBufferPriceMax | int |
haloSkinBufferPricePercent | int |
haloSkinEnableBestPriceItem | int |
haloSkinMarkupTradePercent | int |
haloSkinMaxPerBestPriceItem | int |
haloSkinMaxPerItem | int |
haloSkinMaxPrice | int |
haloSkinMinPrice | int |
haloSkinStoreDailyLimit | int |
haloSkinStoreEnabled | int |
haloSkinTradeDailyLimit | int |
haloSkinTradeEnabled | int |
haloStablePriceMultiplierCheck | int |
hideBotsItemsFromInventory | string |
hourlyLimitHardOutValueOtherGames | int |
inventoryDangerousPercentage | int |
inventoryWarningPercentage | int |
investWithdrawLimit | int |
largeTradesDisabled | tinyint |
listingHackAdmin | string |
listingHackDiscountPercent | int |
listingHackFromPrice | int |
listingHackFromStock | int |
listingHackMinPricePercent | int |
loginByCodeDailyLimitPerSteamId | int |
lootbearDailyLimit | int |
| Key | Type |
|---|---|
lootbearGlobalDailyLimit | int |
maxAddBalanceFromAdminPanel | int |
maxAddBalanceFromAdminPanelBySupporter | int |
maxAttemptLoginByCode | int |
maxStoreTopupPerUserLast24Hours | int |
maxTopupBalanceDaily | int |
maxTopupCryptoDailyGlobalStore | int |
maxTopupCryptoDailyGlobalTrade | int |
maxTradeTopupPerUserLast24Hours | int |
minLevelLoginByCode | int |
minMinuteBetweenTrades | int |
monnectMaxTopupStoreDaily | int |
monnectMaxTopupTradeDaily | int |
newBotStartIndex | int (live: 540) |
numBots | int (live: 541 — total bot fleet size) |
oauth2SellOrBuyBig | int |
oauth2TradeBig | int |
oauth2TradeLowMedium | int |
oauth2TradeMedium | int |
oauth2TradeSmall | int |
revertTradeMissingMax | int |
saleOfferMaxTotalListing | int |
saleWithdrawAllTimeLimit1 | int |
saleWithdrawAllTimeLimit2 | int |
saleWithdrawAllTimeLimit3 | int |
saleWithdrawLimit | int |
saleWithdrawLimit1 | int |
saleWithdrawLimit2 | int |
saleWithdrawLimit3 | int |
saleWithdrawStoreDailyLimit | int |
saleWithdrawStoreLimit1 | int |
saleWithdrawStoreLimit2 | int |
saleWithdrawStoreLimit3 | int |
sentTradesValueLimit1 | int |
sentTradesValueLimit2 | int |
sentTradesValueLimit3 | int |
siteDisabled | tinyint (live: 0 — kill switch) |
skipStockCheckFromPrice | int |
storeCryptoFeePercent | int |
storeSteamLevelsLimited | string |
trade24hSurgeRatio | int |
trade6hSurgeRatio | int |
tradesDisabled | tinyint (live: 0 — auto-flipped by anomaly) |
userInvestWithdrawLimit | int |
userPriceDeductPercentForRustAndTf2 | int |
uuskinStoreDailyLimit | int |
wikiAiContentTtlInDay | int |
withdrawLockedStoreBalanceSystemLimit | int |
withdrawLockedStoreBalanceUserLimit | int |
| Key | Type | Description |
|---|---|---|
unstoreLimit | int | Tradebot unstore limit threshold |
| Key | Type | Description |
|---|---|---|
allowedPendingPurchases | int | Max concurrent pending purchases per user |
balanceLimitLevel1 | int | Balance ceiling for tier 1 (new/unverified) |
balanceLimitLevel2 | int | Balance ceiling for tier 2 (verified) |
balanceLimitLevel3 | int | Balance ceiling for tier 3 (trusted) |
sentTradesValueLimit | int | Default outbound trade value cap |
tradeLimitLevel1 | int | Tier-1 daily trade frequency cap |
tradeLimitLevel2 | int | Tier-2 daily trade frequency cap |
tradeLimitLevel3 | int | Tier-3 daily trade frequency cap |
updateLimitThreshold | int | Trade-update rate-gate threshold |
Total registered keys: 12 + 1 + 77 + 1 + 3 + 15 + 3 + 2 + 98 + 1 + 9 = 222 (matches SELECT COUNT(*) FROM configurations).
After an admin saves a value, tradeit-backend continues serving the previous value until the 180s Redis TTL expires (per-key) and the 180s category-cache TTL expires (for GLOBAL keys consumed via the bootstrap endpoint). There is no DEL or pub/sub triggered by the admin write. The L1 in-process cache adds another 60s on top, but it is bounded by the L2 TTL.
Mitigations:
redis-cli DEL configuration:<key> manually.PUBLISH on admin update + a SUBSCRIBE listener in tradeit-backend's ConfigurationService that DELs both per-key and category caches.server/service/userLimitationService.js:357,377,398 and server/service/tradeOutSummaryService.js:70 write tradesDisabled=1, disableOtherGames=1, or largeTradesDisabled=1 directly into the configurations table when anomaly thresholds (driven by ANOMALY category keys) trip. All trades pause platform-wide.
Recovery flow:
0.tradeit-backend processes — the breaker code did DEL the per-key Redis cache, so L2 refresh is immediate, but each process's L1 still needs to expire.Risk: if the anomaly threshold is tuned too tight, the breaker can re-trip immediately after admin re-enables. The breaker code does not back off — it writes 1 again on the next anomaly tick.
All value columns are VARCHAR(2000). Consumer code in tradeit-backend does manual casts:
Number(...) for ints (e.g., mainQueue.js:152).JSON.parse(...) for stringified JSON arrays (e.g., botsService.js:189 parsing hideBotsItemsFromInventory, tradeBonusService.js:55 parsing firstTradeBonusThreshold).parseInt(...) after getValueByKey for some thresholds.siteSelectedPrice > parseInt(largeTradeValue)).A row with the wrong type (or type = NULL) plus malformed value yields a runtime cast failure or — worse — a silent NaN that compares falsy. There is no integration test covering all 222 rows' parseability.
If the row for a key is deleted (or never seeded), getByKey returns undefined and getValueByKey returns null (no default in code). For limit checks that compare actual >= limit, null makes the comparison false and the limit effectively becomes infinity. This is the failure mode behind several historical "limit not enforced" incidents.
| Source | Where | Notes |
|---|---|---|
| Datadog APM | tradeit-backend.service.configurationService.* spans | One span per getValueByKey / updateValueByKey call |
| Datadog logs | service:tradeit-backend "step:updateValueByKey" | Errors from the deferred MySQL write inside setTimeout (configurationService.js:102) |
| MySQL audit | admin_activities table | Every admin CRUD on configurations recorded via MainService event listeners |
| Redis monitoring | SCAN configuration:* (do NOT run KEYS in prod) | Cache fill ratio observable from populated keys vs 222 total |
| Slack alerts | Anomaly detection alerts via slackService | Posted alongside the auto-flip writes in userLimitationService / tradeOutSummaryService |
There is no metric on cache hit rate, TTL miss rate, or per-key read frequency. A consumer reading tradesDisabled 1000x/sec hits L1 99%+ of the time, but this is invisible without bespoke logging.
All file:line citations validated against current HEAD on 2026-05-10.
Repo SHAs (current HEAD): tradeit-backend a99bf216 · tradeit-admin-backend 6df567ed · tradeit-admin a2e94cff
| Location | Description |
|---|---|
tradeit-backend/server/repository/configurationRepo.js:5 | getByCategory(category) — SELECT * WHERE category = :category |
tradeit-backend/server/repository/configurationRepo.js:11 | getByKey(key) — destructures [[config]] |
tradeit-backend/server/repository/configurationRepo.js:17 | getByKeys(keys) — uses .query() not .execute() (IN list quirk) |
tradeit-backend/server/repository/configurationRepo.js:24 | updateByKey(key, value) — direct UPDATE |
tradeit-admin-backend/repositories/configurationRepo.ts:8 | list() — prisma.configurations.findMany() |
tradeit-admin-backend/repositories/configurationRepo.ts:12 | getByKey(key) — findUnique |
tradeit-admin-backend/repositories/configurationRepo.ts:20 | getById(id) |
tradeit-admin-backend/repositories/configurationRepo.ts:28 | getByCategory(category) |
tradeit-admin-backend/repositories/configurationRepo.ts:36 | getByKeyAndCategory — pre-insert app-layer uniqueness |
tradeit-admin-backend/repositories/configurationRepo.ts:45 | create({...}) |
tradeit-admin-backend/repositories/configurationRepo.ts:58 | update({id, category, key, value}) |
tradeit-admin-backend/repositories/configurationRepo.ts:73 | delete(id) |
| Location | Description |
|---|---|
tradeit-backend/server/service/configurationService.js:10 | cacheConfiguration = {} — L1 in-process Map |
tradeit-backend/server/service/configurationService.js:12 | getGlobalConfigs(useHashKey=false) — shorthand for GLOBAL |
tradeit-backend/server/service/configurationService.js:16 | getConfigsByCategory(category, useHashKey) — Redis 180s |
tradeit-backend/server/service/configurationService.js:38 | getValueByKey(key, useCache=true) — L1 → L2 → MySQL |
tradeit-backend/server/service/configurationService.js:71 | getValuesByKeys(keys) — bulk fetch via repo .query(), no cache |
tradeit-backend/server/service/configurationService.js:94 | updateValueByKey — clears L1, deferred MySQL write, then Redis DEL |
tradeit-admin-backend/services/configurationService.ts:7 | class ConfigurationService extends MainService |
tradeit-admin-backend/services/configurationService.ts:50 | emit('updated', ...) — old + new payload for diff |
tradeit-admin-backend/services/configurationService.ts:66 | getValueByKey — Redis-only, 180s, no L1, no DEL on update |
| Location | Description |
|---|---|
tradeit-backend/server/routes/configuration.js:6-7 | GET / and GET /stats |
tradeit-backend/server/controllers/configuration.js:11 | getConfiguration — composes bootstrap response |
tradeit-admin-backend/routes/configurations.ts:11-14 | All four CRUD routes guarded by [isLoggedIn, isAdmin] |
tradeit-admin-backend/controllers/configuration.ts:71 | update — type validation switch before persist |
| Location | Description |
|---|---|
tradeit-admin/src/views/Configuration.vue | Inline-edit table view |
tradeit-admin/src/store/configuration.ts:33 | Vuex fetch action |
tradeit-admin/src/store/configuration.ts:54 | createConfiguration — note: no updateConfiguration action; updates go straight from view to network |
tradeit-admin/src/networks/configurationNetwork.ts:13 | update → PATCH /configurations/:id |
Full list (40+ citations) in the wiki source. Highlights below.
| Location | Method | Keys read / written |
|---|---|---|
server/index.js:118 | getValueByKey | siteDisabled — gates all public endpoints on startup |
server/controllers/trade.js:301-304 | getValueByKey x3 | largeTradeValue, tradesDisabled, largeTradesDisabled |
server/service/botsService.js:33-35 | getValueByKey x2 | numBots (live: 541), newBotStartIndex (540) |
server/service/tradeOutSummaryService.js:70 | updateValueByKey | writes tradesDisabled = 1 |
server/service/userLimitationService.js:357 | updateValueByKey | writes tradesDisabled = 1 |
server/service/userLimitationService.js:377 | updateValueByKey | writes disableOtherGames = 1 |
server/service/userLimitationService.js:398 | updateValueByKey | writes largeTradesDisabled = 1 |
server/middlewares/nowpayment.js:31 | getValueByKey(..., useCache=false) | minimumCryptoTopupAmount — bypasses both cache layers |
server/redis/tradeRedis.js:21 | getValueByKey | numBots — used to build per-bot Redis key list |
| Location | Description |
|---|---|
tradeit-backend/server/config/enums.js:727 | export const configurationKeys = { ... } — named-key constant map. Always reference this enum; never hardcode strings. |
The Task 5 spelunk left 7 open questions; Task 6 resolved 6 of them. The remaining one is tracked.
| # | Question | Resolution |
|---|---|---|
| 1 | Category cache invalidation — is the 3-min stale window acceptable? | Open — tracked. Pub/sub-based invalidation is the right fix. Captured as a future-work item; needs Linear ticket on the next infra-debt sweep. No active production incident attributed to this lag, so it stays "known limitation," not P0. |
| 2 | Are there duplicate key values across categories? | Resolved. SELECT key, COUNT(*) FROM configurations GROUP BY key HAVING n > 1 returns zero rows. App-layer enforcement via getByKeyAndCategory is sufficient in practice. |
| 3 | Current value of numBots? | Resolved. Live: 541 (newBotStartIndex: 540, botsGoodSizeLimit: 900, botsVeryGoodSizeLimit: 850). |
| 4 | Does TRADEBOT category exist? | Resolved. Yes — 1 row: unstoreLimit (int). |
| 5 | What is key_hash? | Resolved. Stored 6-8 char hex, not auto-computed by either ConfigurationService. Some rows have key_hash = NULL (excluded from hash-keyed bootstrap responses). |
| 6 | tradeit-admin Vuex updateConfiguration action missing — does the local store go stale? | Resolved. Yes — admin view bypasses the store and calls configurationNetwork.update() directly. After save, component must re-fetch (or trigger store.dispatch('fetch')). UX paper-cut, not correctness bug. |
| 7 | getByKeys using .query() not .execute() — intentional? | Resolved. mysql2's .execute() (prepared statements) cannot bind a JS array as an IN (?) list. .query() with format(query, [keys]) is the documented escape hatch. |
Generated 10-05-2026 · tradeit.gg engineering