kammer/ADDENDUM_V2.md
Christopher Mühl 3d246c00ec chore: scaffold SvelteKit project with Capacitor and Tailwind
- SvelteKit 2 (Svelte 5) with TypeScript, static adapter (SPA mode)
- Capacitor config for iOS/Android (so.toph.solidhaus)
- Tailwind CSS v4 via Vite plugin
- All dependencies: idb, nanoid, bwip-js, jspdf, Automerge, Solid
- Route stubs: dashboard, scan, items, locations, labels, settings
- Type definitions and ID generator utility
- Project specification and ontology files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 14:50:10 +01:00

722 lines
26 KiB
Markdown

# SolidHaus — Addendum: Pre-printed Labels, Item Types, Consumables, Svelte, Multi-User
**Supersedes / amends:** PROJECT_SPECIFICATION.md, CLAUDE_CODE_GUIDE.md
**Date:** 2026-02-26 (v2)
---
## 1. Pre-Printed Label Sheets (Not On-Demand)
### The Concept
Instead of printing one label at a time when creating items, **pre-generate sheets of labels with unused IDs**. You peel off a label, stick it on an item, then scan it in the app to associate it with the item's data. This is similar to how asset tags work in enterprise IT.
### ID Format Change
**Old:** 6-character nanoid
**New:** 7-character nanoid from a 30-char alphabet (no ambiguous chars)
```typescript
import { customAlphabet } from 'nanoid';
const ALPHABET = '23456789abcdefghjkmnpqrstuvwxyz'; // lowercase, no 0/o/1/i/l
const generateId = customAlphabet(ALPHABET, 7);
// Examples: "za3rbam", "k7xhw2n", "p4fvt8c"
```
- 30^7 ≈ 21.8 billion combinations — effectively infinite for household use
- Lowercase is easier to type manually and reads better in URIs
- 7 chars gives more headroom for pre-generating batches without collision worry
### Label Sheet Generation
Generate a PDF of label sheets (A4 or letter), each cell containing:
```
┌────────────────┐
│ ▄▄▄▄▄ haus:// │
│ █ █ za3rbam │
│ █ █ │
│ ▀▀▀▀▀ │
└────────────────┘
```
For Brother P-touch tape, generate a continuous strip:
```
┌──────┬──────┬──────┬──────┬──────┬──────┐
│ QR │ QR │ QR │ QR │ QR │ QR │
│za3rb │k7xhw │p4fvt │m2bck │r9gnj │w5dtx │
└──────┴──────┴──────┴──────┴──────┴──────┘
```
### Pre-Generation Workflow
```
1. [App: Settings > Label Sheets]
2. [Generate 50 new IDs] → creates 50 "unassigned" ID records in DB
3. [Download PDF / Print to P-touch]
4. [Physically apply labels to items around the house]
5. [Scan label with app → "New item with ID za3rbam" → Enter name/details]
```
The IDs exist in the database as "unassigned" entries until scanned and linked to an item. This means scanning an unknown-but-valid ID (one that matches the format) prompts: "This looks like a SolidHaus label. Create a new item?"
### Implementation
```typescript
interface PreGeneratedId {
id: string; // the 7-char nanoid
generatedAt: string;
assignedTo: string | null; // null = unassigned, else item shortId
batchId: string; // which print batch this came from
}
// New IndexedDB store
preGeneratedIds: {
key: string;
value: PreGeneratedId;
indexes: {
'by-batch': string;
'by-assigned': string;
};
}
```
---
## 2. URI Scheme: `haus://`
### Format
```
haus://za3rbam
```
- **Protocol:** `haus://` — short, memorable, unique enough
- **Path:** the 7-character item ID
- **Full example in QR code:** `haus://za3rbam`
### How QR Scanning + Deep Linking Works
| Platform | Behavior when scanning `haus://za3rbam` |
|----------|----------------------------------------|
| **Android (Capacitor native app)** | Intent filter catches `haus://` scheme → opens app directly to item |
| **iOS (Capacitor native app)** | URL scheme registered in Info.plist → opens app directly to item |
| **Android (installed PWA via Chrome)** | PWA won't catch custom scheme. Needs workaround (see below). |
| **iOS (Safari PWA)** | Won't work. Custom schemes need native app. |
| **Any phone, no app installed** | Camera shows "haus://za3rbam" as text. User must manually open app. |
### The Deep Link Problem for PWAs
Custom URI schemes (`haus://`) **do not work with PWAs**. PWAs can only handle `https://` URLs on Android (via Web App Manifest scope + intent filters that Chrome creates when installing PWA).
**Dual-encoding solution**: Encode QR codes with an HTTPS URL that falls back gracefully:
```
https://haus.toph.so/za3rbam
```
This web address:
1. **If Capacitor native app is installed:** Android App Links / iOS Universal Links intercept → opens app
2. **If PWA is installed on Android:** Chrome's intent filter for the PWA scope intercepts → opens PWA
3. **If nothing installed:** Opens browser → landing page says "Install SolidHaus" with item preview
The QR code itself contains `https://haus.toph.so/za3rbam`. The app's ID parser extracts the 7-char ID from either format:
```typescript
export function parseItemId(rawValue: string): string | null {
// Pattern 1: HTTPS URI — https://haus.toph.so/za3rbam
const httpsMatch = rawValue.match(/haus\.toph\.so\/([23456789a-hjkmnp-z]{7})$/);
if (httpsMatch) return httpsMatch[1];
// Pattern 2: Custom scheme — haus://za3rbam
const schemeMatch = rawValue.match(/^haus:\/\/([23456789a-hjkmnp-z]{7})$/);
if (schemeMatch) return schemeMatch[1];
// Pattern 3: Raw ID
const rawMatch = rawValue.match(/^[23456789a-hjkmnp-z]{7}$/);
if (rawMatch) return rawMatch[0];
return null;
}
```
### Recommendation
**Use `https://haus.toph.so/{id}` as the canonical QR content.** Register `haus://` as a custom scheme in the Capacitor native app for bonus direct-launch capability. The HTTPS URL works everywhere and degrades gracefully.
---
## 3. Item Types & Lifecycle
### Type Taxonomy
```typescript
type ItemType =
| 'durable' // Long-lasting items (tools, furniture, electronics)
| 'consumable' // Items that get used up (batteries, tape, screws)
| 'disposable' // Single-use items (paper plates, trash bags)
| 'perishable' // Food/drink with expiry (pantry, fridge, freezer)
| 'media' // Books, DVDs, games
| 'clothing' // Wearable items
| 'document' // Important papers, manuals, certificates
| 'container' // Boxes, bins — items that hold other items
;
```
### RDF Mapping
```turtle
solidhaus:ItemType a rdfs:Class ;
rdfs:label "Item Type"@en .
solidhaus:Durable a solidhaus:ItemType ;
rdfs:label "Durable"@en ;
rdfs:comment "Long-lasting items: tools, furniture, electronics, appliances."@en .
solidhaus:Consumable a solidhaus:ItemType ;
rdfs:label "Consumable"@en ;
rdfs:comment "Items that deplete with use: batteries, tape, screws, ink, cleaning supplies."@en .
solidhaus:Disposable a solidhaus:ItemType ;
rdfs:label "Disposable"@en ;
rdfs:comment "Single-use items: paper plates, trash bags, wipes."@en .
solidhaus:Perishable a solidhaus:ItemType ;
rdfs:label "Perishable"@en ;
rdfs:comment "Food and drink with expiration dates."@en .
solidhaus:itemType a rdf:Property ;
rdfs:domain schema:IndividualProduct ;
rdfs:range solidhaus:ItemType .
```
### Quantitative Properties (for Consumables & Perishables)
Items that get used up need quantity tracking:
```typescript
interface QuantityInfo {
currentAmount: number | null; // e.g., 750
originalAmount: number | null; // e.g., 1000
unit: string; // e.g., "ml", "g", "pcs", "sheets", "%"
lowThreshold: number | null; // alert when below this (e.g., 100)
expiryDate: string | null; // ISO date, for perishables
}
```
RDF properties:
```turtle
solidhaus:currentQuantity a rdf:Property ;
rdfs:label "Current Quantity"@en ;
rdfs:comment "Current amount remaining. Pair with quantityUnit."@en ;
rdfs:domain schema:IndividualProduct ;
rdfs:range xsd:decimal .
solidhaus:originalQuantity a rdf:Property ;
rdfs:label "Original Quantity"@en ;
rdfs:comment "Amount when full/new."@en ;
rdfs:domain schema:IndividualProduct ;
rdfs:range xsd:decimal .
solidhaus:quantityUnit a rdf:Property ;
rdfs:label "Quantity Unit"@en ;
rdfs:comment "Unit of measurement: ml, l, g, kg, pcs, sheets, %."@en ;
rdfs:domain schema:IndividualProduct ;
rdfs:range xsd:string .
solidhaus:lowThreshold a rdf:Property ;
rdfs:label "Low Threshold"@en ;
rdfs:comment "Alert when currentQuantity drops below this value."@en ;
rdfs:domain schema:IndividualProduct ;
rdfs:range xsd:decimal .
```
schema.org already has `schema:weight` and `schema:size`, but for dynamic tracking of "how much is left" we need the custom properties above.
### Pantry / Kitchen Extension
With perishable items, this naturally extends to pantry management:
```turtle
<items/olive_oil_01>
a schema:IndividualProduct ;
schema:name "Olivenöl — Bertolli Extra Vergine" ;
solidhaus:itemType solidhaus:Consumable ;
solidhaus:currentQuantity 400 ;
solidhaus:originalQuantity 1000 ;
solidhaus:quantityUnit "ml" ;
solidhaus:lowThreshold 100 ;
solidhaus:custodyState solidhaus:CheckedIn ;
solidhaus:supposedToBeAt <locations/kueche-regal> ;
solidhaus:storageTier solidhaus:HotStorage .
<items/flour_01>
a schema:IndividualProduct ;
schema:name "Mehl Type 405" ;
solidhaus:itemType solidhaus:Perishable ;
solidhaus:currentQuantity 800 ;
solidhaus:originalQuantity 1000 ;
solidhaus:quantityUnit "g" ;
solidhaus:lowThreshold 200 ;
schema:expires "2026-09-15"^^xsd:date ;
solidhaus:supposedToBeAt <locations/kueche-schrank> .
```
### Quick-Update UX for Consumables
When scanning a consumable, offer a fast "update quantity" flow:
```
[Scan olive oil bottle]
→ [Card shows: "Olivenöl Bertolli — 400ml / 1000ml (40%)"]
→ [Quick actions: "Almost empty" | "Half left" | "Just opened" | "Custom"]
→ [Tap "Almost empty" → sets to ~100ml]
→ [If below threshold → "Add to shopping list?" prompt]
```
---
## 4. Barcode Format Support
### What Formats Can Be Read?
| Format | Barcode Detection API (Chrome) | ZXing (fallback) | Capacitor ML Kit | Notes |
|--------|-------------------------------|-------------------|-----------------|-------|
| **QR Code** | ✅ | ✅ | ✅ | Our primary format |
| **DataMatrix** | ✅ | ✅ | ✅ | Very compact, great for small labels |
| **Code 128** | ✅ | ✅ | ✅ | Linear, good for tape labels |
| **EAN-13/UPC** | ✅ | ✅ | ✅ | Product barcodes (scan existing products) |
| **Aztec** | ✅ | ✅ | ✅ | |
| **PDF417** | ✅ | ✅ | ✅ | |
**All common formats are supported across all platforms.** QR is the best default because:
- Highest information density for small size
- Error correction built in
- Phone cameras auto-recognize and offer to open URLs
- Works well even when slightly damaged or at angle
DataMatrix is a great alternative for very small labels (it's more compact than QR at small sizes).
### Scanning Existing Product Barcodes
The app should also recognize standard EAN/UPC barcodes on commercial products. When scanning a product barcode (e.g., the EAN-13 on a bottle of olive oil):
1. Look up in local database first (maybe you've already cataloged it)
2. If not found, optionally query an open product database (Open Food Facts, etc.)
3. Offer to create a new item pre-filled with product info
This is a nice-to-have for Phase 2+.
---
## 5. Framework: SvelteKit + Capacitor (Not React)
### Why Svelte/SvelteKit Over React
Given the preference for Svelte, the recommended stack is:
| Layer | Technology |
|-------|-----------|
| **Framework** | SvelteKit 2 (Svelte 5 with runes) |
| **Build** | Vite (built into SvelteKit) |
| **Native wrapper** | Capacitor (Android + iOS) |
| **Desktop** | Tauri 2 (optional, later) |
| **Styling** | Tailwind CSS 4 |
| **State** | Svelte 5 runes ($state, $derived, $effect) — no external state library needed |
| **Routing** | SvelteKit file-based routing |
| **Barcode (native)** | @capacitor/barcode-scanner or @capacitor-mlkit/barcode-scanning |
| **Barcode (web)** | Barcode Detection API + ZXing fallback |
| **Local DB** | IndexedDB via idb (same as before) |
| **CRDT** | Automerge (for multi-user, see §6) |
### SvelteKit + Capacitor Setup
This is a well-documented, production-proven path:
```bash
# Create SvelteKit project
npx sv create solidhaus
cd solidhaus
# Add static adapter (required for Capacitor)
npm install -D @sveltejs/adapter-static
# Add Capacitor
npm install @capacitor/core @capacitor/cli
npx cap init solidhaus so.toph.solidhaus --web-dir build
# Add platforms
npm install @capacitor/android @capacitor/ios
npx cap add android
npx cap add ios
# Add barcode scanner
npm install @capacitor/barcode-scanner
# or for ML Kit (more powerful):
npm install @capacitor-mlkit/barcode-scanning
# Other deps
npm install nanoid idb bwip-js date-fns
npm install -D tailwindcss @tailwindcss/vite
```
**svelte.config.js:**
```javascript
import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
export default {
preprocess: vitePreprocess(),
kit: {
adapter: adapter({
fallback: 'index.html' // SPA mode for Capacitor
})
}
};
```
**src/routes/+layout.ts:**
```typescript
export const ssr = false; // Capacitor needs client-side only
export const prerender = false;
```
### Deep Linking with Capacitor
Register the custom URI scheme and HTTPS App Links:
**capacitor.config.ts:**
```typescript
const config: CapacitorConfig = {
appId: 'so.toph.solidhaus',
appName: 'SolidHaus',
webDir: 'build',
server: {
// For deep linking via HTTPS
androidScheme: 'https'
},
plugins: {
// App Links / Universal Links configuration
}
};
```
**AndroidManifest.xml additions:**
```xml
<!-- Custom scheme: haus:// -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="haus" />
</intent-filter>
<!-- HTTPS App Links: https://haus.toph.so/* -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="haus.toph.so" />
</intent-filter>
```
### Is There a "Svelte Native"?
**Short answer: not really, not in a usable way.**
- **NativeScript + Svelte** existed but is stuck on Svelte 4 and not actively maintained for Svelte 5.
- **Lynx** (ByteDance) has an agnostic core but only works with React currently. Svelte support requires a custom renderer API that's being worked on by the Mainmatter team but isn't ready.
- **Capacitor** is the pragmatic choice — it wraps your web app in a native WebView with full access to native APIs (camera, Bluetooth, file system, deep links). It's not "truly native" rendering, but for an inventory app the performance difference is negligible.
- **Tauri 2** supports Android and iOS and could be an alternative to Capacitor, with Rust backend instead of Java/Swift. More bleeding-edge but the SvelteKit integration is officially documented.
**Recommendation: SvelteKit + Capacitor.** It's the most mature path for Svelte on mobile with native features (barcode scanning, deep links, Bluetooth).
---
## 6. Multi-User & Offline-First (Hard Requirement)
### The Scenario
Multiple household members scan/update items independently:
- Person A scans the drill in the workshop (phone offline in basement)
- Person B logs the drill as lent to neighbor (phone online upstairs)
- Both changes eventually sync and resolve
### Architecture: CRDT + Solid Pod
With multi-user as a hard requirement, simple LWW is no longer sufficient. We need CRDTs.
**Recommended approach: Automerge as the sync layer, Solid Pod as the persistence/sharing layer.**
```
Device A (Phone) Device B (Tablet)
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Local │ │ Local │
│ Automerge │ │ Automerge │
│ Document │ │ Document │
│ (IndexedDB) │ │ (IndexedDB) │
└──────┬──────┘ └──────┬──────┘
│ │
▼ ▼
┌──────────────────────────────┐
│ Sync Server │
│ (automerge-repo-server │
│ on NixOS infrastructure) │
└──────────────┬───────────────┘
┌───────────┐
│ Solid Pod │
│ (RDF export│
│ periodic) │
└───────────┘
```
### How It Works
1. **Each device** maintains a local Automerge document with all inventory data
2. **Automerge-repo** handles sync between devices via a WebSocket relay server
3. **Offline**: Changes accumulate locally in the Automerge doc. When back online, automerge-repo syncs automatically — merges are conflict-free by design
4. **Solid Pod integration**: A periodic job serializes the Automerge state to RDF/Turtle and writes it to the Solid Pod. This is the "canonical" linked data representation. The Solid Pod is the long-term archive and interoperability layer, not the real-time sync layer.
### Why Not Use Solid Pod Directly for Sync?
Solid Pods use HTTP REST (LDP). They're great for storage and sharing but not for real-time multi-device sync:
- No built-in conflict resolution
- Polling-based (Solid Notifications is still maturing)
- High latency for offline→online sync bursts
CRDTs via Automerge solve the hard sync problem; Solid solves the data ownership/interoperability problem. They're complementary.
### Automerge Document Structure
```typescript
import { next as Automerge } from '@automerge/automerge';
interface InventoryDoc {
items: { [shortId: string]: Item };
locations: { [id: string]: Location };
sightings: { [id: string]: Sighting };
preGeneratedIds: { [id: string]: PreGeneratedId };
}
// Create and modify
let doc = Automerge.init<InventoryDoc>();
doc = Automerge.change(doc, 'Add new item', d => {
d.items['za3rbam'] = {
shortId: 'za3rbam',
name: 'Bosch Drill',
custodyState: 'checked-in',
// ...
};
});
```
### Auth & Permissions
- Each household member authenticates with their Solid WebID
- The Automerge sync server can use simple shared-secret or Solid-based auth
- All household members have read/write access to the shared inventory
- Personal items can be marked as `visibility: 'private'` (only visible to owner)
### Sync Server (NixOS)
The automerge-repo sync server is a small Node.js process:
```nix
# NixOS module sketch
services.solidhaus-sync = {
enable = true;
port = 3030;
package = pkgs.nodejs;
# automerge-repo-network-websocket server
};
```
This runs alongside your existing Solid pod (CSS) and other infrastructure.
### Future: NextGraph
If NextGraph reaches production readiness, it could replace the entire Automerge + Solid Pod stack with a single integrated solution: CRDT-native RDF, E2EE, P2P sync, SPARQL queries — all in one. Keep monitoring.
---
## 7. Updated Project Structure (SvelteKit)
```
solidhaus/
├── src/
│ ├── routes/
│ │ ├── +layout.svelte # App shell, bottom nav
│ │ ├── +layout.ts # SSR disabled
│ │ ├── +page.svelte # Dashboard
│ │ ├── scan/
│ │ │ └── +page.svelte # Scanner view
│ │ ├── items/
│ │ │ ├── +page.svelte # Item list
│ │ │ ├── new/+page.svelte # Create item
│ │ │ └── [id]/
│ │ │ ├── +page.svelte # Item detail
│ │ │ └── edit/+page.svelte
│ │ ├── locations/
│ │ │ └── +page.svelte # Location tree
│ │ ├── labels/
│ │ │ └── +page.svelte # Label sheet generator + print queue
│ │ └── settings/
│ │ └── +page.svelte # Settings, sync status, pod config
│ ├── lib/
│ │ ├── components/
│ │ │ ├── Scanner.svelte
│ │ │ ├── ItemCard.svelte
│ │ │ ├── ItemForm.svelte
│ │ │ ├── LocationPicker.svelte
│ │ │ ├── LocationTree.svelte
│ │ │ ├── QuantitySlider.svelte # For consumables
│ │ │ ├── ConfidenceBadge.svelte
│ │ │ ├── CustodyBadge.svelte
│ │ │ ├── LabelPreview.svelte
│ │ │ ├── BottomNav.svelte
│ │ │ └── PhotoCapture.svelte
│ │ ├── data/
│ │ │ ├── db.ts # IndexedDB
│ │ │ ├── items.ts
│ │ │ ├── locations.ts
│ │ │ ├── sightings.ts
│ │ │ └── labels.ts # Pre-generated ID management
│ │ ├── sync/
│ │ │ ├── automerge.ts # Automerge doc management
│ │ │ ├── repo.ts # automerge-repo setup
│ │ │ └── solid-export.ts # Periodic RDF export to Solid Pod
│ │ ├── solid/
│ │ │ ├── auth.ts
│ │ │ ├── rdf.ts
│ │ │ └── pod.ts
│ │ ├── scanning/
│ │ │ ├── detector.ts # Barcode detection (native + web)
│ │ │ └── parser.ts # ID extraction from barcodes
│ │ ├── printing/
│ │ │ ├── labelSheet.ts # PDF label sheet generation
│ │ │ ├── tapeStrip.ts # P-touch tape strip generation
│ │ │ └── printServer.ts # HTTP print server client
│ │ ├── stores/
│ │ │ ├── inventory.svelte.ts # Svelte 5 runes-based stores
│ │ │ ├── scan.svelte.ts
│ │ │ ├── sync.svelte.ts
│ │ │ └── settings.svelte.ts
│ │ ├── types/
│ │ │ ├── item.ts
│ │ │ ├── location.ts
│ │ │ ├── sighting.ts
│ │ │ └── quantity.ts
│ │ ├── ontology/
│ │ │ ├── solidhaus.ts # Namespace constants
│ │ │ └── namespaces.ts
│ │ └── utils/
│ │ ├── id.ts # nanoid generator
│ │ ├── confidence.ts
│ │ └── format.ts # Quantity formatting ("400ml / 1L")
│ └── app.html
├── static/
│ ├── manifest.json
│ └── icons/
├── ontology/
│ └── solidhaus.ttl
├── android/ # Capacitor Android project
├── ios/ # Capacitor iOS project
├── svelte.config.js
├── capacitor.config.ts
├── vite.config.ts
├── tailwind.config.ts
├── package.json
└── tsconfig.json
```
---
## 8. Updated IndexedDB Schema (Items)
```typescript
interface Item {
shortId: string; // 7-char nanoid
name: string;
description: string;
category: string;
brand: string;
serialNumber: string;
color: string;
purchaseDate: string | null;
// Item type
itemType: 'durable' | 'consumable' | 'disposable' | 'perishable'
| 'media' | 'clothing' | 'document' | 'container';
// Quantity (for consumables/perishables)
currentQuantity: number | null;
originalQuantity: number | null;
quantityUnit: string | null; // "ml", "g", "pcs", "sheets", "%"
lowThreshold: number | null;
expiryDate: string | null; // ISO date
// Barcode
barcodeUri: string; // "https://haus.toph.so/za3rbam"
barcodeFormat: 'qr' | 'datamatrix' | 'code128';
photoIds: string[];
// Location tracking
lastSeenAt: string | null;
lastSeenTimestamp: string | null;
lastUsedAt: string | null;
supposedToBeAt: string | null;
locationConfidence: 'confirmed' | 'likely' | 'assumed' | 'unknown';
// Custody state
custodyState: 'checked-in' | 'checked-out';
checkedOutSince: string | null;
checkedOutReason: 'in-use' | 'in-transit' | 'lent' | 'in-repair' | 'temporary' | 'consumed' | null;
checkedOutFrom: string | null;
checkedOutTo: string | null;
checkedOutNote: string | null;
// Storage
storageTier: 'hot' | 'warm' | 'cold';
storageContainerId: string | null;
storageContainerLabel: string | null;
// Label
labelPrinted: boolean;
labelPrintedAt: string | null;
labelBatchId: string | null; // which pre-gen batch
// Metadata
createdAt: string;
updatedAt: string;
tags: string[];
createdBy: string | null; // WebID of creator (multi-user)
}
```
---
## 9. Summary of All Decisions
| Decision | Choice | Rationale |
|----------|--------|-----------|
| ID length | 7 characters | More headroom for pre-generation batches |
| ID alphabet | Lowercase + digits, no ambiguous | Easier to type in URIs |
| QR content | `https://haus.toph.so/{id}` | Works everywhere, degrades gracefully |
| Custom scheme | `haus://` (bonus, native only) | Direct app launch when native app installed |
| Labels | Pre-printed sheets/strips | Faster than on-demand, batch workflow |
| Framework | SvelteKit 5 + Capacitor | Svelte preference, native features via Capacitor |
| Native mobile | Capacitor (not Svelte Native) | Svelte Native doesn't exist in usable form |
| Desktop | Tauri 2 (future) | Rust backend, SvelteKit frontend, officially supported |
| Multi-user sync | Automerge (CRDT) | Conflict-free offline-first sync between household members |
| Data ownership | Solid Pod (RDF export) | Long-term archive, interoperability, data sovereignty |
| Real-time sync | automerge-repo WebSocket server | Low-latency device-to-device sync |
| Barcode scanning (native) | @capacitor-mlkit/barcode-scanning | ML Kit is fast, supports all formats |
| Barcode scanning (web) | Barcode Detection API + ZXing | Progressive enhancement |
| Item types | durable/consumable/disposable/perishable/... | Naturally extends to pantry tracking |
| Quantity tracking | currentQuantity + unit + threshold | Consumables need "how much is left" |
| Printing | Pre-gen PDF sheets + P-touch strips via print server | Batch workflow, NixOS service |