# 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](../README.md) 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 `.nvmrc` says 24 (`nvm use` picks 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 ```bash 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: ```bash 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 ```bash 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 ```bash 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: ```bash npm --prefix backend run dev # tsx watch -> http://localhost:8000 npm --prefix frontend run dev # vite --host -> http://localhost:5173 ``` Open . **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 sits behind Cloudflare, and some imports need FlareSolverr to get through. It's optional -- without it those imports will simply fail. ```bash 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 test` loads `DATABASE_URL` from `backend/.env` through 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. ```bash 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. ```bash 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. ```bash 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 ```bash 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** -> select `extension/dist/chrome`. - **Firefox**: `about:debugging#/runtime/this-firefox` -> **Load Temporary Add-on** -> select `extension/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](../extension/CONTRIBUTING.md) for the code layout, the per-browser builds, the store zips and the screenshot generator. ## Bridge ```bash 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`](../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: ```json { "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" } ] } ``` - `id` must be unique and never reused: it's what stops an entry being sent twice. - `body` and `url` are optional; `url` must be `https://`, and the notification shows its domain. - `path` is an optional in-app page opened on click, e.g. `/admin`. - `published_at` is the date shown; a future date waits until then. - `audience` is `admins` (the default) or `all` for every user. - `version` limits it to instances on certain versions: `1.2.0` exactly, 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 `notifications` list and ignore `version`, so add there only what every one of them should get; everything else goes in `entries`. - Entries older than 90 days aren't sent, so old ones can stay in the file or be removed. - `SYSTEM_NOTIFICATIONS_URL` points a backend at another file; an empty value turns fetching off. ## Related documents - [CONTRIBUTING.md](../CONTRIBUTING.md) -- branch, commit and pull request workflow - [PROVIDER_SETUP.md](PROVIDER_SETUP.md) -- MakerWorld / Thingiverse / Printables credentials