A small app for personal or family use to keep track of product purchases: rate and review what you buy, where you bought it, and what you paid.
- Backend: Rust (REST API + CLI), SQLite
- Frontend: Svelte
- Track purchases — Record products you buy.
- Rate and review — Give products a rating and write a review.
- Where and how much — Store the place (store or location) where you bought each product and the price you paid.
backend/— Rust API and CLIfrontend/— Svelte appdocs/— Specification and API docsquadlet/— Podman Quadlet units for running the stack as systemd user units
- Rust (stable, 1.85+)
- Bun (latest)
- Node.js 26
The backend is configured via environment variables. In the backend
directory, copy the example file to .env and edit the values:
cd backend
cp .env.example .envEdit .env to suit your setup. Run the backend from backend/ so it finds
.env. In production, set the variables in the environment instead of using a
file.
cd backend
cargo build --releaseThe binary will be at backend/target/release/pocketratings.
cd frontend
bun install
bun run buildImportant: The backend server must be started before starting the frontend server, as the frontend depends on the backend API.
Start the backend server first:
cd backend
cargo build
cargo clippy --all-targets -- -W clippy::pedantic -W clippy::nursery -W clippy::cargo -D warnings
cargo test
# Start the server
cargo run -- server startThe backend API will be available at http://127.0.0.1:3099 (or the
address configured in your .env file).
Once the backend server is running, start the frontend dev server:
cd frontend
bun install
bun run devThe dev server will start at http://localhost:5173.
cd backend && cargo clippy --all-targets --release -- -W clippy::pedantic -W clippy::nursery -W clippy::cargo -D warnings
cd frontend && bun run lintRun tests for both backend and frontend:
cd backend && cargo test --release
cd frontend && bun run testFrom the repo root, run ./scripts/pre-push.sh to run backend (format,
clippy, test, coverage) and frontend (lint, test) checks. To install as
a git hook so it runs before every push:
ln -sf ../../scripts/pre-push.sh .git/hooks/pre-pushBackend coverage (run by the pre-push script) requires
cargo install cargo-llvm-cov.
You can run the full stack (Caddy reverse proxy, backend API, static frontend)
with Docker or Podman Compose, or with Podman Quadlet as systemd user units. The
proxy routes /api/v1/ to the backend and everything else to the frontend; one
entry point, no CORS. Both setups build the images from the repository and read
the same Caddyfile at the repo root. Compose takes the backend settings from
.env, the Quadlet units from drop-in files.
Prerequisites: Docker with Compose, or Podman with Compose (e.g.
podman-compose).
-
Copy the root env example and edit the values:
cp .env.example .env
-
From the repo root, start the stack:
docker compose up -d # or: podman-compose -f compose.yaml up -d -
Open the app at http://localhost (or https://yourdomain.com if you configured the Caddyfile with a domain for Let's Encrypt).
For production HTTPS, edit the Caddyfile at the repo root: replace the site
address :80 with your domain (e.g. https://pocketratings.example.com). Caddy
will obtain and renew a certificate automatically.
Prerequisites: Linux with systemd and Podman 6.1 or newer.
The unit files are in quadlet/. The proxy publishes HTTP on port 3080, which a
rootless container can use; terminate TLS in front of it, or change the
Caddyfile and the published ports as described below.
-
Create the JWT signing secret as a podman secret, and the directory for the settings of the backend:
openssl rand -hex 32 | podman secret create pocketratings-jwt - mkdir -p ~/.config/containers/systemd/pocketratings-backend.container.d
Then create
~/.config/containers/systemd/pocketratings-backend.container.d/settings.confwith the settings of the backend. SetAPP_BASE_URLto the address the app is served from:[Container] Environment=APP_BASE_URL=http://localhost:3080 Secret=pocketratings-jwt,type=env,target=JWT_SECRET
-
Link the unit directory into the Podman configuration of your user and load the units:
mkdir -p ~/.config/containers/systemd ln -s "$PWD/quadlet" ~/.config/containers/systemd/pocketratings systemctl --user daemon-reload
Quadlet follows the link and resolves the relative paths in the units (
../backend,../frontend,../Caddyfile) against the repository.podman quadlet installcopies the units without the repository, so do not use it here. -
Start the stack. The first start builds both images and takes several minutes:
systemctl --user start pocketratings
-
Open the app at http://localhost:3080.
The stack starts at boot when systemd starts your user instance at boot:
loginctl enable-linger "$USER".
Settings of the host. Put host-specific settings in drop-in files instead of
editing the units. A drop-in for the proxy goes in
~/.config/containers/systemd/pocketratings.container.d/ and one for the
backend in pocketratings-backend.container.d/ next to it; the file name must
end in .conf. An empty PublishPort= line removes the default port, for
example to listen on localhost only:
[Container]
PublishPort=
PublishPort=127.0.0.1:3080:80Run systemctl --user daemon-reload after changing a drop-in, then restart its
unit, e.g. systemctl --user restart pocketratings.
Update. Fetch the new version and restart the backend and the frontend; each start of their units rebuilds the image from the repository:
git pull
systemctl --user daemon-reload
systemctl --user restart pocketratings-backend pocketratings-frontendStop the stack with systemctl --user stop 'pocketratings*'.
When the stack is running, the database lives inside the backend container. Run CLI commands in that container so they use the same database:
docker compose exec backend /app/pocketratings <command> [options]
# or: podman-compose -f compose.yaml exec -T backend /app/pocketratings <command> [options]
# or (Quadlet): podman exec pocketratings-backend /app/pocketratings <command> [options]Examples (registration is CLI-only in v1); with Quadlet, replace
docker compose exec backend with podman exec pocketratings-backend:
docker compose exec backend /app/pocketratings user register \
--name "Jane" --email jane@example.com --password secret
docker compose exec backend /app/pocketratings user list
docker compose exec backend /app/pocketratings user set-password \
--email jane@example.com --password newsecret
docker compose exec backend /app/pocketratings user reset-link \
--email jane@example.com
docker compose exec backend /app/pocketratings category listuser reset-link prints a one-time password reset link (valid for 12 hours,
invalidated once used) built from APP_BASE_URL. Send it to the user over a
secure channel; they set their own password without you learning it. Add
--output json to get { "url", "expires_at" } instead.
To run the CLI against a local database instead (e.g. from the repo
with cargo run), use the backend's own .env in backend/ and run from
there; see Configuration and Development above.
The database is stored in a named volume, pocketratings_pocketratings_db with
Compose and pocketratings-db with Quadlet, mounted at /data in the backend
container. The database file is /data/pocketratings.db.
Back up the database
From the repo root, with the stack running:
Recommended: hot backup without stopping the server
docker compose exec backend /app/pocketratings database backup
# or: podman-compose -f compose.yaml exec -T backend /app/pocketratings database backup
# or (Quadlet): podman exec pocketratings-backend /app/pocketratings database backupThis writes a snapshot to /data/pocketratings.db.backup inside the container.
Copy it out (replace CONTAINER_ID with the backend container ID or name; with
Quadlet the name is pocketratings-backend):
docker cp CONTAINER_ID:/data/pocketratings.db.backup backup-$(date +%Y%m%d-%H%M%S).db
# or: podman cp CONTAINER_ID:/data/pocketratings.db.backup backup-$(date +%Y%m%d-%H%M%S).dbAlternative: raw copy (simpler but not a guaranteed consistent snapshot if the server is writing):
docker compose exec backend cat /data/pocketratings.db \
> backup-$(date +%Y%m%d-%H%M%S).db
# or: podman-compose -f compose.yaml exec -T backend cat /data/pocketratings.db \
# > backup-$(date +%Y%m%d-%H%M%S).db
# or (Quadlet): podman exec pocketratings-backend cat /data/pocketratings.db \
# > backup-$(date +%Y%m%d-%H%M%S).dbRestore the database
-
Stop the backend:
docker compose stop backend # or: podman-compose -f compose.yaml stop backend # or (Quadlet, also stops the proxy): systemctl --user stop pocketratings-backend
-
Replace the database in the volume (use the backup file path you have):
docker run --rm \ -v pocketratings_pocketratings_db:/data -v "$(pwd)":/backup \ alpine sh -c "cp /backup/backup-YYYYMMDD-HHMMSS.db /data/pocketratings.db \ && chown 999:999 /data/pocketratings.db"
UID and GID 999 belong to the user app that runs the backend. For Podman use
podman run. Replace backup-YYYYMMDD-HHMMSS.db with your backup filename. The
volume name is pocketratings_pocketratings_db, or pocketratings-db with
Quadlet.
- Start the backend again:
docker compose start backend # or: podman-compose -f compose.yaml start backend # or (Quadlet): systemctl --user start pocketratings
GPL-3.0 — copyleft; derivatives and forks must remain open source. See LICENSE.