|

Sportsbook architecture and workflows

Keystone schema by domain, the operator workflows from catalog to settlement, dual control, the customer wager path, named GraphQL operations, and access control.

Application surfaces

SurfaceLocationNotes
Storefrontapp/page.tsx, app/sports/**, app/events/**, app/betslip, app/promotions, app/account/**, features/storefrontBoard, bet slip, account pages; data from the customer API
Customer APIapp/api/sandbox/sportsbook/**, features/sportsbook/customerREST routes; open only in sandbox mode
Operator dashboardapp/dashboard/(admin)/platform/**, features/platformSportsbook workspaces inside the shared shell
Generic model pagesapp/dashboard/(admin)/[listKey]/**, features/dashboardKeystone list and item screens, mostly read-only
GraphQL/api/graphql (pages/api/graphql.ts)Keystone reads plus custom operations; query size, depth, and alias limits; 256 KB body cap
Workerfeatures/sportsbook/workers/main.tsOutbox, inbox, scheduled jobs, audit anchoring, heartbeats
Other HTTP/api/sandbox/webhooks/[kind], /api/mcp-transport/[transport], /api/completion, /api/health, /api/ready, /api/metricsSandbox provider webhooks, MCP tools, dashboard AI chat, probes

Schema by domain

The repository's docs/schema-map.md lists every list with its states and writers. In summary:

  • Operator: Tenant, Brand, BrandLocale, LegalEntity, Jurisdiction, OperatorJurisdiction, JurisdictionPolicyVersion, OperatorChannel, CurrencyDefinition, TenantCurrency, OnboardingProgress.
  • Catalog: Region, Sport, Competition, Season, CompetitionStage, Venue, Competitor, Event (status SCHEDULED, LIVE, ENDED, CANCELLED, SUSPENDED), EventParticipant, EventScheduleChange, MarketDefinition.
  • Configuration: ConfigDefinition (merge strategies REPLACE, MIN, MAX, UNION, DENY_WINS), ConfigValue, FeatureFlag, HouseRuleVersion, TaxRuleVersion, RetentionPolicyVersion.
  • Trading and risk: Market (OPEN, SUSPENDED, CLOSED, GRADED; PREMATCH or LIVE), Selection, PriceSnapshot (append-only), TradingStatusChange, TraderOverride, FeedHealthSnapshot, LimitProfile, LimitRule, CorrelationGroup, CorrelationRule, ExposurePosition, WagerExposureAllocation.
  • Wagering: BetSlip, BetSlipItem, Quote, CustomerQuoteTerms, CustomerQuoteLegTerms, AcceptanceDecision, Wager (structures STRAIGHT, PARLAY, SYSTEM, ROUND_ROBIN, TEASER, EACH_WAY), WagerLeg, WagerPart, WagerStatusEvent, CashOutOffer, CashOutAcceptance.
  • Results and settlement: ResultVersion (PENDING, APPROVED, REJECTED, SUPERSEDED), MarketGrade, LegGrade, SettlementRun, Settlement (versioned), SettlementLine, ResettlementCase.
  • Finance: LedgerAccount, JournalTransaction, JournalEntry, BalanceProjection, Hold, PaymentIntent, PaymentAttempt, LedgerAdjustmentRequest, ReconciliationRun, ReconciliationItem, SafeguardingAccount, SafeguardingReconciliation, TaxAssessment.
  • Customers: Player, PlayerIdentity (encrypted), CustomerCredential, CustomerSession, CustomerDevice, CustomerRecoveryToken, CustomerCommand (idempotency), CustomerFavorite, Consent, PlayerStatement, AccountClosure, AffiliateAttribution, PamSubjectMapping, Notification.
  • Compliance and safer play: VerificationCase, VerificationCheck, GeoSession, GeoCheck, AMLCase, ScreeningHit, AffordabilityAssessment, IntegrityCase, CustomerPrivacyRequest, AccountRestriction, PlayerLimit, SelfExclusion.
  • Support and promotions: SupportCase, SupportCaseEvent, Dispute, DisputeEvent, Promotion, PromotionRuleVersion, CampaignBudget, PromotionRedemption, BonusAward, BonusTransaction.
  • Integrations and operations: ProviderConnection, ProviderHealthSample, ProviderEntityMapping, AdapterCertificationRun, TradingReplayRun, RawEnvelope, InboxMessage, OutboxMessage, WebhookEndpoint, WebhookDelivery, IntegrationDeliveryAttempt, WebhookReplay, DeadLetter, ScheduledJob, WorkerHeartbeat, Incident, ApiRateBucket.
  • Governance and access: AuditEvent (hash-chained), AuditAnchor, ApprovalRequest, DataCorrectionCase, LegalHold, AccessReview, ComplianceReport, ReportArtifact, DataExport, RetentionExecution, User, Role, RoleGrant, Team.

Money is stored as integer minor units in text fields (for example stakeMinor, amountMinor) with a currency code. Every money movement posts a balanced JournalTransaction.

Dual control

Many changes are split into a request and an apply step. The request writes an ApprovalRequest with the reviewed payload and a payload hash; it expires after 24 hours. A different operator with canApproveSensitiveActions decides it in the Approval inbox with decideSensitiveApproval. The requester cannot approve their own request (DUAL_CONTROL_REQUIRED). The apply mutation then checks the approval, the hash, and expiry before it changes anything. Request and apply pairs include settings, roles, role grants and revocations, operator admission, legal entities, jurisdiction activation, brands, currencies, brand locales, house rules, feature flags, promotions, webhook endpoints, trader overrides, correlation rules, tax rules, ledger adjustments, resettlements, cash-out corrections, dispute resolutions, retention executions, and data exports.

Workflow: set up the operator

  1. Onboarding creates the tenant, brand, locale, legal entity, jurisdiction, channel, currency, config, providers, and a demo catalog (see Getting started).
  2. Configuration and jurisdictions: request and apply legal entities, jurisdiction policy drafts and activation, brands, currencies, locales, house rules, and feature flags.
  3. Settings: request and apply typed setting changes (requestSettingChange, applyApprovedSetting) and edit store branding.
  4. Roles: create the independent approver, request role definitions, grants, revocations, and new operator admissions.

Workflow: catalog and trading

  1. Catalog reference: create sports, competitions, seasons, stages, venues, and competitors, and activate or deactivate them (createCatalogSport, createCatalogCompetition, createCompetitionSeason, createCatalogStage, createCatalogVenue, createCatalogCompetitor, setCatalogReferenceStatus).
  2. Create an event with its first market (createCatalogEvent), add participants (attachEventParticipant), reschedule (rescheduleEvent, which writes EventScheduleChange), and add markets (createEventMarket).
  3. New markets start suspended until every selection is priced. Set prices with setMarketPrices (or ingestSandboxPrice from the sandbox feed), then open with setMarketStatus, which checks the reviewed market version.
  4. Move events through live, ended, and other states with setEventTradingState. Suspend markets with suspendMarket. Each change writes TradingStatusChange.
  5. Request and apply trader overrides and correlation rules; create stake, payout, and liability limits with createRiskLimit.

Workflow: customer wager (sandbox)

  1. The customer registers, signs in, and completes verification. Verification calls the sandbox KYC and geo adapters and writes PlayerIdentity, VerificationCase, VerificationCheck, GeoSession, and GeoCheck.
  2. The customer funds the wallet through the sandbox payments adapter (PaymentIntent, PaymentAttempt, Hold, journal postings).
  3. The bet slip requests a quote (/quote or /quote-combination). The quote and its immutable terms are stored with an expiry.
  4. Placement (/wager or /wager-combination) runs the acceptance gates: tenant and player active, KYC approved, location allowed, not self-excluded or restricted, within player limits, market open, price not stale (MAX_PRICE_AGE_MS), funds available, liability within the cap. The result is stored as an AcceptanceDecision with a rejection code such as KYC_REQUIRED, GEO_REQUIRED, STALE_PRICE, or LIMIT_EXCEEDED.
  5. Accepted wagers reserve funds, post journals, and update exposure. Customers can request and accept cash-out offers from /account/bets.

Workflow: result, grade, and settle

  1. Enter a result for an event that has started (ingestSandboxResult) and send it for approval (requestResultApproval). A pending version can be withdrawn (withdrawResultVersion).
  2. The independent approver decides the request. approveResultVersion (needs canManageRisk) then marks the version approved, supersedes the previous approved version, and emits settlement.requested.
  3. Grade selections from the approved result under the current house rules (gradeMarketSelection). gradeMarket always refuses with SELECTION_RESULT_REQUIRED.
  4. Settle: settleWager for simple straight wagers, settleCombinationWager for wagers with parts (it also routes full straight wagers). fullStraightSettlementPlan previews a plan. Settlements are versioned with lines for stake return, profit, tax, and rounding.
  5. Correct with requestResettlementApproval and resettleWager, or requestCombinationResettlement and resettleCombinationWager. Cash-out corrections use requestCashOutCorrection and correctCashOut.

Workflow: finance and reconciliation

  • sandboxPayment posts an operator deposit or withdrawal for a player through the sandbox payments adapter. openChargeback opens a chargeback.
  • requestLedgerAdjustment and applyLedgerAdjustment post manual adjustments under dual control.
  • runReconciliation and runSafeguardingReconciliation compare the ledger with provider and safeguarded-funds figures.
  • requestTaxRule, applyTaxRule, and assessSettlementTax manage tax rules and assess tax on settled wagers.

Workflow: customers, compliance, and safer play

  • Verification: openVerificationCase, decideVerificationCase. AML: reviewAMLCase, recordComplianceScreening. Privacy: transitionPrivacyRequest.
  • Safer play: setResponsibleGamingLimit and selfExclude from the operator side; customers set limits and take breaks from their account.
  • Support: setSupportCaseStatus, and dispute resolution with requestDisputeResolution, withdrawDisputeProposal, and applyDisputeResolution.
  • Wagers: openWagerInvestigation opens an integrity case for a wager.

Workflow: operations and governance

  • Promotions: createPromotionCampaign, then requestPromotionActivation and applyPromotionActivation.
  • Providers: runAdapterCertification, runTradingReplay, setProviderOperationalState, mapProviderEntity.
  • Webhooks: requestWebhookEndpoint, applyWebhookEndpoint, testWebhookEndpoint.
  • Recovery: enqueueRecovery, replayQueueItem, scheduleOperationalJob. Incidents: openOperationalIncident, setIncidentStatus.
  • Reports: generateComplianceReport, requestDataExport, applyApprovedDataExport, runRetentionDryRun, requestRetentionExecution, applyRetentionExecution.
  • Audit: every custom mutation appends to the hash-chained AuditEvent log. The worker anchors the chain head when anchoring is configured.

Custom GraphQL operations

Queries: redirectToInit, operatorSessionAssured, fullStraightSettlementPlan.

Mutations are grouped below. All names come from features/keystone/mutations/sportsbook.ts and sportsbook-full.ts.

AreaMutations
Identity and accesscreateInitialUser, bootstrapOperatorAdmin, beginOperatorMfaEnrollment, completeOperatorMfaEnrollment, verifyOperatorMfaSession, sendUserPasswordResetLink, redeemUserPasswordResetToken, bootstrapIndependentApprover, requestRoleDefinition, applyApprovedRoleDefinition, requestRoleGrant, applyApprovedRoleGrant, requestRoleRevocation, applyApprovedRoleRevocation, requestOperatorAdmission, applyApprovedOperatorAdmission, requestSensitiveApproval, decideSensitiveApproval
Onboarding and configurationpreviewSportsbookOnboarding, applySportsbookOnboarding, dismissOperatorOnboarding, requestSettingChange, applyApprovedSetting, requestLegalEntity, applyLegalEntity, createJurisdictionPolicyDraft, requestJurisdictionActivation, applyJurisdictionActivation, requestOperatorBrand, applyOperatorBrand, requestTenantCurrency, applyTenantCurrency, requestBrandLocale, applyBrandLocale, requestHouseRule, applyHouseRule, requestFeatureFlag, applyFeatureFlag
Catalog and tradingcreateCatalogEvent, createCatalogSport, createCatalogCompetition, createCompetitionSeason, createCatalogStage, createCatalogVenue, createCatalogCompetitor, setCatalogReferenceStatus, attachEventParticipant, rescheduleEvent, createEventMarket, setEventTradingState, ingestSandboxPrice, setMarketPrices, setMarketStatus, suspendMarket, requestTraderOverride, applyTraderOverride, requestCorrelationRule, applyCorrelationRule, createRiskLimit
Wagers, results, settlementacceptStraightWager (customer boundary only), ingestSandboxResult, requestResultApproval, withdrawResultVersion, approveResultVersion, gradeMarket, gradeMarketSelection, settleWager, requestResettlementApproval, resettleWager, settleCombinationWager, requestCombinationResettlement, resettleCombinationWager, requestCashOutCorrection, correctCashOut, openWagerInvestigation
FinancesandboxPayment, openChargeback, runReconciliation, runSafeguardingReconciliation, requestTaxRule, applyTaxRule, assessSettlementTax, requestLedgerAdjustment, applyLedgerAdjustment
Customers and complianceopenVerificationCase, decideVerificationCase, reviewAMLCase, recordComplianceScreening, transitionPrivacyRequest, setResponsibleGamingLimit, selfExclude, setSupportCaseStatus, requestDisputeResolution, withdrawDisputeProposal, applyDisputeResolution
Promotions and providerscreatePromotionCampaign, requestPromotionActivation, applyPromotionActivation, mapProviderEntity, runAdapterCertification, runTradingReplay, setProviderOperationalState, requestWebhookEndpoint, applyWebhookEndpoint, testWebhookEndpoint
Operations and reportsenqueueRecovery, replayQueueItem, scheduleOperationalJob, openOperationalIncident, setIncidentStatus, generateComplianceReport, runRetentionDryRun, requestRetentionExecution, applyRetentionExecution, requestDataExport, applyApprovedDataExport

Access control

Dashboard visibility is not the control. Every list and custom operation checks the operator's role, tenant, and two-factor assurance on the server.

  • Most Sportsbook lists allow only query through GraphQL, gated by one role flag (for example canTrade for catalog and trading lists, canManageCompliance for players and limits) and filtered to the operator's tenant. Create, update, and delete are denied; changes go through custom mutations.
  • Customer identity lists (CustomerSession, CustomerCredential, and similar) are omitted from GraphQL entirely.
  • Role is readable by signed-in users in their tenant and cannot be changed through GraphQL CRUD; roles change through the request and apply mutations.
  • Custom mutations call requireTenant with a capability, require MFA assurance, and re-read the acting user inside the transaction (assertCurrentActor), so a revoked role or removed factor takes effect immediately.
  • In production, GraphQL error messages are masked except for upper-case refusal codes such as DUAL_CONTROL_REQUIRED.

On this page