No description
  • TypeScript 97.2%
  • CSS 2.1%
  • Dockerfile 0.5%
  • JavaScript 0.2%
Find a file
2026-09-29 22:35:27 +10:00
docs/superpowers Make extra-detail keys editable in Settings 2026-09-19 00:07:23 +10:00
drizzle Turn the notes field into a journal of notes per thing 2026-09-19 00:47:37 +10:00
public Initial commit from Create Next App 2026-09-18 21:41:43 +10:00
scripts Turn the notes field into a journal of notes per thing 2026-09-19 00:47:37 +10:00
src colors 2026-09-29 22:35:27 +10:00
.dockerignore Build the Things asset database 2026-09-18 22:15:58 +10:00
.env.example Add receipt uploads backed by S3-compatible storage 2026-09-18 23:40:15 +10:00
.gitignore Add receipt uploads backed by S3-compatible storage 2026-09-18 23:40:15 +10:00
AGENTS.md Initial commit from Create Next App 2026-09-18 21:41:43 +10:00
CLAUDE.md Use Plus Jakarta Sans as the UI font 2026-09-26 08:56:53 +10:00
components.json Build the Things asset database 2026-09-18 22:15:58 +10:00
docker-compose.yml Add receipt uploads backed by S3-compatible storage 2026-09-18 23:40:15 +10:00
Dockerfile Build the Things asset database 2026-09-18 22:15:58 +10:00
drizzle.config.ts Build the Things asset database 2026-09-18 22:15:58 +10:00
eslint.config.mjs Initial commit from Create Next App 2026-09-18 21:41:43 +10:00
next.config.ts Generalise receipts into typed file attachments 2026-09-19 00:30:51 +10:00
package-lock.json Add receipt uploads backed by S3-compatible storage 2026-09-18 23:40:15 +10:00
package.json Add receipt uploads backed by S3-compatible storage 2026-09-18 23:40:15 +10:00
postcss.config.mjs Initial commit from Create Next App 2026-09-18 21:41:43 +10:00
README.md Turn the notes field into a journal of notes per thing 2026-09-19 00:47:37 +10:00
tsconfig.json Initial commit from Create Next App 2026-09-18 21:41:43 +10:00
vitest.config.mts Build the Things asset database 2026-09-18 22:15:58 +10:00

Things

A small personal asset database: what you own, what model it is, where and when you bought it, when the warranty runs out, plus tags and free-form key/value details. Think "CMDB for one person".

Design decisions live in docs/superpowers/specs/, the build plan in docs/superpowers/plans/.

Run locally

npm install
npm run dev            # http://localhost:3000
npm run seed:demo      # optional: a handful of example things

The SQLite file is created at data/things.db on first start. Migrations run automatically when the server boots (src/instrumentation.ts).

Files attached to a thing (receipts, manuals, photos, ...) are stored in an S3-compatible object store. For local development run MiniStack, a free local AWS emulator, before uploading anything:

npm run s3:dev         # docker compose up -d ministack  (port 4566)

The app defaults to http://localhost:4566, bucket things-receipts, credentials test/test, and creates the bucket on boot. Without it the app still runs; only file uploads fail. Override with the S3_* variables in .env.example to point at real S3, MinIO, R2 or similar.

Other scripts:

Script What it does
npm test vitest: validation + repository tests against an in-memory SQLite
npm run typecheck tsc --noEmit
npm run lint eslint
npm run db:generate generate a new migration after editing src/db/schema.ts
npm run db:studio Drizzle Studio, a GUI over the local database
npm run s3:dev start the local MiniStack S3 emulator in Docker
S3_TEST=1 npm test also run the storage tests against the live emulator

Run in Docker

docker compose up --build
# or
docker build -t sophies-things .
docker run -p 3000:3000 -v things-data:/data sophies-things

The image is a multi-stage build on node:24-bookworm-slim using Next.js standalone output. Data lives on the /data volume (DATABASE_URL=file:/data/things.db). The compose file also starts a ministack service for attached files, persisted on its own volume; swap the S3_* environment for a real bucket in production. GET /api/health returns { "ok": true } once the database answers; use it for Kubernetes liveness and readiness probes. The container runs as a non-root user.

Users and auth

There is no login yet. src/lib/current-user.ts is the single place that answers "who is acting"; today it always returns the seeded dev-user. Every asset records created_by / updated_by, and every create, update and delete is appended to activity_log with the acting user, so wiring in real auth later means changing that one function.

Data model

models           manufacturer · name · identifier?           (deduplicated)
assets           asset_tag? (unique) · name? · model_id · status
                 purchase_date? · purchased_from? · purchase_amount? · purchase_currency
                 warranty_expiry? · created/updated by/at
asset_attributes asset_id · key · value · position            (unique per asset+key)
tags / asset_tags free-form labels, many-to-many
attachment_types name · position                               (Receipt, Manual, ... editable in Settings)
attachments      asset_id · type_id · file_name · content_type · size · storage_key · uploaded by/at
asset_notes      asset_id · body · created/updated by/at         (many per thing)
settings         key · value                                    (app preferences, e.g. notes per page)
activity_log     who did what, when
  • Adding a thing only needs a manufacturer and model; the model row is found-or-created in the same transaction, so a first-of-its-kind item is a single form.

  • Money is a decimal string plus an ISO currency code (default AUD).

  • Dates are YYYY-MM-DD strings; timestamps are unix milliseconds.

  • Status is one of active, stored, lent, disposed.

  • Files (PDF, JPEG, PNG, WebP, HEIC, up to 10 MB) are uploaded from a thing's page through a server action, each tagged with a file type (Receipt, Manual, Warranty, Repair, Photo, Other by default; edit the list in Settings), and stored at files/<asset>/<file id>/<name> in the bucket; the browser reads them back via GET /api/files/:id (?download=1 to save), so the bucket never needs to be public. Removing a type in Settings moves its files to Other, which cannot be removed.

  • Notes are a journal: add, edit or delete one straight from a thing's page, sort them newest or oldest first, and page through them. The number shown per page (5, 10, 20 or 50) is a preference saved in settings.

Suggested key/value details

Offered as one-click chips on the form; any key works.

serial_number, mac_address, ip_address, hostname, location, colour, capacity, os_version, firmware_version, account, licence_key, support_url, receipt_ref, dimensions, weight, power_rating.

Moving to Postgres

Everything database-specific is in src/db/:

  1. schema.ts: swap drizzle-orm/sqlite-core for drizzle-orm/pg-core. Same table and column names; integer timestamps become timestamp or bigint, text stays text.
  2. client.ts: replace @libsql/client + drizzle-orm/libsql with pg + drizzle-orm/node-postgres, and the migrator import to drizzle-orm/node-postgres/migrator. Drop the PRAGMA line.
  3. drizzle.config.ts: dialect: "postgresql", then npm run db:generate to produce fresh migrations.
  4. Set DATABASE_URL=postgres://....

The repositories under src/db/repositories/ use only the dialect-agnostic query builder, so they should not need changes. The tests create their own database via createDatabase(url) and will point at whatever you give them.

Layout

src/app                 routes (App Router)
  page.tsx              dashboard
  things/               list, new, [id], [id]/edit, actions.ts (server actions)
  models/ tags/ settings/
  api/health            probe · api/models  model autocomplete
src/components/shell    sidebar, top bar, page header
src/components/things   asset card, form, badges, delete dialog
src/components/ui       shadcn/ui
src/db                  schema, client, seed, repositories (all SQL lives here)
src/lib                 validation (zod), formatting, current-user seam
drizzle/                generated SQL migrations (applied at boot)
scripts/seed-demo.ts    example data