Open Source · Node.js + Postgres + Vue 3 · AGPL v3

Sundries

// domain-agnostic in what it sells, opinionated in how it holds together

A self-hosted ecommerce platform for physical goods.

Single items, items with option variants (color, quality, size), and goods cut to length (feet, yards, meters) are all first-class — over multi-warehouse FIFO-costed inventory, checkout-time reservations, and an immutable audit trail. The same codebase scales down to a single-maker shop doing a few orders a week and up to a multi-instance cloud deployment; the difference is configuration, not code.

Active development 4 test suites in CI AGPL v3 licensed
The Sundries storefront showing a unit good, a good with option variants, and a cut-to-length good priced per foot

Sells anything, remembers everything.

Most ecommerce platforms pick a product model and make you fit your catalog to it. Sundries doesn't assume what you're selling — a plain unit good, a product with a full option-variant matrix, or a cut-to-length good priced per foot are all handled by the same catalog, cart, and inventory model, over multi-warehouse stock with real FIFO costing.

The parts most platforms bolt on as an afterthought are built into the database itself: inventory balances are protected by CHECK constraints, not application code; every stock movement writes a costed, append-only ledger row via trigger; and oversell is a constraint violation, not a bug report. Background work — reservation sweeping, backorder fulfillment, search indexing, scheduled reports — runs on Postgres through pg-boss, so there's no broker to stand up.

It's the same engineering instinct behind naTE, applied to a different domain: push correctness down into the layer that can't be skipped, and keep the moving parts a solo operator can actually run. Free and open source under AGPL v3.

Built around what a real shop needs.

Four thematic areas, from storefront to database trigger.

Catalog & Storefront

Sells single items, variant matrices, and goods by the foot — the same way

  • Product catalog covers plain unit goods, Color × Size-style option variants, and cut-to-length goods priced per foot/yard/meter — no separate code paths per product type
  • Cart, promotions, and both member and guest checkout, with guest orders creating an implicit customer account
  • Passwordless login — emailed one-time codes with brute-force lockout; sessions use rotating refresh tokens, and reuse of a retired token revokes the whole token family
  • Order history with per-line return requests, and a configurable return window
  • Swappable payment provider — a fake local simulator drives the exact same webhook pipeline as Stripe, so checkout is fully testable with no keys and no network

Inventory & Fulfillment

FIFO costing that shows up in the ledger, not just the README

  • Multi-warehouse stock with FIFO cost layers — every issue is stamped with its blended unit cost at insert time, so COGS for any period is just a sum over OUT rows
  • Stock transfers move inventory through a real in-transit transport warehouse, not an instantaneous teleport between locations
  • Purchase orders receive at line level and write costed IN ledger rows; vendors are created directly from the Purchasing screen
  • Backorders are fulfilled and back-in-stock notifications sent automatically by a background job when stock is received
  • Remnant write-offs are costed the same way as sales and show up correctly in shrinkage reporting

Staff Console & Admin

A permission-filtered console, not an all-or-nothing admin panel

  • Sidebar shows staff only the sections their roles can use — grouped as Sales, Catalog, Inventory, Admin, and Reports
  • User management, a roles & permissions editor (the admin and customer roles are locked against deletion), and store-wide settings
  • Every endpoint's required permission lives in one code-only map, validated against the live router and the permissions table at boot — an unguarded route or an unknown permission code fails startup rather than shipping silently
  • The audit log reads an immutable, trigger-written table with the acting user, IP, and correlation ID on every row
  • Reports are self-contained files — metadata, params, columns, and SQL together — discovered automatically at boot; a broken report file is logged and skipped, never crashes the API

Architecture & Ops

Provider adapters everywhere it matters

  • Mail, search, payments, image storage, and the job queue itself all sit behind a provider interface, switched entirely by environment variable — MAIL_PROVIDER, SEARCH_PROVIDER, PAYMENT_PROVIDER, IMAGE_PROVIDER, QUEUE_PROVIDER
  • Three deployment tiers from one codebase — a laptop running Docker Compose for local dev, a single app server + single DB server for a small on-prem shop, and a multi-instance API fleet behind a load balancer with managed Postgres for cloud scale-up
  • Ledger partitioning (inventory.partition_months) is a config flag, not a migration — flip it on before volume arrives, since carving partitions out of an existing default partition needs manual data movement
  • Security headers (CSP, HSTS, frame options) ship in the app via helmet; per-IP rate limiting is left to the reverse proxy or WAF in front, by design

Feature spotlight

Integrity lives in the database, not the application

It's easy to write inventory math correctly once and have it drift the moment a second code path touches the same table. Sundries avoids that by refusing to let the application be the thing enforcing correctness at all.

inventory_balances.qty_on_hand − qty_reserved ≥ 0 is a CHECK constraint — overselling fails at the database, not somewhere downstream in a report. Every insert into inventory_transactions updates balances and FIFO cost layers in the same transaction, via triggers, so no writer — API instance, background worker, or future integration — can accidentally skip it. inventory_transactions, payment_events, and audit_log reject UPDATE and DELETE at the trigger level; corrections are new rows, never edits. That means the safety doesn't depend on every future contributor remembering the rule — the schema itself won't allow the alternative.

Feature spotlight

One codebase, three deployment tiers

A lot of self-hosted software either stays a toy past a certain order volume, or demands Kubernetes from day one. Sundries is built to grow into scale rather than assume it.

Locally, docker compose up -d brings up Postgres and a mail catcher, and npm run setup && npm run seed:demo gets a working storefront with FIFO costing already visible in the ledger. A single-shop on-prem deployment is one app server and one DB server behind nginx, with cron only needed for the nightly backup — every other background job runs inside the API process itself, queued on the Postgres you already have. Scaling up from there is additive, not a rewrite: swap IMAGE_PROVIDER to S3 once there's more than one API instance, point SEARCH_PROVIDER at OpenSearch when Postgres full-text search isn't enough, and set JOBS_INLINE=false to move background work onto dedicated worker instances. The sizing target is 500k orders/day on a mid-size RDS instance, because nothing in the hot path grows with history.

An architecture that enforces itself.

Sundries treats the database as more than storage. Balance maintenance, FIFO costing, reservation math, oversell guards, and ledger immutability are triggers, functions, and CHECK constraints — safe under any number of API instances without coordination.

The layering is conventional and deliberate: routes → controllers → services (domain logic) → models (all SQL, no ORM). inventory_transactions and payment_events are append-only; corrections are reversing entries, and audit_log records privileged changes automatically via database triggers.

Mail, search, payments, image storage, and the background job queue itself all sit behind a provider port, switched entirely by environment variable. Background work runs through pg-boss, a Postgres-backed durable queue with retries and cron scheduling — no Redis or RabbitMQ to operate. Catalog images key on an opaque storage path rather than a URL, so switching from local disk to S3 + CDN is configuration plus a file copy, never a data migration.

Test coverage spans four separate suites: Jest for API/DB integration, node:test for the queue adapter (pg-boss is ESM-only, so it can't share a runtime with Jest), Vitest for frontend units, and Playwright for end-to-end — all four run in CI on every push and pull request.

Stack at a glance

BackendNode.js + Express
DatabasePostgreSQL (plain SQL)
FrontendVue 3
QueuePostgres via pg-boss
SearchPostgres / OpenSearch
PaymentsStripe / fake simulator
ImagesLocal disk / S3 + CDN
TestsJest · Vitest · Playwright
CIGitHub Actions
ArchitectureLayered + Provider Ports
LicenseAGPL v3

Operational invariants.

Not a roadmap — a set of guarantees the schema itself holds, regardless of what the application code does. Details and the full contributing guide are on the GitHub repository.

Constraint

Overselling is a violation, not a bug report

qty_on_hand − qty_reserved ≥ 0 is enforced at the database level, on every warehouse row.

Trigger

No writer can skip the ledger

Every inventory transaction updates balances and FIFO cost layers in the same transaction, automatically.

Append-only

Corrections are new rows, never edits

inventory_transactions, payment_events, and audit_log reject UPDATE and DELETE at the trigger level.

Transit

In-transit stock is a real balance

Transfers move inventory through a transport-type warehouse, tracked with carrier and manifest detail — not an instant teleport.

Run it locally.

The full local stack comes up from a clean checkout with Docker Compose handling Postgres and mail — no external services required to see FIFO costing, variant handling, and the fake-payment checkout pipeline working end to end. Source is freely available under AGPL v3; the full local, on-prem, and cloud deployment guides are in the repository README.

1 docker compose up -d
2 npm run setup && npm run seed:demo && npm run dev
← See Sundries in the context of my full work