# 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 `` 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.