# Nupplo SE ๐งฑ
> **Formerly Brickfolio.** Since 3.0.0 the app is called **Nupplo SE** โ SE
> stands for *Server Edition*, the edition you run yourself. Same app, new
> name; existing installations keep running without any change.
*๐ฌ๐ง English ยท [๐ฉ๐ช Deutsch](README.md)*
[](https://buymeacoffee.com/melle79)
[](LICENSE)
A self-hosted PWA to scan, manage and value a LEGOยฎ collection โ built for the
whole family on one shared database, with an optional **Collector-Pro mode**
for anyone who buys and sells at flea markets.
**Photo โ recognition โ collection.** Recognition runs on the free
[Brickognize API](https://brickognize.com); prices, set inventories and
metadata come from [BrickLink](https://www.bricklink.com) and
[Rebrickable](https://rebrickable.com) (your own API keys required, see below).
> ๐ The full **user manual** lives at [`docs/MANUAL.md`](docs/MANUAL.md)
> ยท [๐ฉ๐ช deutsche Fassung](docs/HANDBUCH.md).
## Features
**Capture & manage**
- ๐ท Scan minifigures and sets straight from the phone camera (candidate list
with match score) or search by name/number โ plain numbers are looked up as
both a set *and* a figure automatically
- ๐ฆ Quantities, condition (new/used), notes, image gallery, full-text search
(with ๐ icon and โ to clear), sorting and type filter; optional **grid
view** (two per row) for browsing
- ๐ Catalogue search from three characters, **10 hits per page** with โload
more resultsโ โ every match is reachable
- ๐ฅ **Add a set's figures with it**: when a set enters the collection, the app
asks which of its minifigures are included (all, none or a selection, with
their own condition) โ without double-counting them in the total value
- ๐ฅ Set linking: sets know their figures (โ๐ฅ 3/4 โโ completeness), figures
show which of your sets they belong to; missing set figures move to the
wishlist with a single tap
- ๐ผ **Reload the image** for an entry if it is missing or wrong
- ๐ **Detail view everywhere**: tapping a search or scan hit opens a popup
with year, theme, prices and โ for minifigures โ the **parts they contain,
with colour names**
- ๐จ **Custom figures**: home-made builds with a number the app assigns for
you (`custom-001`, `-002` โฆ) and your own picture โ optionally the photo
straight from the scan. They live under the โCustomโ theme
- ๐๏ธ **Theme cards**: sorted by theme, the collection shows collapsible cards
(Star Wars, City, Custom โฆ) with count and value per theme. The last sort
order is remembered per profile
**Prices & value**
- ๐ถ BrickLink average prices (new/used) fetched automatically in the
background, with a built-in **price-history log** and per-item chart
- ๐ **Selectable price region** (More โ Price region): worldwide, Germany,
Austria, Switzerland or Europe โ with no sales in the chosen region the app
widens in two steps (Europe first, then worldwide). Existing prices can be
recalculated step by step, priceless items re-fetched with one click. In the
item details a small **flag** (๐ฉ๐ช/๐ช๐บ/๐) on every price row shows where the
price comes from
- ๐ท **Asking prices** (optional): on top of the average sale price, *from how
much* a figure is available right now โ in the item details and as a tag in
wish and shopping lists
- ๐ **Stats tab**: key figures, overall value trend, split by type/condition,
value by release year (tap a bar for the yearโs details), Top 10
**Wishlist**
- โญ Save from any scan/search result, average-price widgets, โboughtโ hand-off
into the collection incl. condition choice
- ๐งฉ Figures that belong to one of your sets and are still missing are flagged
with โmissing from your setโ โ a tap jumps straight to the set
**Trading network** (optional, invite only)
> **It is off by default, and nothing is shared.** Your collection stays on
> your server. The network only exists for you once you join with an
> invitation code โ and even then only what you explicitly choose goes out:
> the items you offer, your profile and, if you switch it on, your wishlist.
> Leaving takes everything out again.
- ๐ค Several Nupplo instances connect through a small **trading hub**:
everyone offers the items they want to pass on โ **for trade, for sale or
both** โ and sees what the others offer
- ๐ข **You pick what goes in**, and how many: out of three identical figures
you can offer just one
- ๐งญ **Discover**: who has what you're looking for, who wants what you have
spare, and who collects the same themes โ worked out on your own instance
- ๐ค **Profiles**: about me, area, favourite themes, trade record โ all
optional; the collection size only rounded and only if you want
- ๐ฆ **Trades with a beginning and an end**: accepted โ sent โ arrived โ
booked โ completed. Adding and removing book straight into the collection,
purchase log included
- ๐ฌ **Messages are end-to-end encrypted** (X25519 + AES-256-GCM). The hub
cannot read them and deletes them once delivered; the readable history stays
on the instances involved
- โ๏ธ **Invitations with a quota**: everyone may invite three friends and can
request more. Nobody gets in without an invite
- โธ **Pause after inactivity**: if you haven't been around for a while, the hub
hides your offers until you come back
- โ **Reports with a follow-up**: the report stays visible to the reporter, the
hub admin can ask questions and take a measure (notice, warning, block) that
the person concerned sees in their app โ without learning who reported them
- ๐ผ๏ธ Custom figures travel with a **downscaled thumbnail** so the other side
doesnโt just see a placeholder
**Collector-Pro mode** (a role the admin grants per user)
- ๐ฐ Purchase price per entry โ pre-filled with the BrickLink average of the
capture day (โ๏ธ) or entered manually (โ๏ธ), with profit/loss display
- ๐ **Shopping lists** for the flea market: fill by scanning, live market
value, purchase price per item, a **bundle offer** with proportional
distribution by market value and a 60 % price suggestion
- ๐ **Sell list**: every duplicate with its giveaway quantity and sale value โ
figures needed for your own sets stay reserved, everything else keeps one
copy for you
- ๐งฉ **Missing set figures**: across all your sets, which minifigures are still
missing (count, sets, estimated cost) โ add them to the wishlist one by one
or all at once, export as CSV or print
- ๐ When receiving from a shopping list, the **list name is written into the
notes** of the collection entry (origin stays traceable)
- ๐ Extra stats: total paid, profit, best value gains
**Family & operations**
- ๐ Multi-user with token login (PBKDF2-hashed passwords), admin and pro
roles (admins can appoint further admins), self-service password/name change
- ๐พ Full **backup** as JSON (download & restore), CSV export and print-ready
lists
- ๐ท Configurable **display name** in logo and title (empty by default); ideal
when several family members each run their own instance
- ๐ Three **themes** to choose from (More โ Design): light โClassicโ, dark
โGalaxyโ with a starfield, and โNovaโ โ a modern glass design with a blue
accent; the choice is per device
- ๐ **Notices on the start screen**: if BrickLink renames or removes a number
from your collection, it shows up in the Scan tab โ and stays there until
someone dismisses it. The app looks up the new number in the BrickLink
Catalog Change Log and applies it everywhere with one click
- ๐ **External-access helper** (More โ External access, admin): turns your
Cloudflare tunnel token into a ready-to-paste `docker-compose` block โ reach the
app on the go without port forwarding. The app never starts the tunnel itself
(no Docker access); the token stays in the browser
- ๐ **Error report** (More โ Error report, admin): errors from every device
report themselves to your own server and identical ones are grouped. With a
GitHub token stored, one click turns an entry into an issue; API keys and the
token are stripped from the text first
- ๐ฅ **Responsive to screen width**: bottom tab bar on phones, a left sidebar
with a wider grid (four to five cards per row) on desktop โ same app, just
laid out to use the space
- โจ **Modern look**: items open in a window that leads with the **image
across the full width**; below it the name, tags and โ for dealers โ
three tiles (paid, value, profit). The rest sits on three tabs (copy,
prices, more) instead of stacking up. In the collection the product
image shows as a subtle card background
- ๐ฒ Installable as a PWA โ on a phone the scan page offers to put it **on the
home screen** (a button on Android, instructions on iPhone) and hides the
hint once it is there. Offline shell, no cloud โ everything stays on your
own server
## Screenshots
| Scan | Collection |
|:---:|:---:|
| | |
| **Stats** | **Shopping list (flea-market mode)** |
| | |
*(Screenshots with demo data)*
## Quick start (Docker)
No source code, no build โ two commands:
```bash
mkdir nupplo && cd nupplo
curl -sLo docker-compose.yml https://raw.githubusercontent.com/Melle79/nupplo/main/docker-compose.example.yml
docker compose up -d
```
Open `http://:8300` โ on first visit a **setup wizard** walks you
through everything: admin account, display name, API keys (with a test
button) and, if you have one, your invite to the trading network. Every step
can be skipped; anything can be added later under *More*. The database is
stored persistently at `./data/brickfolio.db`.
The image (`ghcr.io/melle79/nupplo:latest`, mirrored as
`melle79/nupplo:latest` on Docker Hub) is built for **amd64** โ that is
**x86-64**, any ordinary Intel *or* AMD machine (Synology,
Intel NAS, PC) and **arm64** (Raspberry Pi, ARM NAS).
### Synology NAS
No shell needed: **Container Manager โ Project โ Create**, paste the YAML,
done. Step by step in [`docs/SYNOLOGY.md`](docs/SYNOLOGY.md) (in German).
Over SSH it works just as well โ create `/volume1/docker/nupplo` and run
the commands above with `sudo`.
### Other NAS systems and computers
It is an ordinary OCI image on two public registries โ it runs wherever
containers run. Same YAML as above, just pasted somewhere else:
| System | Where the YAML goes |
|---|---|
| **QNAP** | Container Station โ *Applications* โ Create |
| **UGREEN** | Docker โ *Project* โ Create |
| **TerraMaster** | Docker Manager โ *Compose* |
| **Asustor** | Portainer (from App Central) โ *Stacks* |
| **Unraid** | Docker tab, or *Compose Manager* from Community Apps |
| **TrueNAS SCALE** | Apps โ *Custom App* (YAML) |
| **OpenMediaVault** | omv-extras โ Compose โ *Files* |
| **Linux / Raspberry Pi / Mac / Windows** | create a folder, `docker compose up -d` |
Very old 32-bit ARM devices (`armv7`) are not supported.
**Unraid** has it easier: under *Docker โ Settings โ Template Repositories*
add this address โ
```
https://github.com/Melle79/unraid-templates
```
โ and Nupplo appears under *Docker โ Add Container* in the template
picker, with the port, the `/data` path and the optional keys pre-filled.
The template is maintained in
[Melle79/unraid-templates](https://github.com/Melle79/unraid-templates);
a copy lives here under [`unraid/brickfolio.xml`](unraid/brickfolio.xml).
### Hosted instead of at home
If you would rather not run your own hardware, rent the instance. **It
belongs to whoever creates it** โ data, cost and access. There is still no
Nupplo service in between.
| Provider | How | Note |
|---|---|---|
| **Render** | [](https://render.com/deploy?repo=https://github.com/Melle79/nupplo) | Reads [`render.yaml`](render.yaml). Needs the smallest **paid** plan: Nupplo requires a persistent disk for `/data`, and the free plan has none |
| **Railway** | New project โ *Deploy from GitHub* โ this repo, then set *Settings โ Config-as-Code* to `deploy/railway/railway.json` | Afterwards attach a **volume** at `/data`, otherwise the collection is gone after every restart |
| **Coolify / Dokploy** | Paste `docker-compose.example.yml` | On your own VPS, full control |
> **Why no free plan?** Without a persistent disk the database lives inside
> the container โ every restart would wipe the collection. Better an honest
> note than a deployment that quietly loses data.
Without a compose UI:
```bash
docker run -d --name nupplo --restart unless-stopped \
-p 8300:8300 -v /path/to/data:/data \
ghcr.io/melle79/nupplo:latest
```
### Building it yourself (instead of the ready-made image)
Only needed if you want to run your own changes:
```bash
mkdir nupplo && cd nupplo
curl -sL https://github.com/Melle79/nupplo/archive/refs/heads/main.tar.gz | tar xz --strip-components=1
cp docker-compose.example.yml docker-compose.yml
sed -i 's|image: ghcr.io/melle79/nupplo:latest|build: .|' docker-compose.yml
docker compose up -d --build
```
## Configuration
| Variable | Required | Description |
|---|---|---|
| `ADMIN_USER` / `ADMIN_PASSWORD` | no | Optional: create the admin automatically (otherwise via browser setup) |
| `DB_PATH` | no | Path to the SQLite file (container default: `/data/brickfolio.db`) |
| `BL_CONSUMER_KEY` / `BL_CONSUMER_SECRET` / `BL_TOKEN` / `BL_TOKEN_SECRET` | no | BrickLink Store API for prices & set inventories ([request a key](https://www.bricklink.com/v2/api/register_consumer.page)) |
| `BACKUP_KEEP` | no | Keep this many automatic daily backups (default 14, 0 = off) |
| `REBRICKABLE_KEY` | no | Rebrickable API for name search ([create a key](https://rebrickable.com/api/)) |
| `GITHUB_REPO` | no | Target repository for issues from the error report (default `Melle79/nupplo`) |
| `BRICKFOLIO_NAME` | no | Display name in logo/title (empty by default); also settable in-app |
All API keys can alternatively be stored **in the app** (More โ API keys,
admin only) โ environment variables serve as a fallback.
## Permissions overview
| Action | Default | Collector-Pro | Admin |
|---|:-:|:-:|:-:|
| Scan, collection, wishlist, stats | โ | โ | โ |
| See shopping lists & mark an item โreceivedโ | โยน | โ | โยน |
| Create/fill/archive lists, bundle offer, sell list | โ | โ | โ |
| See purchase prices & profit | โ | โ | โ |
| Users, roles, API keys, backup | โ | โ | โ |
ยน The tab only appears when at least one active list exists.
Roles combine (the admin can grant themselves the pro role).
## Updates & backup
**The app tells you.** Under *More โ Version & updates* it compares the running
version against the latest release, whichever way you installed.
**With the ready-made image** (the quick start above), two commands in the
project folder:
```bash
docker compose pull && docker compose up -d
```
On a Synology the same works without a shell: *Container Manager โ Project โ
nupplo โ Action โ Build and restart*.
The `data/` folder is untouched, database migrations run automatically on
start and are idempotent. **A snapshot beforehand never hurts** โ download the
JSON file in the app under *More โ Backup*.
> `update.sh` and `update-watch.sh` are **not** in the image and not in your
> folder if you only fetched the `docker-compose.yml`. If you want them
> (automatic snapshot, updating from inside the app), fetch them once:
>
> ```bash
> curl -sLO https://raw.githubusercontent.com/Melle79/nupplo/main/update.sh
> curl -sLO https://raw.githubusercontent.com/Melle79/nupplo/main/update-watch.sh
> ```
**Built from source**: `sudo bash update.sh` in the project folder. It takes a
database snapshot first (the last three are kept) and detects on its own
whether to pull the image or rebuild.
### Updating from inside the app (optional)
**Entirely optional** โ without setup nothing changes and updates keep working
via `update.sh` over SSH. The button only appears once the helper below is in
place; until then the app just points to it.
**How it works.** The app does **not** run the update itself โ it canโt, since
it lives inside the container. It only writes `data/update-requested.json`. A
small script on the server picks that up and runs `update.sh`. That way the app
needs **no Docker access** โ handing the socket to a container is effectively
root on the host.
#### Setup
Run `update-watch.sh` regularly. **Once a minute is plenty** โ the update
itself takes one to three minutes anyway.
**Synology (DSM):** Control Panel โ Task Scheduler โ Create โ Scheduled Task โ
User-defined script
| Tab | Setting |
| --- | --- |
| General | User: **`root`** (otherwise the script may not run `docker compose`) |
| Schedule | Daily ยท start `00:00` ยท โContinue running within the same dayโ โ ยท repeat **every minute** ยท last run time: **`23:59`** |
| Task Settings | Command: `sh /path/to/nupplo/update-watch.sh` |
> โ ๏ธ โLast run timeโ defaults to `00:59` โ the task would then only run during
> the first hour of the day. Set it to `23:59`.
**Linux with cron:** `* * * * * sh /path/to/nupplo/update-watch.sh`
#### Multiple instances
Running several Nupplos (each in its own folder with its own
`docker-compose.yml`)? Prefer **one task per instance**. That is the most
robust setup: each runs independently, and the task scheduler shows you per
instance whether it succeeded.
If you would rather use a single task, append `|| true` to every line:
```sh
sh /volume1/docker/nupplo/update-watch.sh || true
sh /volume1/docker/nupplo-zweit/update-watch.sh || true
```
> โ ๏ธ Without `|| true` the second line may never run: if the first one exits
> with an error (missing permissions, say), the scheduler aborts the whole
> script and the second instance never gets a heartbeat.
Either way the instances stay independent โ each has its own marker in its own
`data` folder, so updating one leaves the other alone.
#### Flow
The admin picks now / 1 min / 5 min โ every signed-in browser shows a countdown
(โplease finish your editsโ), then a lock screen. Once the server is back, the
browsers reload themselves. While the countdown runs, the admin can cancel.
- The helper touches `data/update-watch-alive` on every run. That is how the
app knows it is set up โ if the heartbeat is older than five minutes, the
update is not offered at all.
- Log of every run: `data/update-watch.log`.
- Backup: in-app under More โ Backup (JSON with all data incl. users and
price history) **or** simply copy `data/brickfolio.db`.
## Tech
FastAPI + SQLite (no ORM) ยท Vanilla-JS PWA (no build step) ยท
Docker deployment ยท APIs: Brickognize, BrickLink Store API (OAuth1),
Rebrickable.
### What lives in its own repo
This repo holds the **program**. Three things beside it move at their own
pace and therefore have their own repo โ all three private, because they
describe credentials or carry screenshots of a real collection:
| what | repo | for |
|---|---|---|
| project site brickfolio.cc | `brickfolio-website` | static Cloudflare Worker, published by hand |
| catalogue service | `brickfolio-katalogdienst` | builds the catalogue index and writes it here, encrypted, to `katalog/index.nupplo` |
| hub console | `brickfolio-hub-admin` | interface for running the trade network |
| trade hub | `brickfolio-hub` | Cloudflare Worker + D1 that instances talk to for trading |
The app itself only talks to the hub over HTTP; it does not need its
source.
## Legal
LEGOยฎ is a trademark of the LEGO Group, which does not sponsor, authorise or
endorse this project. BrickLink, Rebrickable and Brickognize are trademarks of
their respective owners; their APIs are subject to their respective terms of
use. This is a private hobby project with no commercial intent.
Data and images come from Rebrickable (catalogue search), BrickLink (prices,
set contents, images) and Brickognize (image recognition โ taking a photo
sends it there). The same notes are shown in the app under
**More โ Sources & legal**.
The **Nunito** typeface (SIL Open Font License 1.1, licence text in
`frontend/fonts/OFL.txt`) is served locally โ so no visitor data goes to font
CDNs, and the interface still looks right when the server has no internet
connection.
### What works without internet
Your collection and the interface both live on your own server. If the server
is reachable on the local network but has no internet, these keep working:
- browsing and editing the collection, wishlist and shopping lists
- manual entry, including custom figures with your own picture
- statistics, CSV export and print lists
- downloading and restoring a backup
Everything that asks the outside world needs internet: **scanning**
(Brickognize), **search by name** (Rebrickable), **prices and set contents**
(BrickLink), the **catalogue images** (they are hosted on their servers โ
images you uploaded yourself are not), the **update check** and the **trading
network**.
With no server reachable at all, the installed PWA still loads its interface
from the cache but shows no data โ data always comes live from your own
server, none of it is kept in the browser.
## Support
Nupplo is a private hobby project and free to use. If you like it and want
to support development, I'd be happy about a coffee โ
## License
[MIT](LICENSE) for the program code. The figure catalogue in `katalog/`
is excluded: it may only be used within Nupplo, see
[katalog/LIZENZ.md](katalog/LIZENZ.md).