The nginx block is plain http; certbot adds the TLS server block and the redirect itself. It sets X-Forwarded-Proto, which is load-bearing: the app compares the browser's Origin against the URL it believes it is serving, and without that header it thinks it is on http while the browser says https, decides every save is cross-site and drops the session. Upstream's README is kept as README.nullboard.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UU1vyTHj3uE9PJYSxRxwkU
5.4 KiB
ValidBoard
A fork of Nullboard that stores boards
in SQLite on a server instead of the browser's localStorage, behind a login.
Same board, same keyboard shortcuts, same everything — it just follows you between browsers and machines now, and survives clearing your site data.
Upstream's own README is kept as README.nullboard.md.
How it works
Nullboard's storage layer already bottoms out in three methods over string
keys — getItem, setItem, delItem (see class Storage in
nullboard.html). Boards, revisions, undo history and preferences are all
built on top of those, client-side.
So the server is just a key/value store, and knows nothing about boards:
GET /api/items |
every key/value pair |
POST /api/items |
a batch of set/del ops, applied in one transaction |
DELETE /api/items |
wipe the boards (keeps your password) |
The fork adds one class, Storage_Server, alongside upstream's
Storage_Local, and swaps which one gets instantiated. Everything else in
nullboard.html is untouched, so upstream changes still merge.
Two details worth knowing:
Reads are synchronous, so the data ships with the page. setItem has to
return true or false immediately, which no network round trip can do. The
server therefore embeds the whole keyspace into the page as a JSON block
(#nb-bootstrap), and Storage_Server hydrates an in-memory Map from it
before the app boots. Reads are memory reads. It's in the page rather than a
separate .js file so another site can't <script src> it and read your
boards.
Writes are batched and optimistic. Changes are collected by key — a note edited five times in a second is one write — and flushed ~300 ms later as a single transaction. Failures go back on the queue and retry with a backoff, the logo shows saving… / not saved, and closing the tab with writes still pending asks for confirmation. Nothing is lost if the server is briefly away.
Running it
Dependencies are all packaged on Debian/Ubuntu; no bundler needed:
sudo apt install ruby-full ruby-sqlite3 ruby-sinatra ruby-bcrypt \
ruby-rack ruby-rack-protection ruby-json puma
Set the password, then start it:
bin/validboard-passwd # or: pass show validboard | bin/validboard-passwd
rake server # http://127.0.0.1:8047
Until a password is set every route returns 503 telling you to run that command — letting the first visitor choose one would hand your board to whoever found the URL first.
Tests
rake test
49 tests covering the store (validation, transactions, password hashing,
schema guard) and the app (auth, lockout, the API, cross-site writes, and that
a note containing </script> can't break out of the bootstrap block).
Configuration
| Variable | Default | |
|---|---|---|
VALIDBOARD_DB |
./data/validboard.db |
boards, password hash and session secret |
VALIDBOARD_SECURE_COOKIE |
off | set to 1 behind https |
VALIDBOARD_SECRET |
from the database | session signing key; generated and stored on first run, so restarts don't sign you out |
Deploying
sudo cp deploy/validboard.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now validboard
sudo cp deploy/board.jihakuz.xyz.conf /etc/nginx/sites-available/board.jihakuz.xyz
sudo ln -s /etc/nginx/sites-available/board.jihakuz.xyz /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d board.jihakuz.xyz
The nginx block is plain http on purpose — certbot adds the TLS server block and the redirect itself.
If saves start coming back 401 after enabling TLS, it's the
X-Forwarded-Proto header. The app compares the browser's Origin against the
URL it thinks it's serving; without that header it believes it's on http://
while the browser says https://, decides the write is cross-site, and drops
the session. The supplied nginx config sets it.
Backups
Everything is one file. sqlite3 data/validboard.db ".backup /somewhere/vb.db"
takes a consistent copy while the service is running — don't just cp it, the
WAL sidecar may hold recent writes.
Security
- One password, bcrypt-hashed. No user table; adding a second user means a schema migration.
- Session cookie is signed,
HttpOnly,SameSite=Lax, andSecurewhenVALIDBOARD_SECURE_COOKIE=1. - Ten failed logins from an IP locks that IP out for 15 minutes.
- rack-protection is on, which includes session hijacking detection keyed on
the User-Agent — so a browser that changes its UA string signs you out. If
that ever gets irritating,
set :protection, except: [:session_hijacking]inapp.rb. - Keys are validated against
[A-Za-z0-9._-]{1,128}and values capped at 2 MB.
Keeping up with upstream
git fetch upstream
git merge upstream/master
The changes to nullboard.html are additive — one class, one CSS block, some
markup in the header, and a single changed line where Storage_Local used to
be instantiated — so conflicts should be rare and small.
License
Nullboard is by Alexander Pankratov, under the 2-clause BSD license with the
Commons Clause — free to use, change and redistribute, but not to
sell or run as a paid service. This fork keeps that license unchanged, and the
licence requires the full text to travel with any copy, so LICENSE stays put.