SportBridge Engineering reference

Data models & operations

Every entity in the SportsBridge MVP, the fields it carries, and the operations allowed against it. Derived from the technical specification, sections A3 through A9.

Callers Operator Property Admin System
Kind get post ws fn fn is an internal service function, not a public endpoint
Restricted field Append-only

The specification lists field names; column types below are proposed for the PostgreSQL and Prisma stack named in A2 and lock at the end of Sprint 0. Money is integer cents throughout, never float.

No model matches “

Try a field name such as reserve_price, an endpoint such as reveal, or a model such as Bid.

Identity & access

User / Account

An organization — Operator or Property — has many users.

FieldTypeNotes
iduuidPrimary key
org_iduuidOwning organization
roleenumoperator · property · admin_reviewer · admin_compliance · superadmin. Admin is subdivided from the start — cheap now, painful to retrofit
emailcitextUnique login identity
statusenuminvited · active · suspended
kyb_statusenumpending · submitted · verified · rejected. Held at organization level
created_attimestamptzServer time

Operations

  • post/auth/*PublicSign up, sign in, reset password, refresh session
  • get/orgOperatorPropertyRead own organization profile
  • put/orgOperatorPropertyUpdate own organization profile
  • post/org/kyb-docsOperatorPropertyUpload verification documents; retention policy applies
  • fnmanageAdminUsers()SuperadminCreate, suspend and assign roles to admin users

LicenceProfile

Drives every compliance answer. Verified by an admin before it counts.

FieldTypeNotes
iduuidPrimary key
operator_iduuidOperator organizations only
jurisdictionchar(2)Country the licence covers
licence_typetextSports betting, casino, lottery
proof_doc_reftextStored document reference
verified_attimestamptzNull until an admin verifies. An unverified licence grants no eligibility
verified_byuuidAdmin who verified it

Operations

  • post/org/licencesOperatorAdd a licence; starts unverified
  • fnverifyLicence()AdminSets verified_at, activating the licence for matching
  • fnevaluateEligibility(operator, listing)SystemChecks the listing against every verified licence, best outcome wins, returns eligible · restricted · not_eligible

Supply

Listing

Holds restricted fields

Identity fields reach only an operator who has revealed. Enforced at the serializer, never in the UI.

FieldTypeNotes
iduuidPrimary key
property_iduuidOwning property organization
sportenumFootball, basketball, tennis, venue, and so on
asset_typeenumShirt, naming rights, LED, broadcast, digital, hospitality
regiontextCoarse public label, deliberately vaguer than the jurisdiction
jurisdictionchar(2)Drives compliance matching. Not the same thing as the displayed region
audience_datajsonbAttendance, broadcast reach, social, demographics. Checked for plausibility at review
termtextContract length offered
price_band_minintInteger cents
price_band_maxintInteger cents
statusenumSee the state machine below. Only live listings appear in the marketplace
rejection_reasontextRequired whenever a reviewer rejects
modeenumdirect · auction. Immutable once the listing goes live
auction_enabledboolAdmin gate. The property requests auction mode, an admin grants it during review
anon_titletextPublic title. Review strips anything that could identify the property
identity_nametextRestricted. Stripped at the serializer for any caller without a Reveal
identity_contacttextRestricted. Same rule. A leaked identity is a lost commission

State machine

draft submitted in_review live| rejected paused archived

A live listing under an active exclusivity lock shows as locked to every other operator.

Operations

  • post/listingsPropertyAdminCreate a draft from the wizard. Operators can never create listings
  • post/listings/:id/submitPropertySubmit a draft into the review queue
  • get/my/listingsPropertyOwn listings in any status
  • get/listings?filtersOperatorBrowse live listings, anonymized, with eligibility computed for this caller
  • get/listings/:idOperatorDetail. Identity fields present only when the caller holds a Reveal
  • get/admin/review-queueReviewerListings awaiting review, with SLA age against the 48-hour target
  • post/admin/listings/:id/approveReviewerPublish to live
  • post/admin/listings/:id/rejectReviewerReject with a stored reason
  • post/admin/listings/:id/enable-auctionReviewerGrant auction mode, judged on watchlist interest and eligible operators in that market
  • fnserializeForCaller(listing, caller)SystemStrips identity fields unless the caller holds a Reveal. Guarded by automated tests from Sprint 2 and treated as a release-blocking bug class

WatchlistItem

The only way demand is expressed. Operators cannot post requests of their own.

FieldTypeNotes
iduuidPrimary key
operator_iduuidWatcher
listing_iduuidWatched listing
created_attimestamptzServer time

Operations

  • post/watchlistOperatorWatch a listing
  • del/watchlist/:idOperatorStop watching
  • fnnotifyWatchers(listing)SystemSends price and status change notices to everyone watching
  • fncountForMarket(jurisdiction, asset)AdminFeeds the demand signal shown on the auction-gating toggle

Auction

Auction

One per auction-mode listing. Open ascending with masked bidders and a soft close.

FieldTypeNotes
iduuidPrimary key
listing_iduuidUnique. Exactly one auction per listing
reserve_priceintInteger cents. Bids below it are rejected at the API
reserve_visibleboolThe property chooses whether bidders see the reserve
opens_attimestamptzScheduled open
closes_attimestamptzMutable only by a soft-close extension, and every extension is logged
soft_close_windowintervalProposed 10 minutes, to confirm with the PO before Sprint 6
extension_capintExtensions are unlimited by default but capped by this value
min_incrementintPercentage or fixed, enforced server-side to stop one-cent laddering
stateenumSee below
extension_countintPushed live to all bidders each time it increases

State machine

scheduled open soft_close_extended * closed awarded| declined_all| no_bids

Only awarded creates a Deal. A no_bids or declined_all auction converts the listing back to direct mode automatically rather than being publicly marked as failed.

Operations

  • get/auctions/:idOperatorPropertyLive state: current high, bid count, time left
  • wsauction:{id}OperatorPropertyPushes current high bid, bid count, countdown and extensions
  • fnextendOnLateBid(auction, bid)SystemA bid inside the final window pushes closes_at out, logs it, and notifies every bidder
  • fncloseAuction(auction)SystemCloses on the server clock. The server is the only source of truth for time remaining
  • fnconvertToDirect(auction)SystemSilently returns a failed auction to direct mode, protecting the property's confidence

Bid

Append-only

A raise is a new row, never an update. Treat this table like a payment endpoint.

FieldTypeNotes
iduuidPrimary key
auction_iduuidPlacement is serialized per auction
operator_iduuidRestricted. Masked from other bidders for the whole auction. Disclosed to the property at close
amountintInteger cents. Amounts below the current high are never leaked to other bidders
placed_attimestamptzServer time, authoritative. A client timestamp is never trusted
stateenumactive · outbid · withdrawn · winning · lost
terms_accepted_attimestamptzPlacing a bid accepts the auction terms and binds the operator
rule_versiontextCompliance version in force at bid time, stored on the row

Operations

  • post/auctions/:id/bidsOperatorPlace a binding bid. Requires a prior reveal, re-checks eligibility, enforces reserve and increment
  • get/auctions/:id/bidsPropertyRanked, with bidder identities disclosed at close
  • get/auctions/:id/bidsOperatorOwn bids plus the current high only. Never other bidders
  • fnplaceBidSerialized(auction, operator, amount)SystemRow-level lock or single-writer queue. Two bids in the same millisecond must produce a deterministic order and the current-high read must never be stale. The most test-worthy logic in the MVP
  • fnwithdrawBid(bid, reason)AdminAllowed before close only, with an approved reason, and recorded
  • fnexportBidLog(auction)AdminImmutable, exportable dispute evidence

AuctionAward

The property may award a bid that is not the highest. The rationale is mandatory.

FieldTypeNotes
iduuidPrimary key
auction_iduuidAwarded auction
winning_bid_iduuidSets the deal's opening agreed amount
runner_up_bid_iduuidRecorded so a re-award needs no new auction
awarded_byuuidThe property user who awarded
awarded_attimestamptzServer time
rationaletextMandatory and stored, because the highest number need not win

Operations

  • post/auctions/:id/awardPropertyAward a compliant bid and open a deal room at that price
  • post/auctions/:id/declinePropertyDecline every bid, releasing all bidders
  • fnreawardToRunnerUp(award)PropertyIf the winner misses the signature window, award the runner-up without re-running the auction
  • fnautoDeclineOnSilence(auction)SystemAward window defaults to 5 business days. Silence auto-declines and releases every bidder
  • fnoverrideAward()AdminEmergency only, and always logged. Admins may never bid

Compliance

ComplianceRule

Append-only versions

A rules table, not code. The compliance analyst edits it through the admin console.

FieldTypeNotes
iduuidPrimary key
jurisdictionchar(2)First axis of the lookup
asset_typeenumSecond axis
sponsor_categoryenumThird axis. Betting and gaming at launch
statusenumeligible · restricted · banned
conditionstextHuman-readable conditions, shown on the listing when the status is restricted
sourcetextRegulatory reference backing the entry
versiontextFor example NL-v14. Every edit creates a new version; rows are never updated in place
reviewed_attimestamptzLast analyst review
Fail-closed. A jurisdiction with no rules entry renders as "compliance review pending" and cannot be revealed. Absence of a rule is never treated as permission.

Operations

  • get/admin/compliance-rulesComplianceBrowse the rule set with versions and review dates
  • post/admin/compliance-rulesCompliancePublish a new version. There is no in-place edit
  • fnpreviewImpact(draftRule)ComplianceShows which live listings change status before the version is published
  • fnresolveStatus(jurisdiction, asset, category)SystemReturns the status and the version used. Fails closed when no entry exists

Reveal & credits

CreditLedger

Append-only

Balance is derived by summing deltas, never stored as a mutable field.

FieldTypeNotes
iduuidPrimary key
operator_iduuidAccount the credits belong to
deltaintSigned. Positive on purchase, minus one on a reveal
reasonenumpack_purchase · reveal · expiry · refund
pack_iduuidStarter, Growth or Scale
balance_afterintSnapshot for auditability. The authoritative balance is still the sum of deltas
expires_attimestamptzTwelve months from purchase

Operations

  • get/creditsOperatorCurrent balance and full ledger history
  • post/packs/:id/purchaseOperatorBuy a pack. Properties and admins never hold credits
  • fndebitOneCredit(operator)SystemCalled only inside the atomic reveal transaction, never on its own
  • fnexpireCredits()SystemScheduled job writing expiry rows at the twelve-month mark

Reveal

Immutable

The legal artifact of the whole platform. Immutable and exportable.

FieldTypeNotes
iduuidReferenced by enforcement cases
listing_iduuidListing whose identity was unlocked
operator_iduuidWho paid and saw the identity
timestamptimestamptzServer time. Never updated
non_circumvention_accepted_attimestamptzAcceptance is required before the credit is debited
rule_versiontextProves what the platform showed, and when, if the rules later change

The atomic reveal — one transaction, no partial states

check eligibility check mode require acceptance debit 1 credit write Reveal notify property
On a direct listing it also checks that no competing lock exists, then writes an ExclusivityLock.
On an auction listing there is no lock and no competition check. Many operators reveal the same listing, each paying, and the reveal grants bidding rights instead.

Operations

  • post/listings/:id/revealOperatorThe whole sequence above in a single database transaction. Partial reveals must be impossible
  • get/my/revealsOperatorOwn reveal log
  • fnexportRevealLog(scope)OperatorAdminTimestamped export used as enforcement evidence

ExclusivityLock

Direct listings only. An auction never creates a lock.

FieldTypeNotes
iduuidPrimary key
listing_iduuidLocked listing
operator_iduuidHolder of the exclusive window
starts_attimestamptzSet at the moment of reveal
expires_attimestamptzTwenty-one days after the reveal

Operations

  • fnacquireLock(listing, operator)SystemRedis lock plus a database row, inside the reveal transaction. A competing lock fails the whole reveal
  • fncheckNoCompetingLock(listing)SystemGate on every direct reveal attempt. The double-reveal race test is release-blocking
  • fnexpireLock()SystemAt twenty-one days the listing reopens and an unused deal expires

Deal execution

Deal

Two entry points, one spine. A direct reveal and an auction award both land here.

FieldTypeNotes
iduuidPrimary key
listing_iduuidListing being transacted
operator_iduuidBuying operator
sourceenumdirect_reveal · auction_award
stateenumSee the state machine below
agreed_amountintInteger cents. Pre-set to the winning bid on an auction-sourced deal, so negotiation starts from a settled price
commission_ratenumeric7% to €250k · 6% to €1M · 5% above
commission_amountintInteger cents. Covered by unit tests against all three bands

Entry points

reveal_madedeal_room_opendirect
auction_awardeddeal_room_openauction, at the winning price

Spine

offer_negotiation agreed contract_generated contract_signed escrow_funded activation_pending activation_confirmed completed

Terminal branches

expiredexclusivity lapsed unused cancelledmutual disputedadmin resolution

If an auction-sourced deal is cancelled before contract signature, the property may re-award the runner-up without re-running the auction.

Operations

  • post/dealsSystemOpened by a reveal, or by an auction award at the winning price
  • get/deals/:idOperatorPropertyOwn deals only
  • wsdeal:{id}OperatorPropertyMessages, offers and state changes in real time
  • get/admin/dealsAdminPlatform-wide board with state and age, plus alerts on stalled deals. Admins enter a room only when a deal is flagged
  • fncomputeCommission(amount)SystemInteger cents across the 7, 6 and 5 percent bands. No float arithmetic anywhere

Offer

Immutable rows

A counter is a new row. The latest accepted offer sets the deal's agreed amount.

FieldTypeNotes
iduuidPrimary key
deal_iduuidParent deal
sender_iduuidOperator or property user
amountintInteger cents
stateenumopen · accepted · superseded
created_attimestamptzThe offer trail is evidence and is never rewritten

Operations

  • post/deals/:id/offersOperatorPropertySend an offer or a counter as a new immutable row
  • post/offers/:id/acceptOperatorPropertyAccept, which sets the deal's agreed amount and advances it to agreed

Message

Deal-room conversation. Part of the deal record.

FieldTypeNotes
iduuidPrimary key
deal_iduuidParent deal
sender_iduuidAuthor
bodytextMessage text
attachmentsjsonbDocument references, for example audience reports
created_attimestamptzServer time

Operations

  • wsdeal:{id}OperatorPropertySend and receive messages in the deal room
  • get/deals/:idOperatorPropertyReturns message history with the deal
  • fnviewFlaggedDeal(deal)AdminAdmins read a room only once a deal is flagged or disputed

Contract

Generated from standard templates only. No free drafting on the platform.

FieldTypeNotes
iduuidPrimary key
deal_iduuidParent deal
template_versiontextWhich standard template produced this contract
operator_signed_attimestamptzSignature status per party
property_signed_attimestamptzBoth are required to advance the deal
signed_doc_reftextProvider document reference

Operations

  • fngenerateFromTemplate(deal)SystemProduces the contract at the agreed amount
  • post/deals/:id/contract/signOperatorPropertySign through the e-signature provider
  • fnSignatureProviderSystemProvider behind our own interface. A mock ships first so external onboarding never blocks the critical path

EscrowTransaction

Funds are provider-held. They never touch SMH accounts.

FieldTypeNotes
iduuidPrimary key
deal_iduuidParent deal
provider_reftextReference at the licensed escrow provider
stateenuminitiated · funded · released · refunded
gross_amountintInteger cents
commission_amountintDeducted at release, not before
net_payout_amountintWhat reaches the property

State machine

initiated funded released| refunded

Operations

  • post/deals/:id/escrow/fundOperatorFund escrow once both parties have signed
  • fnreleaseEscrow(deal)SystemFires on activation confirmation, deducts commission and pays the property
  • fnrefundEscrow(deal)SuperadminOverride requiring dual approval. No other role can move escrow
  • fnEscrowProviderSystemProvider abstraction. MockEscrowProvider ships first so the deal flow is never blocked

ActivationProof

The gate for escrow release. No confirmation, no payout.

FieldTypeNotes
iduuidPrimary key
deal_iduuidParent deal
filesjsonbPhotos, broadcast clips, links
submitted_attimestamptzUploaded by the property after delivery
confirmed_byuuidThe operator user who confirmed
confirmed_attimestamptzTimestamp that unlocks the payout

Operations

  • post/deals/:id/proofPropertyUpload proof that the sponsorship went live
  • post/deals/:id/confirm-activationOperatorConfirm the proof, which releases escrow minus commission

Oversight

BypassAlert

Raised by admins, manually during beta, referencing the reveal and bid logs.

FieldTypeNotes
iduuidPrimary key
listing_iduuidListing the suspected bypass concerns
operator_iduuidSuspected party
severityenumlow · medium · high
evidencejsonbReveal references, bid log export, press items
stateenumopen · investigating · resolved · escalated

Operations

  • get/admin/alertsAdminThe alert queue
  • post/admin/alerts/:id/resolveAdminResolve or escalate an alert
  • fnautoWatchLosingBidders(auction)SystemLosing bidders are a heightened risk — they know the identity and did not win — so their listings are watched automatically
  • fnexportEvidencePack(alert)AdminReveal log plus bid log, packaged for an enforcement case

Notification

Notifications are the engine of an auction. They ship with the bidding module, not after it.

FieldTypeNotes
iduuidPrimary key
user_iduuidRecipient
typeenumoutbid · closing_soon at 24h, 1h and 10min · extension · result · reveal_made · offer_received · review_decision · award_due
payloadjsonbDeep-link target and display values
read_attimestamptzNull while unread

Operations

  • wsuser:{id}OperatorPropertyAdminLive notification channel
  • get/notificationsOperatorPropertyAdminHistory and unread state
  • fnfanOut(event)SystemRedis-backed fan-out, with transactional email for anything time-critical

AuditLog

Append-only

Covers every state transition in the system, from the first sprint.

FieldTypeNotes
iduuidPrimary key
actor_iduuidUser or system process that caused the transition
entity_typetextWhich model changed
entity_iduuidWhich row
from_statetextPrevious state
to_statetextNew state
created_attimestamptzServer time. Rows are never updated or deleted

Operations

  • fnappend(event)SystemCalled on every state transition, including reveals, offers, signatures and escrow events
  • fnexportAuditLog(scope)AdminImmutable and exportable, because this is legal evidence for non-circumvention enforcement

Invariants

Rules that hold across the models above. Breaking one of these is a release-blocking bug, not a ticket.

Identity is unreachable without a reveal

Enforced at the serializer, not the UI. A leaked identity is a lost commission. Automated tests guard this from Sprint 2 onward.

Bidder identities never reach other bidders

Masked for the whole auction, disclosed to the property at close. Bid amounts below the current high are never leaked.

Append-only where it matters

Bid, Reveal, CreditLedger, ComplianceRule versions, Offer and AuditLog are written once. A raise, a counter or a rule change is always a new row.

Money is integer cents

No float arithmetic anywhere. Commission is unit-tested against the 7, 6 and 5 percent bands, and escrow funds stay with the provider.

The server owns time

Bid timestamps and the auction clock are server-side. Clients render a countdown from a server timestamp and must survive clock skew and reconnection without ever showing an open auction as closed.

Bid placement is serialized per auction

Two bids arriving in the same millisecond produce a deterministic order, and the current-high read is never stale. The concurrency suite is part of Sprint 6, not a follow-up.

Reveals are atomic

Eligibility, mode, acceptance, debit, write and notify happen in one transaction. A partial reveal must be impossible.

Compliance fails closed

A market with no rules entry cannot be revealed. Every eligibility answer stores the rule version that produced it.