The threats Sportz defends against today, the mitigations in place, and the honest gaps.
Security documented here is what exists. Sportz has no authentication, so this page covers the layers that are built (input validation, rate limiting, headers, secrets, transport) and is explicit about what isn’t.
Every request body and query is parsed with a Zod schema before any logic runs. Invalid input returns 400 with structured issue details and never reaches the database. This also closes the door on a class of injection by rejecting malformed shapes early. See the validation schemas in Backend.
All queries go through Drizzle ORM, which uses parameterized queries: user input is never string-concatenated into SQL. The one place raw SQL appears (TRUNCATE in the test reset helper) takes no user input.
Sliding window: 50 req/10s on HTTP; 5 connections/2s on WS upgrades.
A denied request gets 429 (rate limit) or 403 (other). If Arcjet itself errors, the HTTP path fails closed (503). Arcjet is mocked in tests so the suite is fast and offline (see Testing).
Production talks to Neon Cloud over verified TLS (rejectUnauthorized: true). The three-way SSL strategy (Cloud verified / Local self-signed / plain-Postgres off) is in Backend and was the subject of a real bug (Issues).
Follows from no auth: no roles or protected resources.
CSRF
➖ N/A today
No cookie-based auth / state-changing browser forms with ambient credentials, so the classic CSRF vector doesn’t apply. Becomes relevant the moment auth + cookies are added.
XSS
🟡 Partial
React escapes rendered content by default; no dangerouslySetInnerHTML in use. A formal CSP (Content-Security-Policy) header is not yet configured, a recommended hardening step.
Secrets rotation
❌ Manual
No automated rotation.
The biggest security caveat: there is no auth. Anyone who can reach the API can create matches and commentary. That is acceptable for a demo/portfolio platform but would be the first thing to add before any real multi-user deployment.
{"docsConfig":{"theme":"mint","$schema":"https://mintlify.com/docs.json","name":"Sportz Docs","colors":{"primary":"#EAB308","light":"#F4C542","dark":"#EAB308"},"logo":{"light":"https://mintcdn.com/sportzdocs/ZqapXRecIQ7JtK1M/logo/light.svg?fit=max\u0026auto=format\u0026n=ZqapXRecIQ7JtK1M\u0026q=85\u0026s=654dae11c1c4160e24667a72a6fa2fe7","dark":"https://mintcdn.com/sportzdocs/ZqapXRecIQ7JtK1M/logo/dark.svg?fit=max\u0026auto=format\u0026n=ZqapXRecIQ7JtK1M\u0026q=85\u0026s=ddc4c9ade6959cf37bcb93e2c3e1f320"},"favicon":"https://mintcdn.com/sportzdocs/ZqapXRecIQ7JtK1M/favicon.svg?fit=max\u0026auto=format\u0026n=ZqapXRecIQ7JtK1M\u0026q=85\u0026s=88b93ba67882ed2c861fa42d6666d149","navbar":{"primary":{"type":"github","href":"https://github.com/Jheff4/sportz-websockets"}},"footer":{"socials":{"github":"https://github.com/Jheff4"}},"contextual":{"options":["copy","view","chatgpt","claude"],"display":"header"},"styling":{"latex":false},"navigation":{"global":{"anchors":[{"anchor":"Live Demo","icon":"rocket","href":"https://sportz-ui.vercel.app"},{"anchor":"Backend Repo","icon":"server","href":"https://github.com/Jheff4/sportz-websockets"},{"anchor":"Frontend Repo","icon":"browser","href":"https://github.com/Jheff4/sportz-ui"}]},"pages":[]}},"deployment":{"deploymentMetadataValue":{"subdomain":"sportzdocs","hasAudioNarration":false,"actualSubdomain":"sportzdocs","customDomains":[],"gitSource":{"type":"github","owner":"jheff4","repo":"sportz-docs","deployBranch":"main","contentDirectory":"","isPrivate":false,"mountPath":"","label":""},"chroma":{"latestBuiltDeploymentHistoryId":"6a91a79bb34810eb4c786144","collectionId":"1d2ebbb9-3581-4408-b4ed-1155a1c227d2","liveDeploymentHistoryId":"6a91a79bb34810eb4c786144","schemaVersion":"v3"},"entitlements":{"AI_CHAT":{"status":"DISABLED"}},"buildId":"6a91a79bb34810eb4c786144","clientVersion":"0.0.4039","searchFilterCounts":[{"language":"default","tag":"Handbook","count":14},{"language":"default","tag":"Learn \u0026 Interview","count":5},{"language":"default","tag":"Architecture","count":4},{"language":"default","tag":"Operations","count":4},{"language":"default","tag":"Overview","count":3},{"language":"default","tag":"Learning","count":3},{"language":"default","tag":"Engineering Stories","count":2},{"language":"default","tag":"Interview Prep","count":2},{"language":"default","tag":"Reference","count":1}]},"assistantConfig":{"enableAskAiButton":false,"enableStarterQuestions":false,"enableContextualStarterQuestions":false,"enableAskAiSearchBar":true},"basePath":"","hasChatPermissions":false,"hasWebMcpPermissions":false,"toggles":[{"name":"kill-isr","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"agent-mintlify-slack","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"agent-integrations-settings","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automations-pr-grouping","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"custom-prefix","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"analytics-v7","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"user-flows-analytics","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"no-publish-flow","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-onboarding","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"llms-txt-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"snapshot-serving-live","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"stripTemperatureMiddleware","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"support-platform-pylon-zendesk","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"snapshot-serving","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"instant-rollback","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"granular-notifications-server","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"granular-notifications","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automations-fable","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"fixed-ai-action-pricing","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automation-resolve-merge-conflict","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"scoped-publishing","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"mintql","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"page-level-analytics","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"editor-version-history-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"billing-page-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"full-page-search","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workspace-projects","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"assistant-v5","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"byok-sep-25","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"fork-pr-previews","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"revalidation-jwt","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"vite-origin","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"billing-updated-9-15","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-sso-scim","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"hobby-abuse-scan","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"dashboard-inbox","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-signup","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"turbopuffer-dual-write","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-enabled","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"approve-review-flow","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false}],"googleFontsInlined":false},"customCss":[{"filePath":"style.css","content":"#navbar .flex-1.border-b,#navbar-transition{border-bottom:none!important}#navbar div.h-16{position:relative}#navbar div.h-16::after{content:\"\";position:absolute;bottom:0;left:50%;transform:translateX(-50%);width:100vw;border-bottom:1px solid rgb(107 114 128/.12);pointer-events:none}.dark #navbar div.h-16::after{border-bottom-color:rgb(209 213 219/.08)}#navbar{overflow-x:clip}"}],"page":{"pagePath":"operations/security","slug":"/operations/security","pageMetadata":{"title":"Security","description":"The threats Sportz defends against today, the mitigations in place, and the honest gaps.","href":"/operations/security"},"description":{"compiledSource":"\"use strict\";\nconst {jsx: _jsx} = arguments[0];\nconst {useMDXComponents: _provideComponents} = arguments[0];\nfunction _createMdxContent(props) {\n const _components = {\n p: \"p\",\n ..._provideComponents(),\n ...props.components\n };\n return _jsx(_components.p, {\n children: \"The threats Sportz defends against today, the mitigations in place, and the honest gaps.\"\n });\n}\nfunction MDXContent(props = {}) {\n const {wrapper: MDXLayout} = {\n ..._provideComponents(),\n ...props.components\n };\n return MDXLayout ? _jsx(MDXLayout, {\n ...props,\n children: _jsx(_createMdxContent, {\n ...props\n })\n }) : _createMdxContent(props);\n}\nreturn {\n default: MDXContent\n};\n","frontmatter":{},"scope":{}},"pageType":"default","mdx":{"mdxSource":{"compiledSource":"\"use strict\";const{Fragment:_Fragment,jsx:_jsx,jsxs:_jsxs}=arguments[0];const{useMDXComponents:_provideComponents}=arguments[0];function _createMdxContent(props){const _components={a:`a`,code:`code`,em:`em`,li:`li`,p:`p`,strong:`strong`,tbody:`tbody`,td:`td`,th:`th`,thead:`thead`,tr:`tr`,ul:`ul`,..._provideComponents(),...props.components},{Heading,Mermaid,Table,Warning}=_components;if(!Heading)_missingMdxReference(`Heading`,true);if(!Mermaid)_missingMdxReference(`Mermaid`,true);if(!Table)_missingMdxReference(`Table`,true);if(!Warning)_missingMdxReference(`Warning`,true);return _jsxs(_Fragment,{children:[_jsxs(_components.p,{children:[`Security documented here is `,_jsx(_components.strong,{children:`what exists`}),`. Sportz has no authentication, so this page covers the layers that `,_jsx(_components.em,{children:`are`}),` built (input validation, rate limiting, headers, secrets, transport) and is explicit about what isn’t.`]}),`\n`,_jsx(Heading,{level:`2`,id:`defense-layers-what’s-built`,children:`Defense layers (what’s built)`}),`\n`,_jsx(Mermaid,{chart:`flowchart TB\n req[\"Incoming request\"] --> helmet[\"Helmet\\\\nsecure headers\"]\n helmet --> cors[\"CORS\\\\norigin allowlist\"]\n cors --> arcjet[\"Arcjet\\\\nshield + bot + rate limit\"]\n arcjet --> zod[\"Zod\\\\ninput validation\"]\n zod --> route[\"Route handler\"]\n route --> drizzle[\"Drizzle\\\\nparameterized queries\"]`}),`\n`,_jsx(Heading,{level:`2`,id:`input-validation-zod`,children:`Input validation: Zod`}),`\n`,_jsxs(_components.p,{children:[`Every request body and query is parsed with a Zod schema before any logic runs. Invalid input returns `,_jsx(_components.code,{children:`400`}),` with structured issue details and never reaches the database. This also closes the door on a class of injection by rejecting malformed shapes early. See the validation schemas in `,_jsx(_components.a,{href:`/architecture/backend`,children:`Backend`}),`.`]}),`\n`,_jsx(Heading,{level:`2`,id:`sql-injection-drizzle-parameterization`,children:`SQL injection: Drizzle parameterization`}),`\n`,_jsxs(_components.p,{children:[`All queries go through Drizzle ORM, which uses parameterized queries: user input is never string-concatenated into SQL. The one place raw SQL appears (`,_jsx(_components.code,{children:`TRUNCATE`}),` in the test reset helper) takes no user input.`]}),`\n`,_jsx(Heading,{level:`2`,id:`rate-limiting-\u0026-bots-arcjet`,children:`Rate limiting \u0026 bots: Arcjet`}),`\n`,_jsx(_components.p,{children:`Arcjet runs on both the HTTP path and the WebSocket upgrade:`}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Shield`}),`: common attack patterns.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Bot detection`}),`: blocks automated traffic (allows search engines / previews).`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Sliding window`}),`: 50 req/10s on HTTP; 5 connections/2s on WS upgrades.`]}),`\n`]}),`\n`,_jsxs(_components.p,{children:[`A denied request gets `,_jsx(_components.code,{children:`429`}),` (rate limit) or `,_jsx(_components.code,{children:`403`}),` (other). If Arcjet itself errors, the HTTP path `,_jsx(_components.strong,{children:`fails closed`}),` (`,_jsx(_components.code,{children:`503`}),`). Arcjet is mocked in tests so the suite is fast and offline (see `,_jsx(_components.a,{href:`/operations/testing`,children:`Testing`}),`).`]}),`\n`,_jsx(Heading,{level:`2`,id:`secure-headers-helmet`,children:`Secure headers: Helmet`}),`\n`,_jsxs(_components.p,{children:[_jsx(_components.code,{children:`helmet()`}),` sets a baseline of secure response headers (HSTS, no-sniff, frame options, etc.) on every Express response.`]}),`\n`,_jsx(Heading,{level:`2`,id:`transport-security-tls`,children:`Transport security: TLS`}),`\n`,_jsxs(_components.p,{children:[`Production talks to Neon Cloud over verified TLS (`,_jsx(_components.code,{children:`rejectUnauthorized: true`}),`). The three-way SSL strategy (Cloud verified / Local self-signed / plain-Postgres off) is in `,_jsx(_components.a,{href:`/architecture/backend`,children:`Backend`}),` and was the subject of a real bug (`,_jsx(_components.a,{href:`/issues`,children:`Issues`}),`).`]}),`\n`,_jsx(Heading,{level:`2`,id:`secrets-\u0026-environment-variables`,children:`Secrets \u0026 environment variables`}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsxs(_components.li,{children:[`Secrets (`,_jsx(_components.code,{children:`DATABASE_URL`}),`, `,_jsx(_components.code,{children:`ARCJET_KEY`}),`) are `,_jsx(_components.strong,{children:`never committed`}),`: `,_jsx(_components.code,{children:`.env*`}),` is gitignored; only `,_jsx(_components.code,{children:`.env.*.example`}),` templates are tracked.`]}),`\n`,_jsxs(_components.li,{children:[`In production they’re injected via Render dashboard (`,_jsx(_components.code,{children:`sync: false`}),` in `,_jsx(_components.code,{children:`render.yaml`}),`), not baked into the image.`]}),`\n`,_jsx(_components.li,{children:`In CI they’re GitHub Actions secrets.`}),`\n`]}),`\n`,_jsx(Heading,{level:`2`,id:`dependency-security`,children:`Dependency security`}),`\n`,_jsxs(_components.p,{children:[_jsx(_components.code,{children:`npm audit`}),` surfaces known vulnerabilities; CI installs with `,_jsx(_components.code,{children:`npm ci`}),` for reproducible, lockfile-pinned installs.`]}),`\n`,_jsx(Heading,{level:`2`,id:`honest-gaps-not-built`,children:`Honest gaps (not built)`}),`\n`,_jsxs(Table,{children:[_jsx(_components.thead,{children:_jsxs(_components.tr,{children:[_jsx(_components.th,{children:`Area`}),_jsx(_components.th,{children:`Status`}),_jsx(_components.th,{children:`Note`})]})}),_jsxs(_components.tbody,{children:[_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.strong,{children:`Authentication`})}),_jsx(_components.td,{children:`❌ None`}),_jsx(_components.td,{children:`No user accounts. The API is open.`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.strong,{children:`Authorization`})}),_jsx(_components.td,{children:`❌ None`}),_jsx(_components.td,{children:`Follows from no auth: no roles or protected resources.`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.strong,{children:`CSRF`})}),_jsx(_components.td,{children:`➖ N/A today`}),_jsx(_components.td,{children:`No cookie-based auth / state-changing browser forms with ambient credentials, so the classic CSRF vector doesn’t apply. Becomes relevant the moment auth + cookies are added.`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.strong,{children:`XSS`})}),_jsx(_components.td,{children:`🟡 Partial`}),_jsxs(_components.td,{children:[`React escapes rendered content by default; no `,_jsx(_components.code,{children:`dangerouslySetInnerHTML`}),` in use. A formal CSP (`,_jsx(_components.code,{children:`Content-Security-Policy`}),`) header is `,_jsx(_components.strong,{children:`not`}),` yet configured, a recommended hardening step.`]})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.strong,{children:`Secrets rotation`})}),_jsx(_components.td,{children:`❌ Manual`}),_jsx(_components.td,{children:`No automated rotation.`})]})]})]}),`\n`,_jsx(Warning,{children:_jsxs(_components.p,{children:[`The biggest security caveat: `,_jsx(_components.strong,{children:`there is no auth.`}),` Anyone who can reach the API can create matches and commentary. That is acceptable for a demo/portfolio platform but would be the first thing to add before any real multi-user deployment.`]})})]})}function MDXContent(props={}){const{wrapper:MDXLayout}={..._provideComponents(),...props.components};return MDXLayout?_jsx(MDXLayout,{...props,children:_jsx(_createMdxContent,{...props})}):_createMdxContent(props)}return{default:MDXContent};function _missingMdxReference(id,component){throw new Error(`Expected `+(component?`component`:`object`)+\" `\"+id+\"` to be defined: you likely forgot to import, pass, or provide it.\")}","frontmatter":{},"scope":{"config":{},"pageMetadata":{"title":"Security","description":"The threats Sportz defends against today, the mitigations in place, and the honest gaps.","href":"/operations/security"}}},"mdxSourceWithNoJs":null},"mdxExtracts":{"tableOfContents":[{"title":"Defense layers (what’s built)","slug":"defense-layers-what’s-built","depth":2,"children":[]},{"title":"Input validation: Zod","slug":"input-validation-zod","depth":2,"children":[]},{"title":"SQL injection: Drizzle parameterization","slug":"sql-injection-drizzle-parameterization","depth":2,"children":[]},{"title":"Rate limiting \u0026 bots: Arcjet","slug":"rate-limiting-\u0026-bots-arcjet","depth":2,"children":[]},{"title":"Secure headers: Helmet","slug":"secure-headers-helmet","depth":2,"children":[]},{"title":"Transport security: TLS","slug":"transport-security-tls","depth":2,"children":[]},{"title":"Secrets \u0026 environment variables","slug":"secrets-\u0026-environment-variables","depth":2,"children":[]},{"title":"Dependency security","slug":"dependency-security","depth":2,"children":[]},{"title":"Honest gaps (not built)","slug":"honest-gaps-not-built","depth":2,"children":[]}],"codeExamples":{}},"jsLinks":[],"subdomain":"sportzdocs","siteName":"Sportz Docs","title":"Security - Sportz Docs","navMaps":{"firstHrefInVersion":{},"firstHrefInLanguage":{}},"lastModified":"2026-08-28T14:51:30.943Z","originalFileLocation":"operations/security.mdx"},"headHtml":"\u003cscript data-mint-head>(function(){var d=document.documentElement,p=null;if(!p){try{p=localStorage.getItem(\"isDarkMode\")}catch(e){}if(p==='true')p='dark';else if(p==='false')p='light';if(p!=='light'\u0026\u0026p!=='dark'\u0026\u0026p!=='system')p=\"system\"}var r=p==='system'?(matchMedia(\"(prefers-color-scheme: dark)\").matches?'dark':'light'):p;d.classList.remove(r==='dark'?'light':'dark');d.classList.add(r);d.setAttribute(\"data-theme-preference\",p)})();\u003c/script>\u003cmeta data-mint-head name=\"description\" content=\"The threats Sportz defends against today, the mitigations in place, and the honest gaps.\">\u003cmeta data-mint-head name=\"application-name\" content=\"Sportz Docs\">\u003cmeta data-mint-head name=\"generator\" content=\"Mintlify\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"192x192\" href=\"/mintlify-assets/_mintlify/favicons/sportzdocs/PXW7TdvbuE1uPR7j/_generated/favicon/android-chrome-192x192.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"apple-touch-icon\" type=\"image/png\" sizes=\"180x180\" href=\"/mintlify-assets/_mintlify/favicons/sportzdocs/PXW7TdvbuE1uPR7j/_generated/favicon/apple-touch-icon.png\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/mintlify-assets/_mintlify/favicons/sportzdocs/PXW7TdvbuE1uPR7j/_generated/favicon/favicon-16x16.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/mintlify-assets/_mintlify/favicons/sportzdocs/PXW7TdvbuE1uPR7j/_generated/favicon/favicon-32x32.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/mintlify-assets/_mintlify/favicons/sportzdocs/PXW7TdvbuE1uPR7j/_generated/favicon/favicon.ico\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"192x192\" href=\"/mintlify-assets/_mintlify/favicons/sportzdocs/PXW7TdvbuE1uPR7j/_generated/favicon-dark/android-chrome-192x192.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/mintlify-assets/_mintlify/favicons/sportzdocs/PXW7TdvbuE1uPR7j/_generated/favicon-dark/favicon-16x16.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/mintlify-assets/_mintlify/favicons/sportzdocs/PXW7TdvbuE1uPR7j/_generated/favicon-dark/favicon-32x32.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/mintlify-assets/_mintlify/favicons/sportzdocs/PXW7TdvbuE1uPR7j/_generated/favicon-dark/favicon.ico\" media=\"(prefers-color-scheme: dark)\">\u003cmeta data-mint-head property=\"og:title\" content=\"Security - Sportz Docs\">\u003cmeta data-mint-head property=\"og:description\" content=\"The threats Sportz defends against today, the mitigations in place, and the honest gaps.\">\u003cmeta data-mint-head property=\"og:site_name\" content=\"Sportz Docs\">\u003cmeta data-mint-head property=\"og:url\" content=\"https://sportzdocs.mintlify.app/operations/security\">\u003cmeta data-mint-head property=\"og:type\" content=\"website\">\u003cmeta data-mint-head property=\"og:image\" content=\"https://sportzdocs.mintlify.app/_mintlify/api/og/v2?division=Operations\u0026amp;title=Security\u0026amp;description=The+threats+Sportz+defends+against+today%2C+the+mitigations+in+place%2C+and+the+honest+gaps.\u0026amp;theme=613b975bd84dd49fe79bfd9d\u0026amp;signature=d90f1af31888610c7feebe7c9d0039f68f7cd0c855fbecc1a7d59b886ee848d3\">\u003cmeta data-mint-head property=\"og:image:width\" content=\"1200\">\u003cmeta data-mint-head property=\"og:image:height\" content=\"630\">\u003cmeta data-mint-head name=\"twitter:card\" content=\"summary_large_image\">\u003cmeta data-mint-head name=\"twitter:title\" content=\"Security - Sportz Docs\">\u003cmeta data-mint-head name=\"twitter:description\" content=\"The threats Sportz defends against today, the mitigations in place, and the honest gaps.\">\u003cmeta data-mint-head name=\"twitter:image\" content=\"https://sportzdocs.mintlify.app/_mintlify/api/og/v2?division=Operations\u0026amp;title=Security\u0026amp;description=The+threats+Sportz+defends+against+today%2C+the+mitigations+in+place%2C+and+the+honest+gaps.\u0026amp;theme=613b975bd84dd49fe79bfd9d\u0026amp;signature=d90f1af31888610c7feebe7c9d0039f68f7cd0c855fbecc1a7d59b886ee848d3\">\u003cmeta data-mint-head name=\"twitter:image:width\" content=\"1200\">\u003cmeta data-mint-head name=\"twitter:image:height\" content=\"630\">\u003cmeta data-mint-head name=\"msapplication-config\" content=\"/mintlify-assets/_mintlify/favicons/sportzdocs/PXW7TdvbuE1uPR7j/_generated/favicon/browserconfig.xml\">\u003cmeta data-mint-head name=\"apple-mobile-web-app-title\" content=\"Sportz Docs\">\u003cmeta data-mint-head name=\"msapplication-TileColor\" content=\"#EAB308\">\u003clink data-mint-head rel=\"canonical\" href=\"https://sportzdocs.mintlify.app/operations/security\">\u003clink data-mint-head rel=\"alternate\" type=\"application/xml\" href=\"/sitemap.xml\">\u003clink data-mint-head rel=\"alternate\" type=\"text/markdown\" href=\"/operations/security.md\">\u003cscript data-mint-head type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@graph\":[{\"@type\":\"Organization\",\"@id\":\"https://sportzdocs.mintlify.app/#organization\",\"name\":\"Sportz Docs\",\"url\":\"https://sportzdocs.mintlify.app\",\"logo\":{\"@type\":\"ImageObject\",\"url\":\"https://mintcdn.com/sportzdocs/ZqapXRecIQ7JtK1M/logo/light.svg?fit=max\u0026auto=format\u0026n=ZqapXRecIQ7JtK1M\u0026q=85\u0026s=654dae11c1c4160e24667a72a6fa2fe7\"}},{\"@type\":\"WebSite\",\"@id\":\"https://sportzdocs.mintlify.app#website\",\"name\":\"Sportz Docs\",\"url\":\"https://sportzdocs.mintlify.app\",\"publisher\":{\"@id\":\"https://sportzdocs.mintlify.app/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https://sportzdocs.mintlify.app/operations/security#webpage\",\"url\":\"https://sportzdocs.mintlify.app/operations/security\",\"name\":\"Security\",\"description\":\"The threats Sportz defends against today, the mitigations in place, and the honest gaps.\",\"dateModified\":\"2026-08-28T14:51:30.943Z\",\"isPartOf\":{\"@id\":\"https://sportzdocs.mintlify.app#website\"},\"breadcrumb\":{\"@id\":\"https://sportzdocs.mintlify.app/operations/security#breadcrumb\"}},{\"@type\":\"BreadcrumbList\",\"@id\":\"https://sportzdocs.mintlify.app/operations/security#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Operations\",\"item\":\"https://sportzdocs.mintlify.app/operations/testing\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Security\",\"item\":\"https://sportzdocs.mintlify.app/operations/security\"}]},{\"@type\":[\"Article\",\"TechArticle\"],\"@id\":\"https://sportzdocs.mintlify.app/operations/security#article\",\"headline\":\"Security\",\"name\":\"Security\",\"description\":\"The threats Sportz defends against today, the mitigations in place, and the honest gaps.\",\"url\":\"https://sportzdocs.mintlify.app/operations/security\",\"mainEntityOfPage\":{\"@id\":\"https://sportzdocs.mintlify.app/operations/security#webpage\"},\"dateModified\":\"2026-08-28T14:51:30.943Z\",\"publisher\":{\"@id\":\"https://sportzdocs.mintlify.app/#organization\"},\"isPartOf\":{\"@id\":\"https://sportzdocs.mintlify.app#website\"}}]}\u003c/script>\u003cscript data-mint-head type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"WebSite\",\"name\":\"Sportz Docs\",\"creator\":{\"@type\":\"Organization\",\"name\":\"Mintlify\",\"url\":\"https://mintlify.com\"}}\u003c/script>","siteName":"Sportz Docs","nav":{"global":{"anchors":[{"anchor":"Live Demo","icon":"rocket","href":"https://sportz-ui.vercel.app"},{"anchor":"Backend Repo","icon":"server","href":"https://github.com/Jheff4/sportz-websockets"},{"anchor":"Frontend Repo","icon":"browser","href":"https://github.com/Jheff4/sportz-ui"}]},"tabs":[{"tab":"Handbook","groups":[{"group":"Overview","pages":[{"title":"Sportz Engineering Handbook","href":""},{"title":"Project Status \u0026 Scope","href":"/project-status"},{"title":"User Stories","href":"/stories"}]},{"group":"Architecture","pages":[{"title":"System Architecture (C4)","href":"/architecture/system"},{"title":"Frontend Architecture","href":"/architecture/frontend"},{"title":"Backend Architecture","href":"/architecture/backend"},{"title":"Real-Time System Deep Dive","href":"/architecture/realtime"}]},{"group":"Engineering Stories","pages":[{"title":"Architectural Decision Records","href":"/decisions"},{"title":"Engineering Log: Issues \u0026 Post-Mortems","href":"/issues"}]},{"group":"Operations","pages":[{"title":"Testing Strategy","href":"/operations/testing"},{"title":"DevOps \u0026 CI/CD","href":"/operations/devops"},{"title":"Observability","description":"What's instrumented, end to end, and what's not built yet.","href":"/operations/observability"},{"title":"Security","href":"/operations/security"}]},{"group":"Reference","pages":[{"title":"API Reference","description":"REST endpoints and the WebSocket protocol.","href":"/reference/api"}]}]},{"tab":"Learn \u0026 Interview","pages":[{"title":"Learn \u0026 Interview","href":"/learning/curriculum"}]}]},"basePath":"","title":"Security - Sportz Docs","buildId":"dpl_77Hw7ZuAABM8E3WMbYAf2ohQCr3v","deploymentId":"dpl_77Hw7ZuAABM8E3WMbYAf2ohQCr3v","dynamics":[],"chunks":[]}