Self-hosted asset management for small teams. Track physical assets with custom categories, hierarchical locations, configurable fields, service scheduling, file attachments, and printable barcode labels.
- Frontend: Next.js 16 (App Router, Turbopack), React 19, Tailwind CSS 4, shadcn/ui
- Backend: PocketBase (SQLite, auth, file storage, realtime subscriptions)
- Labels: bwip-js for barcode/data-matrix generation, CSS
@media print
You need Node.js 20+, pnpm, and bash + curl (for the PocketBase setup script).
pnpm install
pnpm pb:setup # downloads the pocketbase binary into ./bin
cp .env.example .env.local # then set POCKETBASE_SUPERUSER_EMAIL + _PASSWORD
pnpm dev # runs Next.js + PocketBase side-by-side via mprocsThen open http://localhost:3000/setup and create the first admin account.
pnpm pb:bootstrap upserts a PocketBase superuser from the env vars — run it once after editing credentials, or whenever you rotate them.
iOS Safari will not hand out camera access over plain HTTP on a LAN IP — getUserMedia needs a secure context. For the scan feature:
pnpm dev:https # next + pocketbase, Next on HTTPS bound to 127.0.0.1 (loopback)
# or, if you need to reach it from another device on your LAN:
pnpm dev:next:lan # binds Next to 0.0.0.0 — only on a trusted networkOnly use dev:next:lan on a network you control: the /pb/* rewrite forwards requests to PocketBase at the same origin, so any device that can reach your laptop's IP (and accept the self-signed cert) can hit PB directly. Bind to loopback whenever possible.
Set NEXT_PUBLIC_POCKETBASE_URL=/pb in .env.local so the browser hits PocketBase through Next's origin (a rewrite in next.config.ts proxies /pb/* → the real PB instance — development builds only). Open https://<your-laptop-ip>:3000 on the phone and accept the self-signed cert. pnpm pb:seed populates the database with demo assets, locations, and service schedules for the walkthrough.
cp .env.example .env # fill in POCKETBASE_SUPERUSER_EMAIL / _PASSWORD
docker compose up --builddocker-compose.yml brings up two services:
pocketbase— PB 0.37 with./pb_datamounted as a volume, served on port8090app— the Next.js standalone build, served on port3000, pointed at the PB service over the compose network
Open http://localhost:3000/setup to create the first admin.
When running under Docker, the PocketBase container also upserts the configured
superuser on startup using POCKETBASE_SUPERUSER_EMAIL and
POCKETBASE_SUPERUSER_PASSWORD. That keeps the app's admin client credentials
in sync with the PocketBase data volume.
If you prefer to keep Docker settings in .env.local, run Compose with it explicitly:
docker compose --env-file .env.local up --buildThe Next.js container port is configurable with env vars:
APP_PORT— the port the app listens on inside the containerAPP_HOST_PORT— the host port published by Docker Compose
Example:
APP_PORT=4000
APP_HOST_PORT=4000
SITE_URL=http://localhost:4000
docker compose up --buildIf you do not already have a reverse proxy, there is an optional tls profile
that starts a Caddy front-end. Caddy will automatically obtain and renew a
certificate for a public domain via ACME/Let's Encrypt-compatible issuers.
Set these env vars first:
APP_DOMAIN=stowage.example.comNEXT_PUBLIC_POCKETBASE_URL=https://stowage.example.com/pbSITE_URL=https://stowage.example.com
Then start the TLS profile:
docker compose --profile tls up --buildNotes:
APP_DOMAINmust resolve publicly to the Docker host.- Ports
80and443must be reachable from the internet for certificate issuance. - If you already have nginx/Caddy/Traefik in front, skip the
tlsprofile and keep using your existing reverse proxy. - The optional Caddy service proxies
/pb/*to PocketBase so the browser can use PocketBase over the same HTTPS origin.
Stowage uses numbered releases such as 0.2.0, 0.2.1, and 1.0.0.
Each release publishes:
- a git tag like
v0.2.1 - a GitHub Release with human-readable notes
ghcr.io/kroqdotdev/stowage-app:<version>ghcr.io/kroqdotdev/stowage-pocketbase:<version>ghcr.io/kroqdotdev/stowage-caddy:<version>docker-compose.release.ymlas a release deployment bundle
For production/self-hosted deploys, pin exact image tags instead of floating to a new version unintentionally.
Image contents:
stowage-appcontains the production Next.js standalone server plus static/public assetsstowage-pocketbasecontains the pinned PocketBase binary plus this repo'spb_migrations/stowage-caddycontains the bundled Caddy config for TLS termination and/pbproxying- neither image contains your persistent PocketBase data; that still lives in
pb_data/
See RELEASING.md for the release checklist and tag flow.
Tagged releases can now be deployed directly with docker-compose.release.yml.
App only:
STOWAGE_VERSION=0.2.0 docker compose -f docker-compose.release.yml up -dUse this when PocketBase and/or your reverse proxy already live elsewhere. In
that mode, set POCKETBASE_URL and NEXT_PUBLIC_POCKETBASE_URL to your
existing PocketBase endpoints.
App + PocketBase:
STOWAGE_VERSION=0.2.0 docker compose -f docker-compose.release.yml --profile pocketbase up -dFull stack (app + PocketBase + TLS proxy):
STOWAGE_VERSION=0.2.0 docker compose -f docker-compose.release.yml --profile full up -dFor the full stack profile, set:
APP_DOMAINto your public domainSITE_URL=https://<APP_DOMAIN>NEXT_PUBLIC_POCKETBASE_URL=https://<APP_DOMAIN>/pb
By default, the release compose file binds the app and PocketBase ports to
127.0.0.1; set APP_HOST_BIND=0.0.0.0 and/or POCKETBASE_HOST_BIND=0.0.0.0
if you intentionally want them reachable off-host without Caddy.
The browser talks to PocketBase directly for realtime subscriptions, so for a public deployment put both services behind a reverse proxy on a single domain (Caddy, nginx, Traefik). Set NEXT_PUBLIC_POCKETBASE_URL to the proxy URL the browser should use (e.g. https://stowage.example.com/pb), and POCKETBASE_URL to the internal address Next uses to reach PB (e.g. http://pocketbase:8090). pb_data/ is the only stateful directory — back it up.
Environment variables:
| Var | Required | Notes |
|---|---|---|
APP_PORT |
no | Port the Next.js app listens on inside the container |
APP_HOST_PORT |
no | Host port mapped to the Next.js container |
POCKETBASE_HOST_PORT |
no | Host port mapped to PocketBase |
APP_DOMAIN |
no | Public domain for the optional built-in TLS profile |
HTTP_PORT / HTTPS_PORT |
no | Host ports published by the optional Caddy TLS service |
POCKETBASE_URL |
yes | Server-to-PB URL (e.g. http://pocketbase:8090) |
NEXT_PUBLIC_POCKETBASE_URL |
yes | Browser-to-PB URL (same origin as the app in prod) |
POCKETBASE_SUPERUSER_EMAIL / _PASSWORD |
yes | Used by pnpm pb:bootstrap and the app's admin client |
STORAGE_LIMIT_GB |
no | Global cap on total attachment bytes; omit for no limit |
SITE_URL |
no | Base URL embedded in label QR codes |
pnpm test # vitest jsdom suite (components, hooks, lib)
pnpm test:pb # vitest against a throwaway PocketBase instance (domain layer)
pnpm test:e2e # Playwright — boots PB + Next automatically
pnpm test:all # all threeE2E tests can pick up E2E_AUTH_EMAIL / E2E_AUTH_PASSWORD for the positive login path; without them, the auth flow tests fall back to the redirect-only checks.