Desktop-first jewelry repair intake + ticketing web app
Facet is a desktop-first web application for jewelry stores to intake repair jobs, capture photos, print a customer receipt plus a matching physical tag, and manage a clear work queue. The queue is FIFO by default, with a Rush flag for priority overrides. The system tracks status, pricing (quoted vs actual), storage location, and employee attribution for intake, work, and closure.
Core principles:
- Desktop-first, fast data entry
- Photo evidence at intake
- FIFO queue with a Rush override
- Employee attribution for accountability
- Printable receipts/tags to bind the physical item to the ticket
- Offline-capable intake (PWA) for internet outages
- Repair ticket intake with required photos
- Printable receipt (PDF) plus physical label/tag (Ticket ID plus optional QR)
- Status workflow: Intake → In Progress → Waiting on Parts → Ready for Pickup → Closed
- Queue ordering: Rush first, then FIFO
- Quote amount plus actual charged at close
- Storage location tracking (bin, safe drawer, etc.)
- Employee attribution (intake / work / close / key edits)
- Audit trail (status changes, pricing edits, photo uploads)
- Drag-and-drop lane ordering (explicit rank)
- Customer notifications (email/SMS)
- Customer portal (“track my repair”)
- Multi-tenant (multiple stores)
High-level components:
- Web UI (SvelteKit): queue view, intake form, ticket detail, admin
- API (Rust Axum): authentication/authorization, ticket CRUD, print endpoints, audit trail
- Postgres: tickets, customers, employees, status history, notes, pricing
- Object Storage (S3 compatible): photos and generated PDFs/labels (optional)
- PWA Offline Layer: local ticket creation plus background sync when online returns
Data flow:
- Intake creates ticket plus uploads photos (or stores locally when offline).
- Backend persists ticket and photo references.
- Frontend requests print payload and renders receipt/label templates.
- Queue pulls tickets by status lanes and sorts Rush plus FIFO.
- Frontend: SvelteKit (TypeScript recommended)
- Backend: Rust plus Axum
- Database: Postgres
- Storage: S3-compatible (DigitalOcean Spaces)
- Local dev orchestration: Docker Compose
- Migrations: sqlx (recommended) or Diesel (choose one and standardize)
Suggested structure (adjust as needed):
/
apps/
web/ # SvelteKit frontend
api/ # Rust Axum backend
packages/
shared/ # shared types (optional), schemas, utilities
infra/
docker/ # docker compose, nginx, local configs
docs/
PRD.md # detailed product requirements
VISION.md # project vision and feature scope
AGENTS.md # agent instructions for coding assistants
README.md # this file (dev setup)
- Docker plus Docker Compose
- Node.js (LTS)
- Rust toolchain (stable)
- (Optional) sqlx-cli for migrations
-
Create environment files:
apps/api/.envapps/web/.env
-
Start dependencies:
docker compose up -d db- Run API:
cd apps/api
cargo run --bin api- Run Web:
cd apps/web
npm install
npm run dev- Open:
- Web: http://localhost:5173
- API: http://localhost:3001 (or configured port)
Required:
DATABASE_URL=postgres://facet:facet_dev_password@localhost:5432/facet_devAPP_ENV=developmentPORT=3001
Photos / object storage:
S3_ENDPOINT=https://nyc3.digitaloceanspaces.com(example)S3_BUCKET=facet-photos-devS3_ACCESS_KEY=...S3_SECRET_KEY=...S3_REGION=us-east-1(some S3-compatible providers ignore, but keep it)
Security:
ADMIN_PIN_HASH=...(hash, not plain text)EMPLOYEE_PIN_MIN_LENGTH=4SIGNED_URL_TTL_SECONDS=300
Printing:
RECEIPT_TEMPLATE=defaultLABEL_TEMPLATE=defaultSTORE_NAME=Example JewelersSTORE_PHONE=...STORE_ADDRESS_LINE1=...
PUBLIC_API_BASE_URL=http://localhost:8080PUBLIC_APP_NAME=FacetPUBLIC_ENABLE_OFFLINE=true
Recommended approach: sqlx migrations.
Example workflow:
- Create migration:
cd apps/api
sqlx migrate add create_tickets- Apply migrations:
sqlx migrate runKey tables (conceptual):
employees(id, name, pin_hash, role)tickets(uuid, friendly_code, status, rush, promise_date, storage_location, quote, actual, etc.)ticket_photos(id, ticket_uuid, storage_key, uploaded_by, created_at)ticket_status_history(ticket_uuid, from_status, to_status, changed_by, changed_at)ticket_notes(ticket_uuid, note, created_by, created_at)
Photos are stored in S3-compatible storage and referenced from Postgres.
Best practice:
- Upload via API to validate file type/size and attach
ticket_uuid. - Store originals, optionally generate thumbnails later.
- Serve images via signed URLs (especially important in multi-tenant future).
Constraints (suggested):
- Max photo size: 10 MB
- Allowed content types: image/jpeg, image/png, image/webp
- Max photos per ticket: 10 (configurable)
- Receipt: PDF with Ticket ID, customer name, item summary, requested work, quote (optional), promise date (optional), created date/time, store header, and optional QR.
- Tag/Label: Ticket ID large plus optional QR plus short item descriptor.
-
Browser-native printing (MVP-friendly)
- Web renders receipt/label HTML templates
- Print via
window.print()with print CSS - Pros: simple, no hardware coupling
- Cons: printer variations, label printers can be fiddly
-
Server-generated PDFs (more controlled)
- API generates PDFs (receipt/label)
- Web opens PDF for printing
- Pros: consistent output
- Cons: more work
MVP suggestion:
- Use browser printing with strong print CSS.
- Add server PDF generation later if printer chaos becomes a recurring villain.
Label printers:
- If using thermal label printers (Zebra, Brother), expect template tuning.
- Consider supporting a fixed label size in settings (ex: 2x1 inch).
Goal: allow intake creation (including photos) when internet is down.
Approach:
- Use IndexedDB to store:
- draft tickets
- photos (as blobs)
- pending sync queue
- Ticket IDs generated client-side as UUID.
- Sync worker pushes tickets/photos when back online.
- Conflict policy (MVP):
- server wins for existing ticket fields
- offline-created tickets are appended
- display “Sync issues” banner if any uploads fail
- Unit tests for:
- queue ordering (rush plus FIFO)
- status transitions and history creation
- pricing edits audit
- Integration tests with Postgres (docker)
- Smoke tests for:
- intake form required fields plus photo requirement
- queue sorting behavior
- printing template render
- Docker Compose services:
- api
- web (or static build served by nginx)
- postgres
- Use DigitalOcean Spaces for photos
- Backups:
- nightly Postgres dump plus store to Spaces (or DO backups)
- retention policy (ex: 7 daily, 4 weekly)
- Move Postgres to managed database
- Move web to DO App Platform or CDN
- Keep API as container service
- Add centralized logging (Grafana/Loki or a managed solution)
- Never store PINs in plain text. Store salted hashes.
- Use TLS in production.
- Validate and sanitize file uploads.
- Maintain audit trails for critical actions.
- If multi-tenant is introduced:
- strict tenant scoping in every query
- signed URLs with short TTL
- per-tenant buckets or key prefixes
- Intake plus photos (required)
- Receipt plus label printing
- FIFO plus rush queue
- Status transitions plus audit trail
- Quote plus actual
- Employee attribution via ID/PIN entry
- Offline intake (PWA)
- Drag-and-drop ordering within lanes
- Customer notifications (email/SMS)
- Better reporting (throughput, overdue, estimate vs actual)
- Multi-tenant productization
- Customer portal
- POS integrations (optional)