Import, collection import and synced collections from MyMiniFactory, alongside MakerWorld, Printables and Thingiverse. - Backend: API v2 client with an instance-wide API key for metadata, collections and authors; files from MyMiniFactory's own download links (free models need no login). Paid models, and free ones whose files are archives, download as the user from their MyMiniFactory library through their session (PHPSESSID), fetching the model files MyMiniFactory unpacked from each archive. Paid models are left out of collection listings and syncs. - One migration: MyMiniFactory category ids, username and session columns, plus a data migration giving existing users' starter categories the new category ids. Category matching now prefers the most specific site category. - Frontend: provider everywhere the others appear (badges, Configuration and Administration providers, sync Browse tab, user menu, link detection, System Health, category ids), shared instance-key form for Thingiverse and MyMiniFactory, en and lt. - Thingport Grab: new provider sites are allowed once from the popup (optional all-sites access, content script registered at runtime), so adding one never makes the browser disable the extension; captures the signed-in MyMiniFactory username and session; paid-model notice. - Docs, website, store assets and screenshots updated.
11 KiB
Development
How to run Thingport locally with hot reload, so you can work on it. If you just want to run an instance, follow the Docker Compose instructions in the README instead.
What's in the repo
| Directory | What it is |
|---|---|
backend/ |
Express + Prisma API (TypeScript). Serves everything under /api/*. |
frontend/ |
React + Vite single-page app. |
extension/ |
"Thingport Grab" browser extension -- TypeScript + SCSS, built with esbuild. |
bridge/ |
Slicer bridge helper app (Go). |
docs/ |
This documentation. |
Prerequisites
- Node.js 24 or newer. The root
.nvmrcsays 24 (nvm usepicks it up), and CI and the Docker images run on it. - Docker, to run Postgres. Everything else runs natively so you get hot reload.
- Go 1.21+ -- only if you're changing
bridge/.
1. Install dependencies
git clone https://github.com/TautvydasDerzinskas/Thingport.git
cd Thingport
npm install # root -- installs the husky pre-commit hook
npm --prefix backend ci
npm --prefix frontend ci
Each project has its own package-lock.json; there is no workspace root tying them together.
2. Start Postgres
The db service in docker-compose.yml has no host port mapping on purpose -- it's only
reachable from inside the Compose network, so a natively-run backend cannot connect to it. Run
your own container for development:
docker run -d --name thingport-db \
-e POSTGRES_USER=thingport \
-e POSTGRES_PASSWORD=thingport \
-e POSTGRES_DB=thingport_dev \
-p 5432:5432 \
postgres:16-alpine
If something already listens on 5432, map a different host port and adjust DATABASE_URL below.
3. Configure the backend
cp backend/.env.example backend/.env
backend/.env.example is written for the Docker Compose deployment, so two values must change
for local development:
| Variable | Set it to | Why |
|---|---|---|
DATABASE_URL |
postgresql://thingport:thingport@localhost:5432/thingport_dev?schema=public |
The example points at db:5432 -- the Compose service hostname, which doesn't resolve outside Docker. |
FILE_STORAGE |
an absolute or relative path you own, e.g. ./storage |
The example uses /app/storage, the path inside the container. |
Also set AUTH_SECRET to any non-empty random string -- it signs login tokens, and the example
value is a placeholder.
Leave API_PORT=8000. The Vite dev server proxies /api to http://localhost:8000
(frontend/vite.config.js), so changing one means changing both.
backend/.env is gitignored. Never commit it.
4. Create the database schema
cd backend
npm run prisma:generate # required -- there is no postinstall hook, so the client isn't generated by `npm ci`
npx prisma migrate deploy # applies the 27 committed migrations
5. Run it
In two terminals:
npm --prefix backend run dev # tsx watch -> http://localhost:8000
npm --prefix frontend run dev # vite --host -> http://localhost:5173
Open http://localhost:5173. The first account you register becomes the admin -- see
backend/src/routes/auth.ts, which bootstraps the first user as ADMIN and everyone after as
MEMBER.
Optional: FlareSolverr
Thingiverse and MyMiniFactory sit behind Cloudflare, and some imports need FlareSolverr to get through. It's optional -- without it those imports will simply fail.
docker run -d --name thingport-flaresolverr -p 8191:8191 ghcr.io/flaresolverr/flaresolverr:latest
Then set its URL (http://localhost:8191, with or without /v1) in Administration → Connections →
FlareSolverr. FLARESOLVERR_URL in backend/.env is copied into that setting on the first startup; after that the
saved value wins and the env var is ignored.
Running the tests
Backend
19 test files, 151 tests. They run against a real Postgres database, registering accounts and creating and deleting rows as they go.
Warning
npm testloadsDATABASE_URLfrombackend/.envthrough dotenv. If that points at your development database, the suite will mutate it. Always pass the variable on the command line -- an explicit shell variable takes precedence over.env, so this is safe.
docker run -d --name thingport-test-db \
-e POSTGRES_USER=thingport -e POSTGRES_PASSWORD=thingport -e POSTGRES_DB=thingport_test \
-p 55432:5432 postgres:16-alpine
cd backend
export TEST_DB="postgresql://thingport:thingport@localhost:55432/thingport_test?schema=public"
DATABASE_URL="$TEST_DB" npx prisma migrate deploy
DATABASE_URL="$TEST_DB" FILE_STORAGE=/tmp/thingport-test-storage npm test
The suite takes about 30 seconds. It runs test files sequentially on purpose -- several files
mutate the same global Setting rows, and parallel workers race each other. See the comment in
backend/vitest.config.ts.
Frontend
There are no frontend tests yet. npm run test:run uses --passWithNoTests, so it succeeds
against an empty suite. New tests are welcome.
Regression pack
Playwright scenarios in regression/ that drive the real app in Chromium: sign-up, sign-in,
uploading (single and multiple files, zips, folder trees), the consume folder, collections, and the
sidebar (navigation, the admin-only route, bookmarks and their drag-to-reorder, collapsing). Upload
fixtures are generated in tests/fixtures.ts rather than checked in. The consume folder is
regression/consume/, bind-mounted into the backend for the run. run.sh builds the backend and
frontend images from your checkout, starts them with Postgres on port 8090, runs the scenarios, and
then removes the stack. The database and storage live on tmpfs, so every run starts empty and leaves nothing behind.
It never touches your development database. It needs Docker.
cd regression
npm install
npx playwright install chromium
./run.sh # whole pack
./run.sh tests/models.spec.ts # extra arguments go to `playwright test`
CI runs it every Saturday at 00:00 UTC (.github/workflows/regression.yml). You can also start it
from the Actions tab. When it fails, the HTML report, traces and container logs are uploaded as the
regression-report artifact.
App screenshots
The README and website screenshots in frontend/src/assets/screenshots/ are captured from a running
instance with a populated library, by regression/screenshots.mjs (the extension's screenshots
aren't covered). It signs in, captures every shot at 1400×790 and 2× scale, and overwrites the files.
The account's theme and Categories/Folders choice are switched for the shots and restored afterwards.
cd regression
THINGPORT_PASSWORD='…' npm run screenshots -- --url https://thingport.example --email you@example.com
npm run screenshots -- --url … --email … --only 03,04 # just some shots
npm run screenshots -- --url … --email … --model "Benchy" # model for the details and 3D shots
--out writes somewhere else instead, e.g. to compare before replacing. Self-signed certificates
are accepted.
Quality checks
| Command | What it runs |
|---|---|
npm run format |
Prettier, gofmt, prisma format |
npm run format:check |
the same, check only (CI) |
npm --prefix backend run lint |
oxlint |
npm --prefix frontend run lint |
oxlint |
npm --prefix frontend run typecheck |
tsc --noEmit |
npm --prefix frontend run verify |
typecheck + lint + tests + build |
A husky pre-commit hook formats staged files and runs oxlint over whichever project you touched (see
lint-staged.config.js). It does not typecheck, so run typecheck yourself before pushing
frontend changes -- CI will.
Extension
cd extension
npm install
npm run dev # watch build into dist/chrome (npm run dev:firefox for dist/firefox)
- Chrome / Edge:
chrome://extensions-> enable Developer mode -> Load unpacked -> selectextension/dist/chrome. - Firefox:
about:debugging#/runtime/this-firefox-> Load Temporary Add-on -> selectextension/dist/firefox/manifest.json.
Reload the extension after each rebuild, and point it at http://localhost:5173 in the popup.
Chrome and Edge run the background bundle as a service worker while Firefox runs it as an event
page -- if you add top-level code there, make sure it works in both. See
extension/CONTRIBUTING.md for the code layout, the per-browser
builds, the store zips and the screenshot generator.
Bridge
cd bridge
go test ./...
go run ./cmd/thingport-bridge
macOS URL-scheme handling uses Cocoa and needs cgo, so the macOS build only works on a Mac.
Windows and Linux binaries cross-compile with CGO_ENABLED=0.
System notifications
Announcements for every instance, such as a known bug or a new feature, live in
system-notifications.json at the repo root. Each backend fetches it
from main at startup and every 6 hours, and sends each entry once to each user it's meant for, as an
ordinary notification (the System tab). To post one, add an entry to entries and merge it:
{
"entries": [
{
"id": "2026-10-imports-fail-on-printables",
"title": "Printables imports fail since Oct 10",
"body": "Printables changed its API. A fix is on the way; update when it lands.",
"url": "https://github.com/TautvydasDerzinskas/Thingport/issues/123",
"published_at": "2026-10-11T12:00:00Z",
"audience": "admins",
"version": "<1.2.0"
}
]
}
idmust be unique and never reused: it's what stops an entry being sent twice.bodyandurlare optional;urlmust behttps://, and the notification shows its domain.pathis an optional in-app page opened on click, e.g./admin.published_atis the date shown; a future date waits until then.audienceisadmins(the default) orallfor every user.versionlimits it to instances on certain versions:1.2.0exactly, or<,<=,>,>=a version. Leave it out for all versions. An instance that later updates into the range still gets it.- Versions before 1.0.0 read only the
notificationslist and ignoreversion, so add there only what every one of them should get; everything else goes inentries. - Entries older than 90 days aren't sent, so old ones can stay in the file or be removed.
SYSTEM_NOTIFICATIONS_URLpoints a backend at another file; an empty value turns fetching off.
Related documents
- CONTRIBUTING.md -- branch, commit and pull request workflow
- PROVIDER_SETUP.md -- MakerWorld / Thingiverse / Printables / MyMiniFactory credentials