Architecture

Design Philosophy

The CLI is always an HTTP client. The server is always the authority.

Every operation goes through the Hono HTTP API. In local mode, the server is booted in-process for each command (~2ms overhead). In remote mode, the server is at a URL. The CLI code is identical in both cases.

CLI (HTTP client)         Server (Hono)              SQLite + Filesystem
  |                         |                           |
  |  POST /api/parts        |                           |
  |  { name: "NE555" }  --> |  Validate (Zod)           |
  |                         |  Call core logic           |
  |                         |  Write to database ------> |
  |                         |  Return JSON               |
  |  <-- 201 { id: 1 ... }  |                           |

This gives you exactly one code path for every operation. The CLI, the web UI, and any external client all use the same API.

Package Structure

packages/
  core/     Domain logic only. No HTTP awareness. Imports: Kysely, Zod.
  api/      Hono routes with Zod validation. Imports: core.
  cli/      Cliffy commands + Hono RPC client. Imports: api types only.

The one rule: data flows inward. core never imports from api or cli. The CLI never imports from core directly — it talks to the API.

Database

SQLite via Deno’s built-in node:sqlite module, accessed through a custom Kysely dialect (packages/core/src/dialect.ts). This bridges the synchronous DatabaseSync API to Kysely’s async interface.

Key database features:

Schema

17 tables. Source of truth: packages/core/src/schema.ts (Kysely Database interface).

categories          -- Hierarchical tree (parent_id self-ref)
parts               -- Core entity, stock cached via trigger
part_tags           -- Junction table (part_id, tag)
stock_lots          -- Quantity at a location with status
storage_locations   -- Hierarchical tree
suppliers           -- Vendor (name, url)
supplier_parts      -- Links parts to suppliers (SKU)
price_breaks        -- Quantity-based pricing tiers
part_parameters     -- Key/value with SI-parsed numeric
attachments         -- File metadata (content on disk)
projects            -- Things you build
bom_lines           -- Bill of materials for a project
build_orders        -- Consume BOM, deduct stock
purchase_orders     -- Replenish stock from suppliers
po_lines            -- Lines on a purchase order
users               -- Multi-user (serve mode only)
audit_log           -- Every mutation logged

Attachment Storage (BlobStore)

Attachment file content is stored via a BlobStore interface, not directly on disk. The default implementation is FsBlobStore, which writes to ~/.tray/blobs/. Storage is content-addressed: files are named by their sha256 hash, so identical files are automatically deduplicated.

interface BlobStore {
  /** Store a blob by key. Overwrites if key already exists. */
  put(key: string, data: Uint8Array): Promise<void>;
  /** Read a blob by key. Throws if not found. */
  get(key: string): Promise<Uint8Array>;
  /** Check if a blob exists by key. */
  has(key: string): Promise<boolean>;
  /** Delete a blob by key. No-op if not found. */
  delete(key: string): Promise<void>;
  /** Compute SHA-256 hash of data, return lowercase hex string. */
  hash(data: Uint8Array): Promise<string>;
}

The Hono app is created via createApp(db, { blobs?: BlobStore }). The Hono context carries db: Kysely<Database> and blobs: BlobStore — routes access these via c.var.db and c.var.blobs. The old attachments_dir: string approach has been replaced by this abstraction.

The CLI never touches the blob store. Upload and download always go through the API. The server handles hashing, dedup, thumbnail generation, and streaming.

API Routes

All routes are defined in packages/api/src/router.ts as a single Hono chain (required for RPC type inference).

Parts

MethodPathDescription
GET/api/partsList/filter parts
POST/api/partsCreate a part
GET/api/parts/:idGet part by ID or name
PATCH/api/parts/:idUpdate a part
DELETE/api/parts/:idDelete a part
PUT/api/parts/:id/thumbnailSet thumbnail from attachment
DELETE/api/parts/:id/thumbnailClear thumbnail
GET/api/parts/:id/suppliersSupplier parts for a part
GET/api/parts/:id/best-priceBest price across suppliers

Categories

MethodPathDescription
GET/api/categoriesList categories
POST/api/categoriesCreate category
POST/api/categories/resolveResolve/create category path
GET/api/categories/:idGet category with path
PATCH/api/categories/:idUpdate category
DELETE/api/categories/:idDelete category (re-parents children)
GET/api/categories/treeFull category tree

Search & Tags

MethodPathDescription
GET/api/search?q=...Full-text search (FTS5)
GET/api/tagsAll tags with counts

Stock & Locations

MethodPathDescription
POST/api/stock/addAdd stock (create/merge lot)
POST/api/stock/adjustAdjust with reason
POST/api/stock/moveMove between locations
GET/api/stock/:part_idList lots for a part
GET/api/locationsList locations
GET/api/locations/treeLocation tree
GET/api/locations/:idGet location with path
DELETE/api/locations/:idDelete location

Suppliers

MethodPathDescription
POST/api/suppliersCreate supplier
GET/api/suppliersList suppliers
GET/api/suppliers/:idGet supplier
PATCH/api/suppliers/:idUpdate supplier
DELETE/api/suppliers/:idDelete supplier
POST/api/supplier-partsLink part to supplier
GET/api/suppliers/:id/partsParts for a supplier
DELETE/api/supplier-parts/:idUnlink part from supplier

Attachments

MethodPathDescription
POST/api/attachmentsUpload file (multipart)
GET/api/attachments/:idGet metadata
GET/api/attachments/:id/fileDownload file
GET/api/attachments?entity_type=...&entity_id=...List attachments for entity
DELETE/api/attachments/:idDelete attachment

Projects & BOM

MethodPathDescription
POST/api/projectsCreate project
GET/api/projectsList projects
GET/api/projects/:idGet project with BOM
PATCH/api/projects/:idUpdate project
DELETE/api/projects/:idDelete project
POST/api/projects/:id/bomAdd BOM line
GET/api/projects/:id/bomGet BOM lines
DELETE/api/bom-lines/:idRemove BOM line
GET/api/projects/:id/checkCheck BOM availability

Builds

MethodPathDescription
POST/api/buildsCreate build order
POST/api/builds/:id/completeComplete build (deduct stock)
GET/api/buildsList build orders

Purchase Orders

MethodPathDescription
POST/api/purchase-ordersCreate PO
GET/api/purchase-ordersList POs
GET/api/purchase-orders/:idGet PO with lines
PATCH/api/purchase-orders/:idUpdate PO
POST/api/purchase-orders/:id/linesAdd PO line
PATCH/api/po-lines/:idUpdate PO line
POST/api/po-lines/:id/receiveReceive PO line (adds stock)

KiCad HTTP Library

MethodPathDescription
GET/kicad/v1/Root (schema info)
GET/kicad/v1/categories.jsonCategories for Symbol Chooser
GET/kicad/v1/parts/:category.jsonParts in a category
GET/kicad/v1/parts/:id.jsonFull part detail

Other

MethodPathDescription
GET/healthHealth check
GET/api/auditQuery audit log
GET/api/audit/:idGet audit entry

Testing

Tests across five layers:

Every test gets its own setupDb(":memory:"). No shared state, no fixtures, no cleanup.

deno task test            # All tests
deno task test:core       # Core only
deno task test:api        # API only
deno task test:e2e        # CLI end-to-end
deno task test:kicad      # KiCad contract tests
deno task test:scenarios  # Workflow scenario tests
deno task check           # Type checking
deno task lint            # Linting

Development

Adding a New Feature

  1. Add the domain function in packages/core/src/ (takes db as first parameter)
  2. Write tests in packages/core/tests/
  3. Add the API route in packages/api/src/router.ts
  4. Write API tests in packages/api/tests/
  5. Add the CLI command in packages/cli/src/commands/
  6. Export from packages/core/src/mod.ts
  7. Register the command in packages/cli/src/mod.ts

Conventions

Tech Stack

ComponentTechnology
RuntimeDeno 2+
DatabaseSQLite via node:sqlite (built into Deno)
Query builderKysely with custom NodeSqliteDialect
HTTP frameworkHono
RPC clientHono hc with full type inference
ValidationZod v4
CLI frameworkCliffy
Image processingImageScript (128x128 JPEG thumbnails)
Dependencies100% JSR, zero npm
Binary size~75MB (Deno compiled)
Startup time~50ms (compiled), ~200ms (Deno runtime)