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
| Surface | Location | Notes |
|---|---|---|
| Storefront | app/page.tsx, app/sports/**, app/events/**, app/betslip, app/promotions, app/account/**, features/storefront | Board, bet slip, account pages; data from the customer API |
| Customer API | app/api/sandbox/sportsbook/**, features/sportsbook/customer | REST routes; open only in sandbox mode |
| Operator dashboard | app/dashboard/(admin)/platform/**, features/platform | Sportsbook workspaces inside the shared shell |
| Generic model pages | app/dashboard/(admin)/[listKey]/**, features/dashboard | Keystone 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 |
| Worker | features/sportsbook/workers/main.ts | Outbox, inbox, scheduled jobs, audit anchoring, heartbeats |
| Other HTTP | /api/sandbox/webhooks/[kind], /api/mcp-transport/[transport], /api/completion, /api/health, /api/ready, /api/metrics | Sandbox 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(statusSCHEDULED,LIVE,ENDED,CANCELLED,SUSPENDED),EventParticipant,EventScheduleChange,MarketDefinition. - Configuration:
ConfigDefinition(merge strategiesREPLACE,MIN,MAX,UNION,DENY_WINS),ConfigValue,FeatureFlag,HouseRuleVersion,TaxRuleVersion,RetentionPolicyVersion. - Trading and risk:
Market(OPEN,SUSPENDED,CLOSED,GRADED;PREMATCHorLIVE),Selection,PriceSnapshot(append-only),TradingStatusChange,TraderOverride,FeedHealthSnapshot,LimitProfile,LimitRule,CorrelationGroup,CorrelationRule,ExposurePosition,WagerExposureAllocation. - Wagering:
BetSlip,BetSlipItem,Quote,CustomerQuoteTerms,CustomerQuoteLegTerms,AcceptanceDecision,Wager(structuresSTRAIGHT,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
- Onboarding creates the tenant, brand, locale, legal entity, jurisdiction, channel, currency, config, providers, and a demo catalog (see Getting started).
- Configuration and jurisdictions: request and apply legal entities, jurisdiction policy drafts and activation, brands, currencies, locales, house rules, and feature flags.
- Settings: request and apply typed setting changes (
requestSettingChange,applyApprovedSetting) and edit store branding. - Roles: create the independent approver, request role definitions, grants, revocations, and new operator admissions.
Workflow: catalog and trading
- Catalog reference: create sports, competitions, seasons, stages, venues, and competitors, and activate or deactivate them (
createCatalogSport,createCatalogCompetition,createCompetitionSeason,createCatalogStage,createCatalogVenue,createCatalogCompetitor,setCatalogReferenceStatus). - Create an event with its first market (
createCatalogEvent), add participants (attachEventParticipant), reschedule (rescheduleEvent, which writesEventScheduleChange), and add markets (createEventMarket). - New markets start suspended until every selection is priced. Set prices with
setMarketPrices(oringestSandboxPricefrom the sandbox feed), then open withsetMarketStatus, which checks the reviewed market version. - Move events through live, ended, and other states with
setEventTradingState. Suspend markets withsuspendMarket. Each change writesTradingStatusChange. - Request and apply trader overrides and correlation rules; create stake, payout, and liability limits with
createRiskLimit.
Workflow: customer wager (sandbox)
- The customer registers, signs in, and completes verification. Verification calls the sandbox KYC and geo adapters and writes
PlayerIdentity,VerificationCase,VerificationCheck,GeoSession, andGeoCheck. - The customer funds the wallet through the sandbox payments adapter (
PaymentIntent,PaymentAttempt,Hold, journal postings). - The bet slip requests a quote (
/quoteor/quote-combination). The quote and its immutable terms are stored with an expiry. - Placement (
/wageror/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 anAcceptanceDecisionwith a rejection code such asKYC_REQUIRED,GEO_REQUIRED,STALE_PRICE, orLIMIT_EXCEEDED. - 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
- Enter a result for an event that has started (
ingestSandboxResult) and send it for approval (requestResultApproval). A pending version can be withdrawn (withdrawResultVersion). - The independent approver decides the request.
approveResultVersion(needscanManageRisk) then marks the version approved, supersedes the previous approved version, and emitssettlement.requested. - Grade selections from the approved result under the current house rules (
gradeMarketSelection).gradeMarketalways refuses withSELECTION_RESULT_REQUIRED. - Settle:
settleWagerfor simple straight wagers,settleCombinationWagerfor wagers with parts (it also routes full straight wagers).fullStraightSettlementPlanpreviews a plan. Settlements are versioned with lines for stake return, profit, tax, and rounding. - Correct with
requestResettlementApprovalandresettleWager, orrequestCombinationResettlementandresettleCombinationWager. Cash-out corrections userequestCashOutCorrectionandcorrectCashOut.
Workflow: finance and reconciliation
sandboxPaymentposts an operator deposit or withdrawal for a player through the sandbox payments adapter.openChargebackopens a chargeback.requestLedgerAdjustmentandapplyLedgerAdjustmentpost manual adjustments under dual control.runReconciliationandrunSafeguardingReconciliationcompare the ledger with provider and safeguarded-funds figures.requestTaxRule,applyTaxRule, andassessSettlementTaxmanage tax rules and assess tax on settled wagers.
Workflow: customers, compliance, and safer play
- Verification:
openVerificationCase,decideVerificationCase. AML:reviewAMLCase,recordComplianceScreening. Privacy:transitionPrivacyRequest. - Safer play:
setResponsibleGamingLimitandselfExcludefrom the operator side; customers set limits and take breaks from their account. - Support:
setSupportCaseStatus, and dispute resolution withrequestDisputeResolution,withdrawDisputeProposal, andapplyDisputeResolution. - Wagers:
openWagerInvestigationopens an integrity case for a wager.
Workflow: operations and governance
- Promotions:
createPromotionCampaign, thenrequestPromotionActivationandapplyPromotionActivation. - 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
AuditEventlog. 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.
| Area | Mutations |
|---|---|
| Identity and access | createInitialUser, bootstrapOperatorAdmin, beginOperatorMfaEnrollment, completeOperatorMfaEnrollment, verifyOperatorMfaSession, sendUserPasswordResetLink, redeemUserPasswordResetToken, bootstrapIndependentApprover, requestRoleDefinition, applyApprovedRoleDefinition, requestRoleGrant, applyApprovedRoleGrant, requestRoleRevocation, applyApprovedRoleRevocation, requestOperatorAdmission, applyApprovedOperatorAdmission, requestSensitiveApproval, decideSensitiveApproval |
| Onboarding and configuration | previewSportsbookOnboarding, applySportsbookOnboarding, dismissOperatorOnboarding, requestSettingChange, applyApprovedSetting, requestLegalEntity, applyLegalEntity, createJurisdictionPolicyDraft, requestJurisdictionActivation, applyJurisdictionActivation, requestOperatorBrand, applyOperatorBrand, requestTenantCurrency, applyTenantCurrency, requestBrandLocale, applyBrandLocale, requestHouseRule, applyHouseRule, requestFeatureFlag, applyFeatureFlag |
| Catalog and trading | createCatalogEvent, createCatalogSport, createCatalogCompetition, createCompetitionSeason, createCatalogStage, createCatalogVenue, createCatalogCompetitor, setCatalogReferenceStatus, attachEventParticipant, rescheduleEvent, createEventMarket, setEventTradingState, ingestSandboxPrice, setMarketPrices, setMarketStatus, suspendMarket, requestTraderOverride, applyTraderOverride, requestCorrelationRule, applyCorrelationRule, createRiskLimit |
| Wagers, results, settlement | acceptStraightWager (customer boundary only), ingestSandboxResult, requestResultApproval, withdrawResultVersion, approveResultVersion, gradeMarket, gradeMarketSelection, settleWager, requestResettlementApproval, resettleWager, settleCombinationWager, requestCombinationResettlement, resettleCombinationWager, requestCashOutCorrection, correctCashOut, openWagerInvestigation |
| Finance | sandboxPayment, openChargeback, runReconciliation, runSafeguardingReconciliation, requestTaxRule, applyTaxRule, assessSettlementTax, requestLedgerAdjustment, applyLedgerAdjustment |
| Customers and compliance | openVerificationCase, decideVerificationCase, reviewAMLCase, recordComplianceScreening, transitionPrivacyRequest, setResponsibleGamingLimit, selfExclude, setSupportCaseStatus, requestDisputeResolution, withdrawDisputeProposal, applyDisputeResolution |
| Promotions and providers | createPromotionCampaign, requestPromotionActivation, applyPromotionActivation, mapProviderEntity, runAdapterCertification, runTradingReplay, setProviderOperationalState, requestWebhookEndpoint, applyWebhookEndpoint, testWebhookEndpoint |
| Operations and reports | enqueueRecovery, 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
querythrough GraphQL, gated by one role flag (for examplecanTradefor catalog and trading lists,canManageCompliancefor 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. Roleis 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
requireTenantwith 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.
Getting started with Sportsbook
Clone the source, configure PostgreSQL and secrets, apply migrations, create the first operator, enrol two-factor, run onboarding, start the worker, and deploy.
External dashboards
Give an LLM the Sportsbook dashboard skill to adapt the built-in operator experience or create a separate client.