Files
Tom FluxandClaude Opus 5 00d3cb0e61 Add systemd unit, nginx config and a README
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
2026-08-14 22:41:11 +01:00

141 lines
5.4 KiB
Markdown

# ValidBoard
A fork of [Nullboard](https://github.com/apankrat/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](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`, and `Secure` when
`VALIDBOARD_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]`
in `app.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](LICENSE) — 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.