Wordly
Docker app from jakeshakeWordly's Repository
Overview
Readme
View on GitHubWordly
A small, self-hosted collection of daily word games: Wordly (guess the 5 or 6 letter word), Spelly Bee, Boxy Letters, and Connecty. Mobile-friendly, no accounts, shared leaderboards for everyone on your server. The home page (/) is a game menu showing today's progress in each game, and every game shares the same themes, player name, and leaderboard backend.
Play against your family and friends
The big difference from the NYT games: everyone on your server plays the same daily puzzles and lands on the same leaderboard. Challenge your spouse, kids, or friends, compare today's results, and see who's ahead over weeks of streaks, win rates, average guesses, and best times. Each game has its own board, with a Today view and an All-time view.
Pick a name on any device and you're in; there are no accounts or sign-ups.
Screenshots
A few of the themes: Forest, Bubblegum 3D, Synthwave and Ocean. Pick yours with the theme button (◑) on the home menu; it applies to every game.
Leaderboard screenshots use sample players and scores.
Games
Wordly (/wordly)
- 5 or 6 letter mode — toggle in the header, adjusts guess count (6 tries for 5-letter, 7 for 6-letter, word length + 1).
- Daily Challenge — everyone gets the same word each day (per length), computed deterministically from the date so it resets automatically at local midnight. No server round-trip needed to pick it.
- Practice mode — unlimited random-word games, doesn't touch the leaderboard.
- Leaderboard — games played, win %, current streak, best streak, average guesses, best time, and average luck, separately for 5- and 6-letter.
- Speed timer — starts on your first keystroke each Daily game, shown live above the board, recorded on completion, and tracked on the leaderboard as each player's best time.
- Luck rating — on a win, shows how many answer-pool words were still possible right before your winning guess. Solve it in few guesses despite a wide-open field and you'll score high on luck; narrow it down methodically and you'll score low ("All Skill"). Averaged per player on the leaderboard.
- A huge valid-word dictionary (8,645 five-letter / 15,232 six-letter words) so common guesses never get rejected, while the daily/practice answer always comes from a smaller curated common-word list (2,315 / 1,233 words) so the puzzle itself stays fair and guessable.
Spelly Bee (/bee)
- Make words from a 7-letter hive; every word needs the center letter and at least 4 letters. Scoring: 1 point for 4-letter words, 1 per letter above that, +7 for a pangram (uses all 7 letters).
- Ranks from Beginner to Genius (70%) and Queen Bee (every word). Tap the rank bar for the thresholds.
- Daily hive shared by everyone (resets at midnight), re-rolled until it has 20–70 answers so no day is a dud; Practice for unlimited random hives, with a "Reveal answers" button.
- Yesterday's answers, with the words you found ticked off.
- Time to Genius — an active-play clock that pauses when the tab isn't visible.
- Leaderboard: Today (score, rank, words, pangrams, Genius time) and All-time (days played, average % of max, Genius days, Queen Bee days, pangrams, streak, best Genius time). Scores submit as you play (debounced).
- Keyboard: type letters, Enter, Backspace, Space to shuffle.
Boxy Letters (/boxed)
- 12 letters, 3 per side of a square. Words chain end-to-start, consecutive letters must come from different sides, and the goal is to use all 12 letters in as few words as possible.
- Each daily box is generated around a real two-word solution from common words, so a 2-word solve is always possible. A par (4–6 words) is set based on how many 2-word solutions exist.
- Delete past the start of a word to reopen the previous one; Restart clears everything.
- Daily + Practice modes, yesterday's solution, and an active-play timer.
- Leaderboard: Today (words, par, time — fewer words wins, time breaks ties) and All-time (solves, average words, fewest words, 2-word solves, at/under-par solves, best time, streaks). First solve of the day counts.
Connecty (/connections)
- 16 words, four hidden groups of four. Tap four words and Submit; four mistakes and you're out. "One away…" when three of the four belong together, and repeat guesses don't cost a mistake.
- Groups are color-coded by difficulty (yellow → green → blue → purple). Purple groups are wordplay: hidden words, fill-in-the-blank compounds (BALL, FIRE), anagrams, words that start or end with something.
- Red herrings, but one answer: words regularly fit a second group on the board, but every puzzle is checked at build time to have exactly one way to sort all 16 (see Dictionary below).
- Daily puzzle shared by everyone (numbered from #1 on 2026-11-02), Practice plays any puzzle from the archive, Yesterday's answers, active-play timer, and a copyable emoji result grid.
- Leaderboard: Today (solved, mistakes, time) and All-time (played, win %, perfect games, average mistakes, best time, streaks). First finished game of the day counts.
Shared
- 19 themes — Dark, Light, Standard Neon, Neon City, Synthwave, Bubblegum 3D, Ocean, Sunset, Forest, Halloween, Christmas, Neon 80s, Spooky, Retro Terminal, Bubblegum, Art Deco, Tropical, Desert, Dessert — picked from the palette button on any page, saved across visits and games.
- Player name (no password) set once, used by every game's leaderboard.
Architecture
Frontend is plain HTML/CSS/JS (public/), no build step, no framework. It's paired with a small zero-dependency Node backend (server.js, built-in http/fs only — no npm install, no package.json) that does two things:
- Serves the static frontend (gzip-compressed for text assets, with clean URLs like
/bee→bee.html). - Stores and aggregates leaderboard scores as JSON files in the data directory (
scores.jsonfor Wordly,bee.json,boxed.json,connections.json), so results submitted from any device/browser are visible to everyone.
Shared frontend code (themes, player name, modals, dates, seeded RNG, stopwatch) lives in public/common.js; each game has its own *.html + *.js, with Spelly Bee / Boxy Letters / Connecty styles in public/games.css.
Rate limiting
The API is rate limited per client IP with fixed one-minute windows: 30 score submissions/minute and 120 leaderboard reads/minute by default (override with RATE_LIMIT_WRITES / RATE_LIMIT_READS env vars). Over the limit returns 429 with a Retry-After header; the games quietly retry later.
If you put the container behind a reverse proxy (Nginx Proxy Manager, SWAG, Traefik…), set TRUST_PROXY=1 so limits key on the real client IP from X-Forwarded-For rather than the proxy's address. Leave it unset when clients connect directly, so nobody can spoof the header.
The daily word itself is not served by the backend — it's derived client-side from a deterministic hash of the local date, so it needs no network call and stays in sync across devices without any server coordination. (This does mean the word technically sits in the page's JS if someone opens dev tools — there's no server-side anti-cheat. Fine for a friendly household leaderboard; don't peek at words.js before you've guessed.)
Score submissions are trusted from the client (no server-side guess verification) — again, appropriate for a private 2-person leaderboard, not a public competitive one.
Running it locally
You need Node.js (any reasonably recent version) to run the full app with the leaderboard:
node server.js
Visit http://localhost (or set PORT/DATA_DIR env vars, e.g. PORT=5173 node server.js).
To preview just the frontend without Node (leaderboard calls will fail gracefully, everything else works), serve public/ with any static file server, e.g. the bundled zero-dependency PowerShell server for Windows machines without Python/Node:
powershell -File .devserver/serve.ps1 -Port 5173
Docker / Unraid
A Dockerfile (Node 20 alpine) and a GitHub Actions workflow (.github/workflows/docker-publish.yml) are included. Every push to main builds, smoke-tests, and publishes a multi-arch image to GitHub Container Registry at:
ghcr.io/jakeshake/wordly:latest
To run it on Unraid:
- Docker tab → Add Container.
- Repository:
ghcr.io/jakeshake/wordly:latest - Port: map container port
80to whatever host port you want (e.g.8080). - Path: map container path
/datato a host appdata folder (e.g./mnt/user/appdata/wordly) — this is what makes the leaderboard persist across container restarts/updates. Without it, scores reset every time the container is recreated. - Apply — Unraid pulls the image and starts it. Visit
http://<unraid-ip>:8080.
If the GHCR package is private, Unraid needs a registry login first (Docker tab → gear icon → add a registry with a GitHub personal access token that has read:packages scope). Making the repo/package public avoids this.
To build and run locally instead (no GHCR):
docker build -t wordly .
docker run -p 8080:80 -v wordly-data:/data wordly
Dictionary
Spelly Bee and Boxy Letters word lists (public/words-bee.js, public/words-boxed.js) are generated by tools/build_words.py from the ENABLE list, filtered by word frequency (wordfreq) and an offensive-word blocklist:
- Spelly Bee answers (~21.8k) are modeled on the NYT's own list. Fan archives of past NYT puzzles (tedmiston/spelling-bee-answers, philshem/scrape_bee, bwillenbring/nytimes-bee — ~1,200 puzzles, 2019–2025) show which words the NYT accepted and, since every word that fit a puzzle but isn't in its answers was turned down, which it rejected. Each word takes its most recent NYT verdict (~9k accepted, ~6.5k rejected). Words the NYT has never judged fall back to our own list: common words (wordfreq), well-known dictionary words (SCOWL size ≤55) and their inflections (lemminflect). Backtested against a held-out year of NYT puzzles, this matches the NYT's answers to within ~0.4 extra and ~0.7 missing words per puzzle (the previous frequency-based list was off by ~5 extra and ~6.6 missing). No S, ever (same as NYT).
- Hives are still picked from
BEE_WORDS, a stable common-word list, so changing what counts as an answer never reshuffles a daily puzzle. - Petitions:
tools/bee-petitions.txt— one word per line to accept a word regardless of the NYT's verdict (this also overrides the generic blocklists below), or-wordto reject one. Re-run the build after editing. - Offensive words: the LDNOOBW list plus hand-kept
EXTRA_BLOCK/LATE_BLOCKsets (slurs and crude words it misses) are excluded from every answer/solution list — except that a Spelly Bee word the NYT itself has accepted (ANAL, NUDE, NIPPLE, CRAP, POOP…) counts as an answer anyway. Slurs and hate terms (NEVER_ACCEPT) stay out no matter what. Words blocked after launch go inLATE_BLOCK, which lists them inBEE_AVOID/BOXED_AVOIDinstead of removing them from the puzzle-picking lists, so blocking a word never reshuffles daily puzzles. - Boxy Letters valid words (~131k): the full ENABLE list minus words with doubled letters (impossible to play). Solutions for generated puzzles come from a ~12k common-word subset.
Re-run with pip install wordfreq lemminflect && python3 tools/build_words.py (needs network access to GitHub and SourceForge; downloads are cached in tools/.cache).
Connecty (public/words-connections.js) is generated by tools/build_connections.py from the category bank in tools/connections-categories.txt (~200 categories, each with the words that can be shown plus extra words that also fit it):
- Each puzzle takes one category per difficulty and four words from each. It's only kept if the 16 words can be sorted into its four categories in exactly one way, and no other category in the bank covers four of the words (other than one of the puzzle's own groups), so there's never a second "correct" grouping. Wordplay categories carry a rule (
before:BALL,start:CAR CAB,contains:RED TAN,anagram:STEAL…) that the checker also applies to every other word on the board — fill-in rules are checked against the ENABLE dictionary — so a stray SNOWMAN or hidden RED gets caught. - Among valid candidates it prefers ones with red herrings, and doesn't reuse a category until most of its difficulty tier has had a turn.
- 1,000 puzzles are generated (~2¾ years of dailies, then it loops). Re-running the build keeps existing puzzles and only appends, so editing the bank never changes a puzzle someone has played;
--rebuildstarts over. Runpython3 tools/build_connections.py(uses the same cached downloads asbuild_words.py).
Wordly:
- Valid guesses (
GUESSES[5],GUESSES[6]inpublic/words.js): a large Scrabble-style word-game dictionary (ENABLE word list), ~8.6k five-letter and ~15.2k six-letter words. This is what guesses are checked against, so legitimate everyday words are essentially never rejected. - Possible answers (
ANSWERS[5],ANSWERS[6]): the curated common-word lists from the original build — a curated 2,314-word list of common words for 5 letters, and a manually-screened ~1,233-word list for 6 letters. Daily and Practice answers are always picked from here, so the puzzle stays solvable/fair.ANSWERSis a subset ofGUESSES, guaranteed at build time.
Customizing
- Add a theme: add a
[data-theme="yourname"]block inpublic/style.csswith the same CSS custom properties as the existing themes, then add{ id, name, swatch }to theTHEMESarray at the top ofpublic/common.js. Optionally set--bee-center/--bee-center-textif the theme's--color-presentdoesn't stand out as the Spelly Bee center hex. - Add a length: add
GUESSES[n]andANSWERS[n]arrays inpublic/words.js, a<button class="len-btn" data-len="n">n</button>inindex.html's.length-toggle, and the board/keyboard/daily logic handles the rest automatically (max guesses = length + 1). - Change the daily reset time or make it shared across timezones:
todayDateString()inpublic/common.jsuses the browser's local date. Switch it to a fixed UTC-based string if you'd rather everyone share one global reset time regardless of timezone.
Theme background images
public/images/themes/ holds per-theme background images, wired up as a background-image on the matching [data-theme="..."] block in public/style.css (see the "Theme background images" section near the top of that file). Currently wired: Ocean, Christmas, Spooky, Bubblegum 3D, Sunset, Standard Neon, Neon City, Neon 80s, Synthwave, Forest, Halloween (Sunset, Forest and Halloween use user-supplied photos/art), Retro Terminal, Bubblegum, Art Deco, Tropical, Desert, Dessert. Dark and Light intentionally stay flat.
The Retro Terminal, Bubblegum, Art Deco, Tropical, Desert, and Dessert backgrounds are hand-built SVGs generated by tools/make_theme_art.py (seeded, so re-running reproduces them exactly) — edit the script and re-run to tweak them.
Convention:
- File name matches the theme's
idfrom theTHEMESarray inpublic/common.js(e.g.ocean.jpg,neon-city.svg). Extension can be.jpg,.png,.webp, or.svg. - Portrait-friendly / mobile-first framing, since the board is narrow and tall on phones.
background-position: center topkeeps the top of the image anchored under the header. - Each wired theme pairs its image with a
linear-gradient(...)scrim tuned to that image's brightness, layered in the samebackground-imagedeclaration, so tiles/keys (which always paint a solid theme color) and header text stay legible on top.
To add one for a theme that doesn't have it yet: drop the image in this folder, then add its [data-theme="..."] body { background-image: linear-gradient(...), url("images/themes/yourfile"); } rule alongside the others, and add the theme's selector to the shared background-size/position/repeat rule above them.
Unraid
Wordly is listed in Unraid's Community Applications (search for Wordly in the Apps tab). The template lives in templates/wordly.xml.
Versions
Every push to main publishes ghcr.io/jakeshake/wordly:latest. Release tags (v1.0.0, ...) also publish :1.0.0 and :1.0, so you can pin a version.
Credits
- Word data: ENABLE (public domain), wordfreq, SCOWL, lemminflect, and the LDNOOBW list.
- Several theme backgrounds are wallpapers collected from the web. If you own one of them and want it credited or removed, please open an issue and it will be taken care of promptly.
- Wordly is an independent project and is not affiliated with or endorsed by The New York Times.
License
If you enjoy Wordly, you can buy me a coffee.
Categories
Related apps
Explore more like this
Explore allDetails
ghcr.io/jakeshake/wordly:latestRuntime arguments
- Web UI
http://[IP]:[PORT:80]/- Network
bridge- Shell
sh- Privileged
- false
Template configuration
Port to open Wordly on.
- Target
- 80
- Default
- 8080
- Value
- 8080
Leaderboard scores are stored here. Keep this mapped so they survive container updates.
- Target
- /data
- Default
- /mnt/user/appdata/wordly
- Value
- /mnt/user/appdata/wordly
Set to 1 if Wordly sits behind a reverse proxy or Cloudflare Tunnel, so rate limits use each player's real IP. Leave empty if players connect directly.
Score submissions allowed per IP per minute.
- Default
- 30
- Value
- 30
Leaderboard reads allowed per IP per minute.
- Default
- 120
- Value
- 120