Latest commit

History

128 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Oche - Game Session Dashboard

A live scoreboard for competitive-socialising sessions (darts, golf, and friends): see players and scores update in real time, edit scores, browse past matches, and watch the game video.

Built for the 501 Entertainment Cloud Developer take-home - the same domain as the 501 Hub (venue-facing live games + match history). Stack choices favour edge latency, secure multi-tenant data, and practical media handling.

Submitting? Start with SUBMISSION.md - one-page coverage of the brief, live URLs, and graded README pointers.

CICodeQLLicense: MITNodeTypeScriptCloudflare WorkersNeon Postgres

Live apphttps://oche.humza-butt.space
Staginghttps://oche-staging.humza-butt.space
APIhttps://oche-api.humza-butt.space · OpenAPI /docs
ContributingCONTRIBUTING.md · Code of Conduct · Security

Stack

Vite + React 19 SPA (TanStack Query, Tailwind v4 + shadcn) · Hono on Cloudflare Workers · Neon Postgres via Hyperdrive with Row-Level Security on every table (Drizzle) · SQLite-backed Durable Object for hibernatable WebSockets · R2 media · staging + production on Cloudflare. See ARCHITECTURE.md.

Quick start

Works on macOS, Linux, and Windows (PowerShell). No cp or psql required - use the npm scripts.

npm run setup # install + .env / .dev.vars templates# Fill DATABASE_URL (+ DATABASE_URL_STAGING for deploy) in .env, then:
npm run setup:dev-vars:sync # sync secrets into apps/api/.dev.vars (JWT + media signing for local venue switch)
npm run cf:sync:dry # preview what would upload to Cloudflare Workers + Pages
npm run cf:sync # push secrets/vars from .env → staging + production
npm run db:prepare # migrate + force-rls + seed (one command)
npm run dev # web :5173 + api :8787
npm run check # typecheck + lint + unit tests + bundle budget
npm run db:rls:check # confirm RLS is enabled + forced everywhere

First deploy to staging (full pipeline):

npm run deploy:staging:full # migrate + force-rls + rls check + seed + deploy

See docs/ENVIRONMENTS.md for staging vs production URLs and secrets.

API

MethodPathPurpose
GET/sessionslist sessions (owner-scoped, paginated)
GET/sessions/:idone session + players + recent score events
POST/sessionscreate a session with players
PATCH/sessions/:idupdate status and/or player scores (broadcasts live)
WS/sessions/:id/livelive score/status stream (Durable Object)

All bodies are Zod-validated (strict); errors return { error, issues? }. Every request resolves a principal and runs DB work inside withPrincipal() so RLS enforces isolation.


Frontend performance optimisation

Venue operators expect instant score feedback and smooth scrolling through long match histories. The SPA uses route-level code splitting (lazy routes), TanStack Query for caching, request dedupe, and optimistic score updates with rollback on error, and a virtualised history list (@tanstack/react-virtual) with cursor pagination. Player avatars use srcset where the CDN supports it, with fixed width/height to avoid layout shift, plus loading="lazy" and decoding="async". Video uses preload="metadata" until the user plays. CI enforces a bundle-size budget (npm run size). A PWA service worker caches read-only session lists for flaky venue Wi‑Fi.

Efficient handling of images & video

Game photos and session video are served from R2 on Cloudflare’s CDN (zero egress fees on R2). Objects use immutable hashed keys and long cache lifetimes where public. Player photos use responsive srcset; private uploads use signed, short-lived URLs. Video is MP4 with HTTP byte-range so seeking does not download the whole file - poster frame and metadata-only preload first. The schema supports HLS (hls_url); an offline FFmpeg script (npm run media:transcode) demonstrates transcoding without running paid Container workers in the take-home. Upload from the UI validates mime type and size.

What I'd do differently at scale

At demo scale the current design is sufficient; at 501’s venue volume I would add: keyset pagination with composite indexes (partially done); Postgres read replicas behind Hyperdrive pooling; queue-based transcoding (R2 event → Queue → FFmpeg Container, retries/DLQ, 360p/720p/HLS renditions); Durable Object sharding + hibernation for WebSocket rooms (or pub/sub fan-out); KV edge cache for hot GET /sessions/:id; structured tracing across Worker → DO → Hyperdrive → Neon with error budgets; per-IP rate limiting in a Durable Object instead of in-memory per isolate. See docs/PERFORMANCE.md and docs/SCALE.md.

Security concerns & production mitigations

501 systems handle player data across 40+ countries - the design assumes mistakes in application code and enforces isolation in the database.

  • Input validation - Zod .strict() on every body; unknown keys rejected.
  • Row-Level Security - every table RLS-enabled and forced; the app connects as a NOBYPASSRLS role; access keyed on a transaction-scoped app.current_owner, so a forgotten WHERE cannot leak another venue’s data. score_events is append-only. (Take-home: GUC from API key header. Production: JWT → Neon authenticatedRole + authUid().) See docs/RLS.md.
  • SQL injection - parameterised Drizzle queries only.
  • XSS - React escaping + strict CSP; CORS allowlisted per environment.
  • Media - signed, expiring URLs; strict mime/size validation on upload.
  • PII / GDPR - player names and photos are personal data: minimisation, retention limits, erasure path. Staging data is disposable and never seeded from production.
  • Secrets & isolation - Wrangler secrets per environment; staging and production fully isolated (Neon branch, Worker env, R2 bucket each).
  • Production hardening - real auth + RBAC, WAF, secret rotation, monitoring/alerting, backups, least-privilege Cloudflare tokens, dependency scanning. See SECURITY.md and docs/AUTH.md.

Project layout & docs

apps/web (SPA) · apps/api (Hono Worker + Durable Object) · packages/db (schema + RLS) · packages/shared (Zod + WS types) · scripts (DX) · docs/ (ADRs, diagrams, roadmap & checklists). Build it phase-by-phase with docs/PROMPTS.md.

Community:CONTRIBUTING.md · CODE_OF_CONDUCT.md · .github/SUPPORT.md · SECURITY.md

License: MIT.

About

A live scoreboard for competitive-socialising sessions (darts, golf, and friends): see players and scores update in real time, edit scores, browse past matches, and watch the game video. Built for the 501 Cloud Developer take-home.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Latest commit

History

128 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Oche - Game Session Dashboard

A live scoreboard for competitive-socialising sessions (darts, golf, and friends): see players and scores update in real time, edit scores, browse past matches, and watch the game video.

Built for the 501 Entertainment Cloud Developer take-home - the same domain as the 501 Hub (venue-facing live games + match history). Stack choices favour edge latency, secure multi-tenant data, and practical media handling.

Submitting? Start with SUBMISSION.md - one-page coverage of the brief, live URLs, and graded README pointers.

CICodeQLLicense: MITNodeTypeScriptCloudflare WorkersNeon Postgres

Live apphttps://oche.humza-butt.space
Staginghttps://oche-staging.humza-butt.space
APIhttps://oche-api.humza-butt.space · OpenAPI /docs
ContributingCONTRIBUTING.md · Code of Conduct · Security

Stack

Vite + React 19 SPA (TanStack Query, Tailwind v4 + shadcn) · Hono on Cloudflare Workers · Neon Postgres via Hyperdrive with Row-Level Security on every table (Drizzle) · SQLite-backed Durable Object for hibernatable WebSockets · R2 media · staging + production on Cloudflare. See ARCHITECTURE.md.

Quick start

Works on macOS, Linux, and Windows (PowerShell). No cp or psql required - use the npm scripts.

npm run setup # install + .env / .dev.vars templates# Fill DATABASE_URL (+ DATABASE_URL_STAGING for deploy) in .env, then:
npm run setup:dev-vars:sync # sync secrets into apps/api/.dev.vars (JWT + media signing for local venue switch)
npm run cf:sync:dry # preview what would upload to Cloudflare Workers + Pages
npm run cf:sync # push secrets/vars from .env → staging + production
npm run db:prepare # migrate + force-rls + seed (one command)
npm run dev # web :5173 + api :8787
npm run check # typecheck + lint + unit tests + bundle budget
npm run db:rls:check # confirm RLS is enabled + forced everywhere

First deploy to staging (full pipeline):

npm run deploy:staging:full # migrate + force-rls + rls check + seed + deploy

See docs/ENVIRONMENTS.md for staging vs production URLs and secrets.

API

MethodPathPurpose
GET/sessionslist sessions (owner-scoped, paginated)
GET/sessions/:idone session + players + recent score events
POST/sessionscreate a session with players
PATCH/sessions/:idupdate status and/or player scores (broadcasts live)
WS/sessions/:id/livelive score/status stream (Durable Object)

All bodies are Zod-validated (strict); errors return { error, issues? }. Every request resolves a principal and runs DB work inside withPrincipal() so RLS enforces isolation.


Frontend performance optimisation

Venue operators expect instant score feedback and smooth scrolling through long match histories. The SPA uses route-level code splitting (lazy routes), TanStack Query for caching, request dedupe, and optimistic score updates with rollback on error, and a virtualised history list (@tanstack/react-virtual) with cursor pagination. Player avatars use srcset where the CDN supports it, with fixed width/height to avoid layout shift, plus loading="lazy" and decoding="async". Video uses preload="metadata" until the user plays. CI enforces a bundle-size budget (npm run size). A PWA service worker caches read-only session lists for flaky venue Wi‑Fi.

Efficient handling of images & video

Game photos and session video are served from R2 on Cloudflare’s CDN (zero egress fees on R2). Objects use immutable hashed keys and long cache lifetimes where public. Player photos use responsive srcset; private uploads use signed, short-lived URLs. Video is MP4 with HTTP byte-range so seeking does not download the whole file - poster frame and metadata-only preload first. The schema supports HLS (hls_url); an offline FFmpeg script (npm run media:transcode) demonstrates transcoding without running paid Container workers in the take-home. Upload from the UI validates mime type and size.

What I'd do differently at scale

At demo scale the current design is sufficient; at 501’s venue volume I would add: keyset pagination with composite indexes (partially done); Postgres read replicas behind Hyperdrive pooling; queue-based transcoding (R2 event → Queue → FFmpeg Container, retries/DLQ, 360p/720p/HLS renditions); Durable Object sharding + hibernation for WebSocket rooms (or pub/sub fan-out); KV edge cache for hot GET /sessions/:id; structured tracing across Worker → DO → Hyperdrive → Neon with error budgets; per-IP rate limiting in a Durable Object instead of in-memory per isolate. See docs/PERFORMANCE.md and docs/SCALE.md.

Security concerns & production mitigations

501 systems handle player data across 40+ countries - the design assumes mistakes in application code and enforces isolation in the database.

  • Input validation - Zod .strict() on every body; unknown keys rejected.
  • Row-Level Security - every table RLS-enabled and forced; the app connects as a NOBYPASSRLS role; access keyed on a transaction-scoped app.current_owner, so a forgotten WHERE cannot leak another venue’s data. score_events is append-only. (Take-home: GUC from API key header. Production: JWT → Neon authenticatedRole + authUid().) See docs/RLS.md.
  • SQL injection - parameterised Drizzle queries only.
  • XSS - React escaping + strict CSP; CORS allowlisted per environment.
  • Media - signed, expiring URLs; strict mime/size validation on upload.
  • PII / GDPR - player names and photos are personal data: minimisation, retention limits, erasure path. Staging data is disposable and never seeded from production.
  • Secrets & isolation - Wrangler secrets per environment; staging and production fully isolated (Neon branch, Worker env, R2 bucket each).
  • Production hardening - real auth + RBAC, WAF, secret rotation, monitoring/alerting, backups, least-privilege Cloudflare tokens, dependency scanning. See SECURITY.md and docs/AUTH.md.

Project layout & docs

apps/web (SPA) · apps/api (Hono Worker + Durable Object) · packages/db (schema + RLS) · packages/shared (Zod + WS types) · scripts (DX) · docs/ (ADRs, diagrams, roadmap & checklists). Build it phase-by-phase with docs/PROMPTS.md.

Community:CONTRIBUTING.md · CODE_OF_CONDUCT.md · .github/SUPPORT.md · SECURITY.md

License: MIT.

About

A live scoreboard for competitive-socialising sessions (darts, golf, and friends): see players and scores update in real time, edit scores, browse past matches, and watch the game video. Built for the 501 Cloud Developer take-home.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

128 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Oche - Game Session Dashboard

A live scoreboard for competitive-socialising sessions (darts, golf, and friends): see players and scores update in real time, edit scores, browse past matches, and watch the game video.

Built for the 501 Entertainment Cloud Developer take-home - the same domain as the 501 Hub (venue-facing live games + match history). Stack choices favour edge latency, secure multi-tenant data, and practical media handling.

Submitting? Start with SUBMISSION.md - one-page coverage of the brief, live URLs, and graded README pointers.

CICodeQLLicense: MITNodeTypeScriptCloudflare WorkersNeon Postgres

Live apphttps://oche.humza-butt.space
Staginghttps://oche-staging.humza-butt.space
APIhttps://oche-api.humza-butt.space · OpenAPI /docs
ContributingCONTRIBUTING.md · Code of Conduct · Security

Stack

Vite + React 19 SPA (TanStack Query, Tailwind v4 + shadcn) · Hono on Cloudflare Workers · Neon Postgres via Hyperdrive with Row-Level Security on every table (Drizzle) · SQLite-backed Durable Object for hibernatable WebSockets · R2 media · staging + production on Cloudflare. See ARCHITECTURE.md.

Quick start

Works on macOS, Linux, and Windows (PowerShell). No cp or psql required - use the npm scripts.

npm run setup # install + .env / .dev.vars templates# Fill DATABASE_URL (+ DATABASE_URL_STAGING for deploy) in .env, then:
npm run setup:dev-vars:sync # sync secrets into apps/api/.dev.vars (JWT + media signing for local venue switch)
npm run cf:sync:dry # preview what would upload to Cloudflare Workers + Pages
npm run cf:sync # push secrets/vars from .env → staging + production
npm run db:prepare # migrate + force-rls + seed (one command)
npm run dev # web :5173 + api :8787
npm run check # typecheck + lint + unit tests + bundle budget
npm run db:rls:check # confirm RLS is enabled + forced everywhere

First deploy to staging (full pipeline):

npm run deploy:staging:full # migrate + force-rls + rls check + seed + deploy

See docs/ENVIRONMENTS.md for staging vs production URLs and secrets.

API

MethodPathPurpose
GET/sessionslist sessions (owner-scoped, paginated)
GET/sessions/:idone session + players + recent score events
POST/sessionscreate a session with players
PATCH/sessions/:idupdate status and/or player scores (broadcasts live)
WS/sessions/:id/livelive score/status stream (Durable Object)

All bodies are Zod-validated (strict); errors return { error, issues? }. Every request resolves a principal and runs DB work inside withPrincipal() so RLS enforces isolation.


Frontend performance optimisation

Venue operators expect instant score feedback and smooth scrolling through long match histories. The SPA uses route-level code splitting (lazy routes), TanStack Query for caching, request dedupe, and optimistic score updates with rollback on error, and a virtualised history list (@tanstack/react-virtual) with cursor pagination. Player avatars use srcset where the CDN supports it, with fixed width/height to avoid layout shift, plus loading="lazy" and decoding="async". Video uses preload="metadata" until the user plays. CI enforces a bundle-size budget (npm run size). A PWA service worker caches read-only session lists for flaky venue Wi‑Fi.

Efficient handling of images & video

Game photos and session video are served from R2 on Cloudflare’s CDN (zero egress fees on R2). Objects use immutable hashed keys and long cache lifetimes where public. Player photos use responsive srcset; private uploads use signed, short-lived URLs. Video is MP4 with HTTP byte-range so seeking does not download the whole file - poster frame and metadata-only preload first. The schema supports HLS (hls_url); an offline FFmpeg script (npm run media:transcode) demonstrates transcoding without running paid Container workers in the take-home. Upload from the UI validates mime type and size.

What I'd do differently at scale

At demo scale the current design is sufficient; at 501’s venue volume I would add: keyset pagination with composite indexes (partially done); Postgres read replicas behind Hyperdrive pooling; queue-based transcoding (R2 event → Queue → FFmpeg Container, retries/DLQ, 360p/720p/HLS renditions); Durable Object sharding + hibernation for WebSocket rooms (or pub/sub fan-out); KV edge cache for hot GET /sessions/:id; structured tracing across Worker → DO → Hyperdrive → Neon with error budgets; per-IP rate limiting in a Durable Object instead of in-memory per isolate. See docs/PERFORMANCE.md and docs/SCALE.md.

Security concerns & production mitigations

501 systems handle player data across 40+ countries - the design assumes mistakes in application code and enforces isolation in the database.

  • Input validation - Zod .strict() on every body; unknown keys rejected.
  • Row-Level Security - every table RLS-enabled and forced; the app connects as a NOBYPASSRLS role; access keyed on a transaction-scoped app.current_owner, so a forgotten WHERE cannot leak another venue’s data. score_events is append-only. (Take-home: GUC from API key header. Production: JWT → Neon authenticatedRole + authUid().) See docs/RLS.md.
  • SQL injection - parameterised Drizzle queries only.
  • XSS - React escaping + strict CSP; CORS allowlisted per environment.
  • Media - signed, expiring URLs; strict mime/size validation on upload.
  • PII / GDPR - player names and photos are personal data: minimisation, retention limits, erasure path. Staging data is disposable and never seeded from production.
  • Secrets & isolation - Wrangler secrets per environment; staging and production fully isolated (Neon branch, Worker env, R2 bucket each).
  • Production hardening - real auth + RBAC, WAF, secret rotation, monitoring/alerting, backups, least-privilege Cloudflare tokens, dependency scanning. See SECURITY.md and docs/AUTH.md.

Project layout & docs

apps/web (SPA) · apps/api (Hono Worker + Durable Object) · packages/db (schema + RLS) · packages/shared (Zod + WS types) · scripts (DX) · docs/ (ADRs, diagrams, roadmap & checklists). Build it phase-by-phase with docs/PROMPTS.md.

Community:CONTRIBUTING.md · CODE_OF_CONDUCT.md · .github/SUPPORT.md · SECURITY.md

License: MIT.

About

A live scoreboard for competitive-socialising sessions (darts, golf, and friends): see players and scores update in real time, edit scores, browse past matches, and watch the game video. Built for the 501 Cloud Developer take-home.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

128 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Oche - Game Session Dashboard

A live scoreboard for competitive-socialising sessions (darts, golf, and friends): see players and scores update in real time, edit scores, browse past matches, and watch the game video.

Built for the 501 Entertainment Cloud Developer take-home - the same domain as the 501 Hub (venue-facing live games + match history). Stack choices favour edge latency, secure multi-tenant data, and practical media handling.

Submitting? Start with SUBMISSION.md - one-page coverage of the brief, live URLs, and graded README pointers.

CICodeQLLicense: MITNodeTypeScriptCloudflare WorkersNeon Postgres

Live apphttps://oche.humza-butt.space
Staginghttps://oche-staging.humza-butt.space
APIhttps://oche-api.humza-butt.space · OpenAPI /docs
ContributingCONTRIBUTING.md · Code of Conduct · Security

Stack

Vite + React 19 SPA (TanStack Query, Tailwind v4 + shadcn) · Hono on Cloudflare Workers · Neon Postgres via Hyperdrive with Row-Level Security on every table (Drizzle) · SQLite-backed Durable Object for hibernatable WebSockets · R2 media · staging + production on Cloudflare. See ARCHITECTURE.md.

Quick start

Works on macOS, Linux, and Windows (PowerShell). No cp or psql required - use the npm scripts.

npm run setup # install + .env / .dev.vars templates# Fill DATABASE_URL (+ DATABASE_URL_STAGING for deploy) in .env, then:
npm run setup:dev-vars:sync # sync secrets into apps/api/.dev.vars (JWT + media signing for local venue switch)
npm run cf:sync:dry # preview what would upload to Cloudflare Workers + Pages
npm run cf:sync # push secrets/vars from .env → staging + production
npm run db:prepare # migrate + force-rls + seed (one command)
npm run dev # web :5173 + api :8787
npm run check # typecheck + lint + unit tests + bundle budget
npm run db:rls:check # confirm RLS is enabled + forced everywhere

First deploy to staging (full pipeline):

npm run deploy:staging:full # migrate + force-rls + rls check + seed + deploy

See docs/ENVIRONMENTS.md for staging vs production URLs and secrets.

API

MethodPathPurpose
GET/sessionslist sessions (owner-scoped, paginated)
GET/sessions/:idone session + players + recent score events
POST/sessionscreate a session with players
PATCH/sessions/:idupdate status and/or player scores (broadcasts live)
WS/sessions/:id/livelive score/status stream (Durable Object)

All bodies are Zod-validated (strict); errors return { error, issues? }. Every request resolves a principal and runs DB work inside withPrincipal() so RLS enforces isolation.


Frontend performance optimisation

Venue operators expect instant score feedback and smooth scrolling through long match histories. The SPA uses route-level code splitting (lazy routes), TanStack Query for caching, request dedupe, and optimistic score updates with rollback on error, and a virtualised history list (@tanstack/react-virtual) with cursor pagination. Player avatars use srcset where the CDN supports it, with fixed width/height to avoid layout shift, plus loading="lazy" and decoding="async". Video uses preload="metadata" until the user plays. CI enforces a bundle-size budget (npm run size). A PWA service worker caches read-only session lists for flaky venue Wi‑Fi.

Efficient handling of images & video

Game photos and session video are served from R2 on Cloudflare’s CDN (zero egress fees on R2). Objects use immutable hashed keys and long cache lifetimes where public. Player photos use responsive srcset; private uploads use signed, short-lived URLs. Video is MP4 with HTTP byte-range so seeking does not download the whole file - poster frame and metadata-only preload first. The schema supports HLS (hls_url); an offline FFmpeg script (npm run media:transcode) demonstrates transcoding without running paid Container workers in the take-home. Upload from the UI validates mime type and size.

What I'd do differently at scale

At demo scale the current design is sufficient; at 501’s venue volume I would add: keyset pagination with composite indexes (partially done); Postgres read replicas behind Hyperdrive pooling; queue-based transcoding (R2 event → Queue → FFmpeg Container, retries/DLQ, 360p/720p/HLS renditions); Durable Object sharding + hibernation for WebSocket rooms (or pub/sub fan-out); KV edge cache for hot GET /sessions/:id; structured tracing across Worker → DO → Hyperdrive → Neon with error budgets; per-IP rate limiting in a Durable Object instead of in-memory per isolate. See docs/PERFORMANCE.md and docs/SCALE.md.

Security concerns & production mitigations

501 systems handle player data across 40+ countries - the design assumes mistakes in application code and enforces isolation in the database.

  • Input validation - Zod .strict() on every body; unknown keys rejected.
  • Row-Level Security - every table RLS-enabled and forced; the app connects as a NOBYPASSRLS role; access keyed on a transaction-scoped app.current_owner, so a forgotten WHERE cannot leak another venue’s data. score_events is append-only. (Take-home: GUC from API key header. Production: JWT → Neon authenticatedRole + authUid().) See docs/RLS.md.
  • SQL injection - parameterised Drizzle queries only.
  • XSS - React escaping + strict CSP; CORS allowlisted per environment.
  • Media - signed, expiring URLs; strict mime/size validation on upload.
  • PII / GDPR - player names and photos are personal data: minimisation, retention limits, erasure path. Staging data is disposable and never seeded from production.
  • Secrets & isolation - Wrangler secrets per environment; staging and production fully isolated (Neon branch, Worker env, R2 bucket each).
  • Production hardening - real auth + RBAC, WAF, secret rotation, monitoring/alerting, backups, least-privilege Cloudflare tokens, dependency scanning. See SECURITY.md and docs/AUTH.md.

Project layout & docs

apps/web (SPA) · apps/api (Hono Worker + Durable Object) · packages/db (schema + RLS) · packages/shared (Zod + WS types) · scripts (DX) · docs/ (ADRs, diagrams, roadmap & checklists). Build it phase-by-phase with docs/PROMPTS.md.

Community:CONTRIBUTING.md · CODE_OF_CONDUCT.md · .github/SUPPORT.md · SECURITY.md

License: MIT.

About

A live scoreboard for competitive-socialising sessions (darts, golf, and friends): see players and scores update in real time, edit scores, browse past matches, and watch the game video. Built for the 501 Cloud Developer take-home.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Latest commit

History

128 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Oche - Game Session Dashboard

A live scoreboard for competitive-socialising sessions (darts, golf, and friends): see players and scores update in real time, edit scores, browse past matches, and watch the game video.

Built for the 501 Entertainment Cloud Developer take-home - the same domain as the 501 Hub (venue-facing live games + match history). Stack choices favour edge latency, secure multi-tenant data, and practical media handling.

Submitting? Start with SUBMISSION.md - one-page coverage of the brief, live URLs, and graded README pointers.

CICodeQLLicense: MITNodeTypeScriptCloudflare WorkersNeon Postgres

Live apphttps://oche.humza-butt.space
Staginghttps://oche-staging.humza-butt.space
APIhttps://oche-api.humza-butt.space · OpenAPI /docs
ContributingCONTRIBUTING.md · Code of Conduct · Security

Stack

Vite + React 19 SPA (TanStack Query, Tailwind v4 + shadcn) · Hono on Cloudflare Workers · Neon Postgres via Hyperdrive with Row-Level Security on every table (Drizzle) · SQLite-backed Durable Object for hibernatable WebSockets · R2 media · staging + production on Cloudflare. See ARCHITECTURE.md.

Quick start

Works on macOS, Linux, and Windows (PowerShell). No cp or psql required - use the npm scripts.

npm run setup # install + .env / .dev.vars templates# Fill DATABASE_URL (+ DATABASE_URL_STAGING for deploy) in .env, then:
npm run setup:dev-vars:sync # sync secrets into apps/api/.dev.vars (JWT + media signing for local venue switch)
npm run cf:sync:dry # preview what would upload to Cloudflare Workers + Pages
npm run cf:sync # push secrets/vars from .env → staging + production
npm run db:prepare # migrate + force-rls + seed (one command)
npm run dev # web :5173 + api :8787
npm run check # typecheck + lint + unit tests + bundle budget
npm run db:rls:check # confirm RLS is enabled + forced everywhere

First deploy to staging (full pipeline):

npm run deploy:staging:full # migrate + force-rls + rls check + seed + deploy

See docs/ENVIRONMENTS.md for staging vs production URLs and secrets.

API

MethodPathPurpose
GET/sessionslist sessions (owner-scoped, paginated)
GET/sessions/:idone session + players + recent score events
POST/sessionscreate a session with players
PATCH/sessions/:idupdate status and/or player scores (broadcasts live)
WS/sessions/:id/livelive score/status stream (Durable Object)

All bodies are Zod-validated (strict); errors return { error, issues? }. Every request resolves a principal and runs DB work inside withPrincipal() so RLS enforces isolation.


Frontend performance optimisation

Venue operators expect instant score feedback and smooth scrolling through long match histories. The SPA uses route-level code splitting (lazy routes), TanStack Query for caching, request dedupe, and optimistic score updates with rollback on error, and a virtualised history list (@tanstack/react-virtual) with cursor pagination. Player avatars use srcset where the CDN supports it, with fixed width/height to avoid layout shift, plus loading="lazy" and decoding="async". Video uses preload="metadata" until the user plays. CI enforces a bundle-size budget (npm run size). A PWA service worker caches read-only session lists for flaky venue Wi‑Fi.

Efficient handling of images & video

Game photos and session video are served from R2 on Cloudflare’s CDN (zero egress fees on R2). Objects use immutable hashed keys and long cache lifetimes where public. Player photos use responsive srcset; private uploads use signed, short-lived URLs. Video is MP4 with HTTP byte-range so seeking does not download the whole file - poster frame and metadata-only preload first. The schema supports HLS (hls_url); an offline FFmpeg script (npm run media:transcode) demonstrates transcoding without running paid Container workers in the take-home. Upload from the UI validates mime type and size.

What I'd do differently at scale

At demo scale the current design is sufficient; at 501’s venue volume I would add: keyset pagination with composite indexes (partially done); Postgres read replicas behind Hyperdrive pooling; queue-based transcoding (R2 event → Queue → FFmpeg Container, retries/DLQ, 360p/720p/HLS renditions); Durable Object sharding + hibernation for WebSocket rooms (or pub/sub fan-out); KV edge cache for hot GET /sessions/:id; structured tracing across Worker → DO → Hyperdrive → Neon with error budgets; per-IP rate limiting in a Durable Object instead of in-memory per isolate. See docs/PERFORMANCE.md and docs/SCALE.md.

Security concerns & production mitigations

501 systems handle player data across 40+ countries - the design assumes mistakes in application code and enforces isolation in the database.

  • Input validation - Zod .strict() on every body; unknown keys rejected.
  • Row-Level Security - every table RLS-enabled and forced; the app connects as a NOBYPASSRLS role; access keyed on a transaction-scoped app.current_owner, so a forgotten WHERE cannot leak another venue’s data. score_events is append-only. (Take-home: GUC from API key header. Production: JWT → Neon authenticatedRole + authUid().) See docs/RLS.md.
  • SQL injection - parameterised Drizzle queries only.
  • XSS - React escaping + strict CSP; CORS allowlisted per environment.
  • Media - signed, expiring URLs; strict mime/size validation on upload.
  • PII / GDPR - player names and photos are personal data: minimisation, retention limits, erasure path. Staging data is disposable and never seeded from production.
  • Secrets & isolation - Wrangler secrets per environment; staging and production fully isolated (Neon branch, Worker env, R2 bucket each).
  • Production hardening - real auth + RBAC, WAF, secret rotation, monitoring/alerting, backups, least-privilege Cloudflare tokens, dependency scanning. See SECURITY.md and docs/AUTH.md.

Project layout & docs

apps/web (SPA) · apps/api (Hono Worker + Durable Object) · packages/db (schema + RLS) · packages/shared (Zod + WS types) · scripts (DX) · docs/ (ADRs, diagrams, roadmap & checklists). Build it phase-by-phase with docs/PROMPTS.md.

Community:CONTRIBUTING.md · CODE_OF_CONDUCT.md · .github/SUPPORT.md · SECURITY.md

License: MIT.

About

A live scoreboard for competitive-socialising sessions (darts, golf, and friends): see players and scores update in real time, edit scores, browse past matches, and watch the game video. Built for the 501 Cloud Developer take-home.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

128 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Oche - Game Session Dashboard

A live scoreboard for competitive-socialising sessions (darts, golf, and friends): see players and scores update in real time, edit scores, browse past matches, and watch the game video.

Built for the 501 Entertainment Cloud Developer take-home - the same domain as the 501 Hub (venue-facing live games + match history). Stack choices favour edge latency, secure multi-tenant data, and practical media handling.

Submitting? Start with SUBMISSION.md - one-page coverage of the brief, live URLs, and graded README pointers.

CICodeQLLicense: MITNodeTypeScriptCloudflare WorkersNeon Postgres

Live apphttps://oche.humza-butt.space
Staginghttps://oche-staging.humza-butt.space
APIhttps://oche-api.humza-butt.space · OpenAPI /docs
ContributingCONTRIBUTING.md · Code of Conduct · Security

Stack

Vite + React 19 SPA (TanStack Query, Tailwind v4 + shadcn) · Hono on Cloudflare Workers · Neon Postgres via Hyperdrive with Row-Level Security on every table (Drizzle) · SQLite-backed Durable Object for hibernatable WebSockets · R2 media · staging + production on Cloudflare. See ARCHITECTURE.md.

Quick start

Works on macOS, Linux, and Windows (PowerShell). No cp or psql required - use the npm scripts.

npm run setup # install + .env / .dev.vars templates# Fill DATABASE_URL (+ DATABASE_URL_STAGING for deploy) in .env, then:
npm run setup:dev-vars:sync # sync secrets into apps/api/.dev.vars (JWT + media signing for local venue switch)
npm run cf:sync:dry # preview what would upload to Cloudflare Workers + Pages
npm run cf:sync # push secrets/vars from .env → staging + production
npm run db:prepare # migrate + force-rls + seed (one command)
npm run dev # web :5173 + api :8787
npm run check # typecheck + lint + unit tests + bundle budget
npm run db:rls:check # confirm RLS is enabled + forced everywhere

First deploy to staging (full pipeline):

npm run deploy:staging:full # migrate + force-rls + rls check + seed + deploy

See docs/ENVIRONMENTS.md for staging vs production URLs and secrets.

API

MethodPathPurpose
GET/sessionslist sessions (owner-scoped, paginated)
GET/sessions/:idone session + players + recent score events
POST/sessionscreate a session with players
PATCH/sessions/:idupdate status and/or player scores (broadcasts live)
WS/sessions/:id/livelive score/status stream (Durable Object)

All bodies are Zod-validated (strict); errors return { error, issues? }. Every request resolves a principal and runs DB work inside withPrincipal() so RLS enforces isolation.


Frontend performance optimisation

Venue operators expect instant score feedback and smooth scrolling through long match histories. The SPA uses route-level code splitting (lazy routes), TanStack Query for caching, request dedupe, and optimistic score updates with rollback on error, and a virtualised history list (@tanstack/react-virtual) with cursor pagination. Player avatars use srcset where the CDN supports it, with fixed width/height to avoid layout shift, plus loading="lazy" and decoding="async". Video uses preload="metadata" until the user plays. CI enforces a bundle-size budget (npm run size). A PWA service worker caches read-only session lists for flaky venue Wi‑Fi.

Efficient handling of images & video

Game photos and session video are served from R2 on Cloudflare’s CDN (zero egress fees on R2). Objects use immutable hashed keys and long cache lifetimes where public. Player photos use responsive srcset; private uploads use signed, short-lived URLs. Video is MP4 with HTTP byte-range so seeking does not download the whole file - poster frame and metadata-only preload first. The schema supports HLS (hls_url); an offline FFmpeg script (npm run media:transcode) demonstrates transcoding without running paid Container workers in the take-home. Upload from the UI validates mime type and size.

What I'd do differently at scale

At demo scale the current design is sufficient; at 501’s venue volume I would add: keyset pagination with composite indexes (partially done); Postgres read replicas behind Hyperdrive pooling; queue-based transcoding (R2 event → Queue → FFmpeg Container, retries/DLQ, 360p/720p/HLS renditions); Durable Object sharding + hibernation for WebSocket rooms (or pub/sub fan-out); KV edge cache for hot GET /sessions/:id; structured tracing across Worker → DO → Hyperdrive → Neon with error budgets; per-IP rate limiting in a Durable Object instead of in-memory per isolate. See docs/PERFORMANCE.md and docs/SCALE.md.

Security concerns & production mitigations

501 systems handle player data across 40+ countries - the design assumes mistakes in application code and enforces isolation in the database.

  • Input validation - Zod .strict() on every body; unknown keys rejected.
  • Row-Level Security - every table RLS-enabled and forced; the app connects as a NOBYPASSRLS role; access keyed on a transaction-scoped app.current_owner, so a forgotten WHERE cannot leak another venue’s data. score_events is append-only. (Take-home: GUC from API key header. Production: JWT → Neon authenticatedRole + authUid().) See docs/RLS.md.
  • SQL injection - parameterised Drizzle queries only.
  • XSS - React escaping + strict CSP; CORS allowlisted per environment.
  • Media - signed, expiring URLs; strict mime/size validation on upload.
  • PII / GDPR - player names and photos are personal data: minimisation, retention limits, erasure path. Staging data is disposable and never seeded from production.
  • Secrets & isolation - Wrangler secrets per environment; staging and production fully isolated (Neon branch, Worker env, R2 bucket each).
  • Production hardening - real auth + RBAC, WAF, secret rotation, monitoring/alerting, backups, least-privilege Cloudflare tokens, dependency scanning. See SECURITY.md and docs/AUTH.md.

Project layout & docs

apps/web (SPA) · apps/api (Hono Worker + Durable Object) · packages/db (schema + RLS) · packages/shared (Zod + WS types) · scripts (DX) · docs/ (ADRs, diagrams, roadmap & checklists). Build it phase-by-phase with docs/PROMPTS.md.

Community:CONTRIBUTING.md · CODE_OF_CONDUCT.md · .github/SUPPORT.md · SECURITY.md

License: MIT.

About

A live scoreboard for competitive-socialising sessions (darts, golf, and friends): see players and scores update in real time, edit scores, browse past matches, and watch the game video. Built for the 501 Cloud Developer take-home.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

128 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Oche - Game Session Dashboard

A live scoreboard for competitive-socialising sessions (darts, golf, and friends): see players and scores update in real time, edit scores, browse past matches, and watch the game video.

Built for the 501 Entertainment Cloud Developer take-home - the same domain as the 501 Hub (venue-facing live games + match history). Stack choices favour edge latency, secure multi-tenant data, and practical media handling.

Submitting? Start with SUBMISSION.md - one-page coverage of the brief, live URLs, and graded README pointers.

CICodeQLLicense: MITNodeTypeScriptCloudflare WorkersNeon Postgres

Live apphttps://oche.humza-butt.space
Staginghttps://oche-staging.humza-butt.space
APIhttps://oche-api.humza-butt.space · OpenAPI /docs
ContributingCONTRIBUTING.md · Code of Conduct · Security

Stack

Vite + React 19 SPA (TanStack Query, Tailwind v4 + shadcn) · Hono on Cloudflare Workers · Neon Postgres via Hyperdrive with Row-Level Security on every table (Drizzle) · SQLite-backed Durable Object for hibernatable WebSockets · R2 media · staging + production on Cloudflare. See ARCHITECTURE.md.

Quick start

Works on macOS, Linux, and Windows (PowerShell). No cp or psql required - use the npm scripts.

npm run setup # install + .env / .dev.vars templates# Fill DATABASE_URL (+ DATABASE_URL_STAGING for deploy) in .env, then:
npm run setup:dev-vars:sync # sync secrets into apps/api/.dev.vars (JWT + media signing for local venue switch)
npm run cf:sync:dry # preview what would upload to Cloudflare Workers + Pages
npm run cf:sync # push secrets/vars from .env → staging + production
npm run db:prepare # migrate + force-rls + seed (one command)
npm run dev # web :5173 + api :8787
npm run check # typecheck + lint + unit tests + bundle budget
npm run db:rls:check # confirm RLS is enabled + forced everywhere

First deploy to staging (full pipeline):

npm run deploy:staging:full # migrate + force-rls + rls check + seed + deploy

See docs/ENVIRONMENTS.md for staging vs production URLs and secrets.

API

MethodPathPurpose
GET/sessionslist sessions (owner-scoped, paginated)
GET/sessions/:idone session + players + recent score events
POST/sessionscreate a session with players
PATCH/sessions/:idupdate status and/or player scores (broadcasts live)
WS/sessions/:id/livelive score/status stream (Durable Object)

All bodies are Zod-validated (strict); errors return { error, issues? }. Every request resolves a principal and runs DB work inside withPrincipal() so RLS enforces isolation.


Frontend performance optimisation

Venue operators expect instant score feedback and smooth scrolling through long match histories. The SPA uses route-level code splitting (lazy routes), TanStack Query for caching, request dedupe, and optimistic score updates with rollback on error, and a virtualised history list (@tanstack/react-virtual) with cursor pagination. Player avatars use srcset where the CDN supports it, with fixed width/height to avoid layout shift, plus loading="lazy" and decoding="async". Video uses preload="metadata" until the user plays. CI enforces a bundle-size budget (npm run size). A PWA service worker caches read-only session lists for flaky venue Wi‑Fi.

Efficient handling of images & video

Game photos and session video are served from R2 on Cloudflare’s CDN (zero egress fees on R2). Objects use immutable hashed keys and long cache lifetimes where public. Player photos use responsive srcset; private uploads use signed, short-lived URLs. Video is MP4 with HTTP byte-range so seeking does not download the whole file - poster frame and metadata-only preload first. The schema supports HLS (hls_url); an offline FFmpeg script (npm run media:transcode) demonstrates transcoding without running paid Container workers in the take-home. Upload from the UI validates mime type and size.

What I'd do differently at scale

At demo scale the current design is sufficient; at 501’s venue volume I would add: keyset pagination with composite indexes (partially done); Postgres read replicas behind Hyperdrive pooling; queue-based transcoding (R2 event → Queue → FFmpeg Container, retries/DLQ, 360p/720p/HLS renditions); Durable Object sharding + hibernation for WebSocket rooms (or pub/sub fan-out); KV edge cache for hot GET /sessions/:id; structured tracing across Worker → DO → Hyperdrive → Neon with error budgets; per-IP rate limiting in a Durable Object instead of in-memory per isolate. See docs/PERFORMANCE.md and docs/SCALE.md.

Security concerns & production mitigations

501 systems handle player data across 40+ countries - the design assumes mistakes in application code and enforces isolation in the database.

  • Input validation - Zod .strict() on every body; unknown keys rejected.
  • Row-Level Security - every table RLS-enabled and forced; the app connects as a NOBYPASSRLS role; access keyed on a transaction-scoped app.current_owner, so a forgotten WHERE cannot leak another venue’s data. score_events is append-only. (Take-home: GUC from API key header. Production: JWT → Neon authenticatedRole + authUid().) See docs/RLS.md.
  • SQL injection - parameterised Drizzle queries only.
  • XSS - React escaping + strict CSP; CORS allowlisted per environment.
  • Media - signed, expiring URLs; strict mime/size validation on upload.
  • PII / GDPR - player names and photos are personal data: minimisation, retention limits, erasure path. Staging data is disposable and never seeded from production.
  • Secrets & isolation - Wrangler secrets per environment; staging and production fully isolated (Neon branch, Worker env, R2 bucket each).
  • Production hardening - real auth + RBAC, WAF, secret rotation, monitoring/alerting, backups, least-privilege Cloudflare tokens, dependency scanning. See SECURITY.md and docs/AUTH.md.

Project layout & docs

apps/web (SPA) · apps/api (Hono Worker + Durable Object) · packages/db (schema + RLS) · packages/shared (Zod + WS types) · scripts (DX) · docs/ (ADRs, diagrams, roadmap & checklists). Build it phase-by-phase with docs/PROMPTS.md.

Community:CONTRIBUTING.md · CODE_OF_CONDUCT.md · .github/SUPPORT.md · SECURITY.md

License: MIT.

About

A live scoreboard for competitive-socialising sessions (darts, golf, and friends): see players and scores update in real time, edit scores, browse past matches, and watch the game video. Built for the 501 Cloud Developer take-home.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Latest commit

History

128 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Oche - Game Session Dashboard

A live scoreboard for competitive-socialising sessions (darts, golf, and friends): see players and scores update in real time, edit scores, browse past matches, and watch the game video.

Built for the 501 Entertainment Cloud Developer take-home - the same domain as the 501 Hub (venue-facing live games + match history). Stack choices favour edge latency, secure multi-tenant data, and practical media handling.

Submitting? Start with SUBMISSION.md - one-page coverage of the brief, live URLs, and graded README pointers.

CICodeQLLicense: MITNodeTypeScriptCloudflare WorkersNeon Postgres

Live apphttps://oche.humza-butt.space
Staginghttps://oche-staging.humza-butt.space
APIhttps://oche-api.humza-butt.space · OpenAPI /docs
ContributingCONTRIBUTING.md · Code of Conduct · Security

Stack

Vite + React 19 SPA (TanStack Query, Tailwind v4 + shadcn) · Hono on Cloudflare Workers · Neon Postgres via Hyperdrive with Row-Level Security on every table (Drizzle) · SQLite-backed Durable Object for hibernatable WebSockets · R2 media · staging + production on Cloudflare. See ARCHITECTURE.md.

Quick start

Works on macOS, Linux, and Windows (PowerShell). No cp or psql required - use the npm scripts.

npm run setup # install + .env / .dev.vars templates# Fill DATABASE_URL (+ DATABASE_URL_STAGING for deploy) in .env, then:
npm run setup:dev-vars:sync # sync secrets into apps/api/.dev.vars (JWT + media signing for local venue switch)
npm run cf:sync:dry # preview what would upload to Cloudflare Workers + Pages
npm run cf:sync # push secrets/vars from .env → staging + production
npm run db:prepare # migrate + force-rls + seed (one command)
npm run dev # web :5173 + api :8787
npm run check # typecheck + lint + unit tests + bundle budget
npm run db:rls:check # confirm RLS is enabled + forced everywhere

First deploy to staging (full pipeline):

npm run deploy:staging:full # migrate + force-rls + rls check + seed + deploy

See docs/ENVIRONMENTS.md for staging vs production URLs and secrets.

API

MethodPathPurpose
GET/sessionslist sessions (owner-scoped, paginated)
GET/sessions/:idone session + players + recent score events
POST/sessionscreate a session with players
PATCH/sessions/:idupdate status and/or player scores (broadcasts live)
WS/sessions/:id/livelive score/status stream (Durable Object)

All bodies are Zod-validated (strict); errors return { error, issues? }. Every request resolves a principal and runs DB work inside withPrincipal() so RLS enforces isolation.


Frontend performance optimisation

Venue operators expect instant score feedback and smooth scrolling through long match histories. The SPA uses route-level code splitting (lazy routes), TanStack Query for caching, request dedupe, and optimistic score updates with rollback on error, and a virtualised history list (@tanstack/react-virtual) with cursor pagination. Player avatars use srcset where the CDN supports it, with fixed width/height to avoid layout shift, plus loading="lazy" and decoding="async". Video uses preload="metadata" until the user plays. CI enforces a bundle-size budget (npm run size). A PWA service worker caches read-only session lists for flaky venue Wi‑Fi.

Efficient handling of images & video

Game photos and session video are served from R2 on Cloudflare’s CDN (zero egress fees on R2). Objects use immutable hashed keys and long cache lifetimes where public. Player photos use responsive srcset; private uploads use signed, short-lived URLs. Video is MP4 with HTTP byte-range so seeking does not download the whole file - poster frame and metadata-only preload first. The schema supports HLS (hls_url); an offline FFmpeg script (npm run media:transcode) demonstrates transcoding without running paid Container workers in the take-home. Upload from the UI validates mime type and size.

What I'd do differently at scale

At demo scale the current design is sufficient; at 501’s venue volume I would add: keyset pagination with composite indexes (partially done); Postgres read replicas behind Hyperdrive pooling; queue-based transcoding (R2 event → Queue → FFmpeg Container, retries/DLQ, 360p/720p/HLS renditions); Durable Object sharding + hibernation for WebSocket rooms (or pub/sub fan-out); KV edge cache for hot GET /sessions/:id; structured tracing across Worker → DO → Hyperdrive → Neon with error budgets; per-IP rate limiting in a Durable Object instead of in-memory per isolate. See docs/PERFORMANCE.md and docs/SCALE.md.

Security concerns & production mitigations

501 systems handle player data across 40+ countries - the design assumes mistakes in application code and enforces isolation in the database.

  • Input validation - Zod .strict() on every body; unknown keys rejected.
  • Row-Level Security - every table RLS-enabled and forced; the app connects as a NOBYPASSRLS role; access keyed on a transaction-scoped app.current_owner, so a forgotten WHERE cannot leak another venue’s data. score_events is append-only. (Take-home: GUC from API key header. Production: JWT → Neon authenticatedRole + authUid().) See docs/RLS.md.
  • SQL injection - parameterised Drizzle queries only.
  • XSS - React escaping + strict CSP; CORS allowlisted per environment.
  • Media - signed, expiring URLs; strict mime/size validation on upload.
  • PII / GDPR - player names and photos are personal data: minimisation, retention limits, erasure path. Staging data is disposable and never seeded from production.
  • Secrets & isolation - Wrangler secrets per environment; staging and production fully isolated (Neon branch, Worker env, R2 bucket each).
  • Production hardening - real auth + RBAC, WAF, secret rotation, monitoring/alerting, backups, least-privilege Cloudflare tokens, dependency scanning. See SECURITY.md and docs/AUTH.md.

Project layout & docs

apps/web (SPA) · apps/api (Hono Worker + Durable Object) · packages/db (schema + RLS) · packages/shared (Zod + WS types) · scripts (DX) · docs/ (ADRs, diagrams, roadmap & checklists). Build it phase-by-phase with docs/PROMPTS.md.

Community:CONTRIBUTING.md · CODE_OF_CONDUCT.md · .github/SUPPORT.md · SECURITY.md

License: MIT.

About

A live scoreboard for competitive-socialising sessions (darts, golf, and friends): see players and scores update in real time, edit scores, browse past matches, and watch the game video. Built for the 501 Cloud Developer take-home.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages