Data Ownership
Status
Active
Date
2026-04-28
Owners
- Platform Backend
Last Verified Commit
def491e5
Ownership Model
servers_v2 currently shares one PostgreSQL runtime, but logical ownership is
still service-specific.
The practical rule is:
- a service may read shared state when required for orchestration or projection
- only the owner service may define the write semantics of its aggregate data
- balance mutation remains exclusive to
wallet_service
Owned Table Families And Aggregates
| Owner | Primary Data Responsibility | Notes |
|---|---|---|
player_service | player auth/profile state, registration flow, player-owned outbox rows, message/common projections | serves gateway-facing player queries |
wallet_service | wallet idempotency, wallet outbox, balance mutation, approved deposit/withdraw side effects, balance logs | single money writer |
rolling_service | rolling records, rolling inbox/outbox state, completion/cancel retry state | consumes wallet events |
promotion_service | coupon usage, promotion configs, settlement projections, promotion_coupon_saga | consumes wallet settlement events |
game_service | provider callback transaction state and provider-specific integration records | callback ownership stays local |
admin_service | operational query models, top-info aggregation support, admin-facing config and control surfaces | orchestrates but should not steal ownership |
recon_service | shooter_* table family: pushbullet, SMS, phone whitelist, recharge templates, device compatibility | extracted from middle_server |
gateway | no business table ownership | edge only |
Important Ownership Boundaries
wallet_serviceowns deposit approval and withdraw approval side effects even when initiated throughadmin_serviceorrecon_service.recon_servicemay persist matchedorder_id, review state, orsms_id, but not final balance mutation.admin_serviceowns back-office compatibility and operator workflows, not domain balance logic.player_serviceowns player registration semantics even thoughgatewayexposes the public route surface.
Multi-Brand Scoping
Per ADR-009, every row in the table families below is scoped by brand_id:
player_service: player auth/profile, registration helpers,agent_setting,agent_domain, player-owned outbox rows, message/common projectionswallet_service:wallet_account,wallet_bucket,wallet_bucket_type,wallet_coupon_grant,wallet_bet_authorization,wallet_ledger,wallet_transfer,wallet_idempotency, wallet outbox,wallet_inbox,wallet_dead_letter;wallet_topologyandwallet_policydocuments are brand-scopedrolling_service: rolling records, rolling inbox/outbox, completion/cancel retry statepromotion_service: coupon usage, promotion configs, settlement projections,promotion_coupon_sagagame_service: provider callback transaction state and provider-specific records (resolved from outbound account namespace{brand_code}_{account})admin_service: per-brand operational config and per-brand control surfacesrecon_service:shooter_sms(brand_id populated from matched deposit) and player/deposit-referencing review-state rows. Operator-infrastructure tables (shooter_device,shooter_pushbullet,shooter_phone,shooter_template_recharge) stay brand-global
Brand-global aggregates (no brand_id):
brandandbrand_config(owned byadmin_service)agentaggregate (owned byagent_service)agent_brandjoin (write-owned byadmin_service; read byagent_serviceonly). No other service consults the allow list in this iteration; if a future agent-initiated wallet/promotion command needs to honor the allow list, it must readagent_brandthroughagent_servicerather than directly. Migration-window note: between Phase 2 (0023b) and Phase 12 (0028), a PostgresBEFORE INSERTtrigger onagentautomatically inserts(agent_id, default_brand_id, 'enabled')intoagent_brandso new agents created during the window are not orphaned. The trigger is removed onceadmin_servicewritesagent_brandat agent creation in Phase 12.- game provider credentials and integration secrets (owned by
game_service) - recon operator infrastructure:
shooter_device,shooter_pushbullet,shooter_phone,shooter_template_recharge(owned byrecon_service) - infrastructure-level Redis keys, idempotency tokens, and shared rate limits
Cross-brand reads are forbidden in domain code paths. Admin-side aggregations across brands must opt in explicitly and must not produce money writes.
Brand-Scoped Table Inventory
This is the canonical, table-by-table inventory of every physical
brand-relevant table (whether ORM-declared in
servers_v2/shared/rgb_db/src/rgb_db/models/ or defined only in raw SQL
inside an alembic migration) and its multi-brand disposition. The
brand-isolation migration chain is:
0022_create_brand_aggregates.py--brand,brand_config,agent_brandmachinery.0023_seed_default_brand.py-- singledefaultbrand row +agent_brand(agent_id, default_brand_id, 'enabled')for every pre-migrationagent.0023b_install_agent_brand_autoseed_trigger.py-- BEFORE INSERT trigger onagentso any new agent created during the Phase 2-12 window picks up anagent_brandrow automatically.0024a_add_brand_id_nullable.py-- addsbrand_id(nullable) to every brand-scoped table.0024b_backfill_brand_id.py-- writes thedefaultbrand id into every existing row.0024c_set_brand_id_not_null.py-- flips the column toNOT NULL(every brand-scoped query post this point can rely on the column being present).0026_add_brand_id_fk.py-- addsFOREIGN KEY (brand_id) REFERENCES brand(brand_id)to every brand-scoped table.0027_brand_scoped_uniqueness.py-- replaces every legacy single- column unique index with a brand-scoped equivalent (see "Single- Column PKs Requiring Rewrite In0027" below).0028_remove_agent_brand_autoseed_trigger.py-- drops the0023btrigger now thatadmin_servicewritesagent_brandexplicitly at agent creation in Phase 12.0029_admin_audit_table.py-- createsadmin_auditfor the P1-3 / P1-4 operator-id + money-write audit guarantees.0030_game_callback_dead_letter.py-- createsgame_callback_dead_letterfor brand-unresolved game-provider callback persistence (DLQ replayed viatools/multi_brand_backfill/replay_game_callback_dlq.py).0031_brand_scope_outbox_idempotency.py-- brand-scopes the cross-service outbox + idempotency uniqueness keys so the same request id / dedup key may legitimately appear under two brands.0032_admin_audit_append_only.py-- DB-level append-only triggers onadmin_audit(see ADR-009 audit guarantees and the rollout runbook "Audit row tamper-evidence" entry).0033_recovery_audit_table.py-- createsrecovery_auditfor the brand-aware recovery flow's append-only audit trail.0034_fix_agent_brand_autoseed_conflict_target.py-- corrects the0023btrigger's ON CONFLICT target so concurrent agent inserts no longer race onagent_brandinsertion.0035_brand_scope_player_stat_day.py-- brand-scopesplayer_stat_dayandprovider_stat_dayuniqueness so per-brand daily aggregations no longer collide on a shared(player_id, date)key.0036_extend_append_only_triggers.py-- extends the 0032 append-only trigger pattern towallet_ledger(per-bucket money movement) andplayer_balance_log(legacy compatibility ledger), so an attacker with DB write access cannot rewrite financial history to cover fraud or hide adjustments. UPDATE/DELETE on a finalised row + TRUNCATE of either table now raise loud plpgsql exceptions.0037_relax_shooter_sms_brand_id.py--shooter_sms.brand_idmust be nullable because inbound recon SMS arrives via Pushbullet/ Telegram before any brand correlation is possible. Brand_id is populated at match-time from the matchedplayer_deposit.brand_id, withassert_sms_brand_after_match(recon_store.py) raising loudly if the match-time UPDATE fails to set it. The audit baseline (docs/runbooks/multi-brand/2026-04-28-brand-id-insert-audit.md) documents theBRAND_NULLABLE_BY_DESIGNexemption forshooter_sms; new producers writing into shooter_sms must rely on the recon match-time runtime guard rather than a local classifier script.
The tools/multi_brand_backfill/ verification script iterates the
inventory below and the migration chain above.
Brand-scoped (gain brand_id NOT NULL + FK to brand)
Player-owned aggregates:
player,player_login,player_register,player_register_tag,player_referral,player_message,player_message_read,player_note,player_notice,player_tag,player_tag_mapping,player_groupplayer_deposit,player_withdraw,player_usdt,player_withdraw_usdt,player_adjust,player_balance_logplayer_wallet_limit(PK rewrite -- see below)player_level,player_level_domainplayer_rolling,player_rolling_logplayer_cashback_log,player_lossback_log,player_lossback_sports_log,player_rebate_log,player_rebate_details,player_rebate_sports,player_rebate_sports_log,player_provider_stat_day,player_stat_acc,player_stat_day
Agent satellites (agent itself is brand-global; per-brand satellites
carry brand_id):
agent_setting(PK rewrite -- see below),agent_domain,agent_announcement,agent_balance_log,agent_global_mail,agent_login,agent_messages,agent_notice,agent_provider_percentage,agent_provider_stat_day,agent_stat_acc,agent_stat_day,agent_withdraw,sub_agent_provider_stat_day
Promotion / event:
event,register_event,attendance_event,attendance_event_details,banner,message_templatecashback_config,lossback_config,rebate_config,rebate_stat_day,promotion,promotion_stat_day,coupon,coupon_condition,coupon_log,coupon_recycle_log
Banking / payment:
bank,bank_card,bank_card_group,bank_owner_color,bank_stat_day
Risk / safety:
ban,ip_blacklist
Crypto / USDT (per-player rows):
coin_transaction,usdt_stat_day
Provider transaction state (per-account namespaced by brand):
bti_balance_change,bti_commit_reserve,bti_credit,bti_debit_reserve,bti_reserveho_transactionmg_account(PK rewrite -- see below),mg_transactionwc_account(PK rewrite -- see below)digitain_credit_batch,digitain_debit,digitain_rollback,digitain_token(PK rewrite -- see below),digitain_transaction
Provider stats (per-brand GGR / payout):
provider_stat_day,provider_ggr_stat
Bet domain:
bet,sports_bet,sports_bet_line,sports_branch_stat_day,sports_event_stat_day,sports_market_stat_day,sports_parlay_stat_day
I18n overrides:
i18n(PK rewrite -- see below)
Daebak password legacy (per-player):
daebak_email(PK rewrite -- see below),daebak_pwd,daebak_agent_pwd
Operations / async:
excel_download,online_stat,sms_log
Wallet aggregates (under wallet schema):
wallet_account,wallet_bucket,wallet_bucket_type,wallet_coupon_grant,wallet_bet_authorization,wallet_ledger,wallet_transfer,wallet_idempotency, wallet outbox,wallet_inbox,wallet_dead_letter,wallet_topology,wallet_policy
Recon (the brand-scoped subset only):
shooter_sms, plus any player- or deposit-referencing recon review-state row added in future migrations
Cross-service outbox / inbox / saga tables (carry brand_id so
consumers can scope projections per brand):
player_outbox(owned byplayer_service)rolling_outbox,rolling_inbox(owned byrolling_service)agent_outbox(owned byagent_service)promotion_inbox,promotion_coupon_saga(owned bypromotion_service)- wallet outbox,
wallet_inbox,wallet_dead_letter(already listed under wallet aggregates above)
Brand-global (no brand_id)
brand,brand_config,agent_brand(the brand machinery itself)agent(perADR-009; brand membership lives inagent_brand)provider,provider_ggr_config,game(provider/game catalogs are shared across brands perADR-009's "credentials are global" posture; per-brand variation lives inprovider_stat_dayrollups)global_var(kept global for values that genuinely never differ;brand_configoverrides for values that may)withdraw_usdt_link(USDT chain configuration is infrastructure, not per-brand)vera_ip,vera_sync(infrastructure / verification)admin_log,admin_stat_acc,admin_stat_day(admin operates across all brands; admin entry surface is brand-global)- Recon operator infrastructure (per
ADR-009):shooter_device,shooter_pushbullet,shooter_phone,shooter_template_recharge
Brand-global tables (intentional) — classification audit
Three tables surfaced during the CR-C-IM-3 audit as ambiguous (not
listed in 0024a's RGB_SCHEMA_TABLES, ORM models present, no
brand_id column). The decision for each is recorded here so future
contributors do not re-litigate the call. The matching ORM models in
servers_v2/shared/rgb_db/src/rgb_db/models/ carry a header comment
pointing at this section.
| Table | Decision | Rationale |
|---|---|---|
bank_card_group_staff | Brand-global (legacy / inactive) | Per ADR-009 the staff identity surface was deleted at Phase 12. The ORM remains for migration completeness only — no code in servers_v2/ reads or writes the table. Adding brand_id would require schema work that supports a feature that does not exist. If a future iteration revives the staff-assignment workflow, that work MUST also brand-scope the table at the same time. |
withdraw_usdt_link | Brand-global (infrastructure) | The table holds the USDT chain endpoints (TRC-20 vs ERC-20 selection, gas defaults, Plisio link metadata) that are properties of the chain itself, not of a brand. Two brands sending USDT use the same chain endpoints; per-brand variation lives in brand_config for things like minimum-withdraw thresholds. INSERT/UPDATE happens at the database layer, not via the application — see the table comment. |
provider_ggr_config | Brand-global (provider catalog) | Per the same ADR-009 policy that keeps provider and game global ("credentials and catalogs are global; per-brand variation lives in stats rollups"), the GGR invoice cadence config is a property of the provider's billing arrangement, not of any brand. Per-brand GGR numbers live in provider_ggr_stat (which IS brand-scoped). |
If a future iteration determines any of these need brand scope, the
process is: open a tracking issue, add the table to
RGB_SCHEMA_TABLES in 0024a, write a follow-up migration
0039_add_brand_id_to_<table>.py, then add brand_id to the ORM and
remove the corresponding row from this table.
Deleted by ADR-009 (not migrated)
bank_card_group_staff(staff identity removed)- Anything else discovered to depend exclusively on the deleted staff routes (audited at Phase 12)
Single-Column PKs Requiring Rewrite In 0027
The following tables have a single-column PK that must be replaced with
a composite PK including brand_id (drop existing PK, add new
composite PK after 0024c sets brand_id NOT NULL):
| Table | Old PK | New PK |
|---|---|---|
agent_setting | agent_id | (agent_id, brand_id) |
player_wallet_limit | player_id | (brand_id, player_id) |
daebak_email | player_id | (brand_id, player_id) |
mg_account | account | (brand_id, account) |
wc_account | account | (brand_id, account) |
digitain_token | account | (brand_id, account) |
i18n | code | (brand_id, code) |
Every other brand-scoped table uses a surrogate guid BIGINT PK that
remains untouched; only uniqueness constraints change.
Legacy Residual Data Still Needing Explicit Ownership
The following legacy middle_server areas are still not fully mapped to a stable
owner in servers_v2:
- role and menu configuration
- legacy system config editing
- i18n management
- web rules and FAQ/content tables
- BI push data and notes
- coin passthrough or exchange configuration
Those areas remain part of the full-cutover gap.