# 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 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 ; solidhaus:storageTier solidhaus:HotStorage . 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 . ``` ### 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 ``` ### 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(); 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 |