tradeit.gg

ENGINEERING · CROSS-CUTTING SUBSYSTEM · 10-05-2026

Platform Configuration

Runtime configuration system — the configurations table, its caches, every consumer service, the admin edit flow, and the canonical key registry.

Read on Wiki: /engineering/backend/configuration · Markdown source · Spelunk notes
Last deepened: 2026-05-10 · Tier A (foundation)
222
Total config keys
11
Categories
2
Service impls (BE + Admin BE)
180s
L2 Redis TTL
3 min
Worst-case stale window

Contents


§1 At-a-glance

MetricValue
Total config keys222
Categories11 (ANOMALY, CRYPTO, GLOBAL, ITEMS, LIMITS, PRICING, SALE, STRIPE, SYSTEM, TRADEBOT, USER)
Consumer modules cited20+ across tradeit-backend + tradeit-admin-backend
ConfigurationService implementations2 — tradeit-backend (JS, L1+L2) and tradeit-admin-backend (TS/Prisma, L2 only)
L1 cache TTL60s in-process Map (backend only)
L2 cache TTL180s Redis (configuration:<key>) — both services
Admin edit cache invalidationNONE — 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 configurationsPRIMARY on id only — no unique index on key

§2 Purpose & boundaries

Owns

  • The steamarbitrage.configurations MySQL table
  • Both ConfigurationService runtime layers and their Redis namespace configuration:* / configurationCategory:*
  • The admin CRUD path (controllers/repo/route + Vue Configuration.vue view)
  • The configurationKeys enum (canonical named keys)

Does NOT own

  • Environment variables (.env*) — owned by deployment / dotenv-vault
  • In-code feature flags / build-time toggles
  • Pricing-model parameter tables (pricing.* schema) — separate subject
  • Bot fleet routing rules — they read numBots but the rules live in botsService

The 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.

§3 Process / runtime model

There are two independent service implementations that read/write the same MySQL table and share the same Redis key namespace:

3.1 tradeit-backend ConfigurationService (JS, two-layer cache)

3.2 tradeit-admin-backend ConfigurationService (TS/Prisma, Redis-only)

Both services therefore share the L2 cache key namespace but have no coordinated invalidation. See §9 and §11.

3.3 Where the services run

§4 Architecture

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).

§5 Inputs & outputs

5.1 HTTP — tradeit-backend (public/frontend)

MethodPathPurpose
GET/api/v2/configurationFrontend bootstrap — returns GLOBAL category (hash-keyed), country, intercomJwt, isAdmin, csgoCollections, csgoTypeMap
GET/api/v2/configuration/statsServer status stats (not config values)

Source: server/routes/configuration.js + server/controllers/configuration.js.

5.2 HTTP — tradeit-admin-backend (admin CRUD)

MethodPathPurpose
GET/configurationsList all rows
POST/configurationsCreate new key
PATCH/configurations/:idUpdate value (validates against stored type)
DELETE/configurations/:idRemove a key

All four guarded by [isLoggedIn, isAdmin]. Source: routes/configurations.ts:11-14, mounted at routes/index.ts:75.

5.3 Internal writes — circuit breakers

Two services in tradeit-backend write to configurations via updateValueByKey without an admin in the loop:

These 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.

§6 Redis surface

KeyTTLSet byPurpose
configuration:<key>180stradeit-backend & tradeit-admin-backend getValueByKeyPer-key string cache
configurationCategory:<category>:0180stradeit-backend getConfigsByCategory(cat, false)Full category fetch, plain keys
configurationCategory:<category>:1180stradeit-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].

§7 MySQL surface

7.1 Table

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)
);

7.2 Indexes

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.

7.3 Categories (live, 222 rows)

CategoryRow countUse
ANOMALY12Inventory anomaly thresholds
CRYPTO1Crypto payment controls
GLOBAL77Frontend-surfaced platform settings
ITEMS1Item-level flags
LIMITS3Hard hourly outflow caps
PRICING15Pricing algorithm parameters
SALE3Sale/listing controls
STRIPE2Stripe level-0 verification
SYSTEM98Backend runtime toggles, bot fleet, kill switches
TRADEBOT1Tradebot-specific limits
USER9User tier limits and gates

7.4 key_hash column

key_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.

§8 OpenSearch surface

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.

§9 Async continuations

The cache invalidation gap is the only async behavior worth calling out — and it is itself the absence of an async path.

Worst case for a GLOBAL admin edit landing on a logged-in user's screen:

  1. Admin clicks save at T+0.
  2. MySQL row updates immediately.
  3. Per-key configuration:<key> cache stays stale until T+180s (no DEL).
  4. Category cache configurationCategory:GLOBAL:1 stays stale until its own T+180s window expires.
  5. Each backend process's L1 is at most 60s stale relative to its own L2 view.

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.

§10 Configuration registry

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).

ANOMALY (12)

KeyTypeDescription
highDailyInflowValueintDaily inflow threshold ratio for anomaly trip
highDailyInPriceintDaily inbound-price threshold
highDailyInVolumeintDaily inbound-volume threshold
highDailyOutflowValueintDaily outflow threshold ratio
highHourlyInflowValueintHourly inflow threshold
highHourlyInPriceintHourly inbound-price threshold
highHourlyInVolumeintHourly inbound-volume threshold
highHourlyOutflowValueintHourly outflow threshold
lowHourlyInflowValueintHourly low-inflow trip
lowHourlyOutflowValueintHourly low-outflow trip
lowWeeklyInVolumeintWeekly low-inbound-volume trip
outPriceDropLastintLast-trade outbound price drop threshold

CRYPTO (1)

KeyTypeDescription
maxActiveCryptoChargesintMax concurrent NowPayments charges per user

GLOBAL (77) — surfaced via GET /api/v2/configuration

Two-column listing for compactness. Type column shows the stored type field; (null) = type is NULL in DB (admin-backend skips type-cast for these).

KeyType
adminsstring
affiliateBaseDiscountPercentint
availableLoginWithCodeCountriesstring
blackfridaytinyint
directBuyFromStackItemIdsstring
disableOtherGamesint
easterCouponCodestring
enableBalanceListingsRevenueWithdrawtinyint
enableBankListingsRevenueWithdrawtinyint
enableBundle(null)
enableCryptoListingsRevenueWithdrawtinyint
enableCryptoTopuptinyint
enableCsgoStoreDirectCartPopupint
enableDeficitIndicationint
enableFirstTradeBonusint
enableGeoIpLangCurrencyAuto(null)
enableGoogleCustomerReviewsint
enableInvestEntertinyint
enableInvestExittinyint
enableLocalCurrencyint
enableLockedStoreBalanceRevenueWithdrawtinyint
enableLoginWithCodeint
enableLootbearint
enableRealtimeCartCheckertinyint
enableRussiaPaymentint
enableSkinSettinyint
enableSteamItemtinyint
enableStoreBalanceRevenueWithdrawtinyint
enableTradeLockedtinyint
firstTradeBonusThresholdstring
firstTradeMaxBonusint
guessGameRoundTimeSecondint
haloSkinMarkupStorePercentint
isDisableWithdrawLimitedint
isEmailVerificationEnabledint
isInstantSellDisabledtinyint
isSaleDisabledtinyint
isSellBalanceDisabledtinyint
maxDiscountPercentint
KeyType
maxItemCountInCollectionint
maxMultipleOfPriceint
maxTagCountInCollectionint
maxTopupPercentint
maxTopupStoreBalanceDailyint
minimumCryptoTopupAmountstring
minInstantSellPriceint
minSalePriceint
monnectProcessingFeePercentint
nowPaymentsProcessingFeePercentint
numberOfChickenClickint
pricingIncludeStoreItemStatsint
promotionImagestring
promotionTextstring
qaAllowedUserIds(null)
recommendedSalePercentint
saleFeePercentint
shouldShowGleamGiveAwayBannerint
showTwitchint
siteMessagestring
skinIqLinkDisplayint
skinIqTicketCostPerRoundint
steamCInvCacheTTLMinutesint
storeDailyPurchaseLimitint
storeDiscountPercentint
storePriceMarkupstring
stripeFeePercentstring
stripeFixedFeeUSDstring
topupCardBonusPercentint
topupCryptoBonusPercentint
topupKinguinGiftcardBonusPercentint
tradeLockBaseFeetinyint
tradeLockChangeOverTimetinyint
withdrawCryptoMintinyint
withdrawCryptoProcessingFeePercenttinyint
withdrawLimitedPercentint
withdrawStripeMintinyint (REDACTED — Stripe min)
withdrawStripeProcessingFeePercenttinyint (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.

ITEMS (1)

KeyTypeDescription
minDopplerItemsStuckPricesintThreshold for the doppler-stuck-prices alert

LIMITS (3) — hard hourly outflow caps

KeyTypeDescription
hourlyLimitHardOutValueintHard ceiling on hourly outflow value (cents)
hourlyLimitSoftOutValueintSoft warning threshold on hourly outflow
largeTradeValueintThreshold above which a trade is "large" (cents)

PRICING (15)

KeyTypeDescription
adjustByLiveToStablePriceRatiotinyintToggle live-to-stable price adjustment
avg7DStableBotPriceModifierstringModifier for 7-day stable bot price
blockDepositByAvg7DStablePricetinyintBlock deposits when 7d stable price guard trips
blockDepositByPrevMonthPricetinyintBlock deposits when prev-month price guard trips
cardIdVerificationAmountintCard-payment ID verification trigger amount
decreaseDepositPriceForCheapItemstinyintToggle cheap-item deposit haircut
liveToStableModifierstringLive-to-stable conversion modifier
pricingMaxBotPlayerPriceintMax bot-player price ceiling
pricingMaxBotPriceintMax bot price ceiling
pricingMaxWantedMaxStockintMax stock cap for the wanted-list pricing path
pricingTableVersionstringActive pricing table version (e.g., items_market_v5)
settledMaxStockEnabledtinyintToggle the settled-max-stock pricing path
tradePriceMarkupstringMarkup applied to trade prices
wantedMaxStockLowCapacityRatiostringLow-capacity ratio for wanted-list cap
weeklyInOutRatioWeightstringWeight applied to weekly in/out ratio in pricing model

SALE (3)

KeyTypeDescription
instantSellPriceAdjustPercentstringInstant-sell price adjustment percent
limitSalePriceUpdateMinutesintCooldown between user sale-price edits
minDivisorOfPriceintMinimum sale-price divisor relative to base

STRIPE (2)

KeyTypeDescription
enableLevel0ChecktinyintToggle Stripe level-0 verification
level0CheckLimitintAmount above which level-0 check triggers (cents)

SYSTEM (98) — backend runtime toggles, kill switches, fleet sizing

Two-column listing for compactness.

KeyType
affiliateWithdrawDailyint
allowGeneratePromptLocalesint
botDangerousPercentagestring
botDisabledstring
botsGoodSizeLimitint
botsVeryGoodSizeLimitint
botWarningPercentagestring
cashListingsFeeint
couponBalanceMaxint
couponBalanceMaxDailyint
couponBalanceMaxPerUserint
couponStoreBalanceMaxDailyint
couponStoreBalanceMaxPerUserint
enable24HrTradeSurgeCheckint
enable6HrTradeSurgeCheckint
enableAIContentGenerationint
enableCSGOCasesStockChecktinyint
enableCSGOStockChecktinyint
enableCsMoneyPriceChecktinyint
enableDopplerPriceCrashint
enableListingHackint
enablePricingSyncCheckerint
enableTradeUrlInventorytinyint
haloSkinBufferPriceMaxint
haloSkinBufferPricePercentint
haloSkinEnableBestPriceItemint
haloSkinMarkupTradePercentint
haloSkinMaxPerBestPriceItemint
haloSkinMaxPerItemint
haloSkinMaxPriceint
haloSkinMinPriceint
haloSkinStoreDailyLimitint
haloSkinStoreEnabledint
haloSkinTradeDailyLimitint
haloSkinTradeEnabledint
haloStablePriceMultiplierCheckint
hideBotsItemsFromInventorystring
hourlyLimitHardOutValueOtherGamesint
inventoryDangerousPercentageint
inventoryWarningPercentageint
investWithdrawLimitint
largeTradesDisabledtinyint
listingHackAdminstring
listingHackDiscountPercentint
listingHackFromPriceint
listingHackFromStockint
listingHackMinPricePercentint
loginByCodeDailyLimitPerSteamIdint
lootbearDailyLimitint
KeyType
lootbearGlobalDailyLimitint
maxAddBalanceFromAdminPanelint
maxAddBalanceFromAdminPanelBySupporterint
maxAttemptLoginByCodeint
maxStoreTopupPerUserLast24Hoursint
maxTopupBalanceDailyint
maxTopupCryptoDailyGlobalStoreint
maxTopupCryptoDailyGlobalTradeint
maxTradeTopupPerUserLast24Hoursint
minLevelLoginByCodeint
minMinuteBetweenTradesint
monnectMaxTopupStoreDailyint
monnectMaxTopupTradeDailyint
newBotStartIndexint (live: 540)
numBotsint (live: 541 — total bot fleet size)
oauth2SellOrBuyBigint
oauth2TradeBigint
oauth2TradeLowMediumint
oauth2TradeMediumint
oauth2TradeSmallint
revertTradeMissingMaxint
saleOfferMaxTotalListingint
saleWithdrawAllTimeLimit1int
saleWithdrawAllTimeLimit2int
saleWithdrawAllTimeLimit3int
saleWithdrawLimitint
saleWithdrawLimit1int
saleWithdrawLimit2int
saleWithdrawLimit3int
saleWithdrawStoreDailyLimitint
saleWithdrawStoreLimit1int
saleWithdrawStoreLimit2int
saleWithdrawStoreLimit3int
sentTradesValueLimit1int
sentTradesValueLimit2int
sentTradesValueLimit3int
siteDisabledtinyint (live: 0 — kill switch)
skipStockCheckFromPriceint
storeCryptoFeePercentint
storeSteamLevelsLimitedstring
trade24hSurgeRatioint
trade6hSurgeRatioint
tradesDisabledtinyint (live: 0 — auto-flipped by anomaly)
userInvestWithdrawLimitint
userPriceDeductPercentForRustAndTf2int
uuskinStoreDailyLimitint
wikiAiContentTtlInDayint
withdrawLockedStoreBalanceSystemLimitint
withdrawLockedStoreBalanceUserLimitint

TRADEBOT (1)

KeyTypeDescription
unstoreLimitintTradebot unstore limit threshold

USER (9) — tier limits

KeyTypeDescription
allowedPendingPurchasesintMax concurrent pending purchases per user
balanceLimitLevel1intBalance ceiling for tier 1 (new/unverified)
balanceLimitLevel2intBalance ceiling for tier 2 (verified)
balanceLimitLevel3intBalance ceiling for tier 3 (trusted)
sentTradesValueLimitintDefault outbound trade value cap
tradeLimitLevel1intTier-1 daily trade frequency cap
tradeLimitLevel2intTier-2 daily trade frequency cap
tradeLimitLevel3intTier-3 daily trade frequency cap
updateLimitThresholdintTrade-update rate-gate threshold

Total registered keys: 12 + 1 + 77 + 1 + 3 + 15 + 3 + 2 + 98 + 1 + 9 = 222 (matches SELECT COUNT(*) FROM configurations).

§11 Failure modes

11.1 Stale cache after admin edit (3-min worst case)

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:

11.2 Self-flipping circuit breakers (auto-disable storms)

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:

  1. Investigate the anomaly source (logs / Datadog).
  2. Admin edits the flipped row back to 0.
  3. Wait up to 60s for L1 expiry across all 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.

11.3 Type cast errors

All value columns are VARCHAR(2000). Consumer code in tradeit-backend does manual casts:

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.

11.4 Missing-row = feature off

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.

§12 Observability

SourceWhereNotes
Datadog APMtradeit-backend.service.configurationService.* spansOne span per getValueByKey / updateValueByKey call
Datadog logsservice:tradeit-backend "step:updateValueByKey"Errors from the deferred MySQL write inside setTimeout (configurationService.js:102)
MySQL auditadmin_activities tableEvery admin CRUD on configurations recorded via MainService event listeners
Redis monitoringSCAN configuration:* (do NOT run KEYS in prod)Cache fill ratio observable from populated keys vs 222 total
Slack alertsAnomaly detection alerts via slackServicePosted 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.

§13 Code map

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

Schema / repository

LocationDescription
tradeit-backend/server/repository/configurationRepo.js:5getByCategory(category)SELECT * WHERE category = :category
tradeit-backend/server/repository/configurationRepo.js:11getByKey(key) — destructures [[config]]
tradeit-backend/server/repository/configurationRepo.js:17getByKeys(keys) — uses .query() not .execute() (IN list quirk)
tradeit-backend/server/repository/configurationRepo.js:24updateByKey(key, value) — direct UPDATE
tradeit-admin-backend/repositories/configurationRepo.ts:8list()prisma.configurations.findMany()
tradeit-admin-backend/repositories/configurationRepo.ts:12getByKey(key)findUnique
tradeit-admin-backend/repositories/configurationRepo.ts:20getById(id)
tradeit-admin-backend/repositories/configurationRepo.ts:28getByCategory(category)
tradeit-admin-backend/repositories/configurationRepo.ts:36getByKeyAndCategory — pre-insert app-layer uniqueness
tradeit-admin-backend/repositories/configurationRepo.ts:45create({...})
tradeit-admin-backend/repositories/configurationRepo.ts:58update({id, category, key, value})
tradeit-admin-backend/repositories/configurationRepo.ts:73delete(id)

Service layer

LocationDescription
tradeit-backend/server/service/configurationService.js:10cacheConfiguration = {} — L1 in-process Map
tradeit-backend/server/service/configurationService.js:12getGlobalConfigs(useHashKey=false) — shorthand for GLOBAL
tradeit-backend/server/service/configurationService.js:16getConfigsByCategory(category, useHashKey) — Redis 180s
tradeit-backend/server/service/configurationService.js:38getValueByKey(key, useCache=true) — L1 → L2 → MySQL
tradeit-backend/server/service/configurationService.js:71getValuesByKeys(keys) — bulk fetch via repo .query(), no cache
tradeit-backend/server/service/configurationService.js:94updateValueByKey — clears L1, deferred MySQL write, then Redis DEL
tradeit-admin-backend/services/configurationService.ts:7class ConfigurationService extends MainService
tradeit-admin-backend/services/configurationService.ts:50emit('updated', ...) — old + new payload for diff
tradeit-admin-backend/services/configurationService.ts:66getValueByKey — Redis-only, 180s, no L1, no DEL on update

Routes / controllers

LocationDescription
tradeit-backend/server/routes/configuration.js:6-7GET / and GET /stats
tradeit-backend/server/controllers/configuration.js:11getConfiguration — composes bootstrap response
tradeit-admin-backend/routes/configurations.ts:11-14All four CRUD routes guarded by [isLoggedIn, isAdmin]
tradeit-admin-backend/controllers/configuration.ts:71update — type validation switch before persist

Admin UI

LocationDescription
tradeit-admin/src/views/Configuration.vueInline-edit table view
tradeit-admin/src/store/configuration.ts:33Vuex fetch action
tradeit-admin/src/store/configuration.ts:54createConfiguration — note: no updateConfiguration action; updates go straight from view to network
tradeit-admin/src/networks/configurationNetwork.ts:13updatePATCH /configurations/:id

Consumer modules (selected)

Full list (40+ citations) in the wiki source. Highlights below.

LocationMethodKeys read / written
server/index.js:118getValueByKeysiteDisabled — gates all public endpoints on startup
server/controllers/trade.js:301-304getValueByKey x3largeTradeValue, tradesDisabled, largeTradesDisabled
server/service/botsService.js:33-35getValueByKey x2numBots (live: 541), newBotStartIndex (540)
server/service/tradeOutSummaryService.js:70updateValueByKeywrites tradesDisabled = 1
server/service/userLimitationService.js:357updateValueByKeywrites tradesDisabled = 1
server/service/userLimitationService.js:377updateValueByKeywrites disableOtherGames = 1
server/service/userLimitationService.js:398updateValueByKeywrites largeTradesDisabled = 1
server/middlewares/nowpayment.js:31getValueByKey(..., useCache=false)minimumCryptoTopupAmount — bypasses both cache layers
server/redis/tradeRedis.js:21getValueByKeynumBots — used to build per-bot Redis key list

Canonical key enum

LocationDescription
tradeit-backend/server/config/enums.js:727export const configurationKeys = { ... } — named-key constant map. Always reference this enum; never hardcode strings.

§14 Open questions

The Task 5 spelunk left 7 open questions; Task 6 resolved 6 of them. The remaining one is tracked.

#QuestionResolution
1Category 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.
2Are 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.
3Current value of numBots?Resolved. Live: 541 (newBotStartIndex: 540, botsGoodSizeLimit: 900, botsVeryGoodSizeLimit: 850).
4Does TRADEBOT category exist?Resolved. Yes — 1 row: unstoreLimit (int).
5What 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).
6tradeit-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.
7getByKeys 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