What's in it
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
transportwarehouse, 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
adminandcustomerroles 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.