Deployment readiness: blueprint and required gates · live milestone.
Centralized inventory operations: a Shopify + Square sync engine with a web dashboard (neutral blue palette, light and dark themes), backed by SQLite in production. Runs as a Ruby on Rails 8.1 app (based on the Shopify Ruby app template) and is currently hosted at herbalhealers.rupertpos.com.
Shopify ──┐ ┌── Dashboard
│ ├── Inventory
Square ───┼── sync engine ──▶ SQLite DB ──▶ GUI
│ (Rails jobs) │ (Rails + Tailwind)
└────────────────────┴── Settings (env, backup/restore)
| Path | Purpose |
|---|---|
web/ |
The Rails 8.1 app: sync engine services, the dashboard GUI (app/assets/tailwind/application.css holds the palette; bin/check-palette-contrast verifies it), JSON APIs |
mobile/ |
Staff iPhone/Android app (see mobile/README.md) |
docs/ |
Docs site, plans, audits and research (see docs/README.md) |
deploy/ |
SER8 (Incus) systemd units (web, jobs, backup) and host-side watcher/backup scripts (see Production and deployment) |
bin/ |
Host scripts (backup.sh) |
shopify.app.toml |
Shopify app config (scopes, webhooks) |
Dockerfile, docker-compose.yml |
Optional container deployment |
Requirements: Ruby 4.0.9 (rbenv), Bundler, and Node (for the Shopify CLI and JavaScript tests). SQLite is the only supported database; no database server is needed.
cp .env.example .env # from repo root; configure local credentials
cd web
bundle install
bin/rails db:prepare
bin/rails tailwindcss:build
bin/rails server # http://localhost:3000The development database is web/storage/development.sqlite3 (override with
RUPERT_SQLITE_DATABASE_DEVELOPMENT). For production operations, tuning and
recovery see SQLite and Incus operations.
Leave DATABASE_URL unset: Rupert rejects every nonempty value, including a
SQLite URL. Set database locations with the per-environment
RUPERT_SQLITE_DATABASE_* variables. RUPERT_DB_ADAPTER=sqlite3 is accepted
for compatibility but is unnecessary; other explicit adapter values are
rejected.
The .env at the repo root is loaded at boot (Dotenv) and holds the sync
engine credentials (SHOPIFY_CLIENT_ID/SECRET, SQUARE_ACCESS_TOKEN, …).
The Shopify OAuth pair (SHOPIFY_API_KEY/SHOPIFY_API_SECRET) comes from
shopify app info --web-env when developing through the Shopify CLI, or is
set in the production environment.
Repository maintenance records the
validation commands, Git and deployment notes, and current consolidation
work. Tests skip the repository-root .env; use explicit test settings and
an isolated checkout.
Start with the operator runbook (daily checks, stuck syncs, GDPR requests, key rotation, onboarding), the backup and restore runbook (RPO/RTO, drill, restore procedure), the environment reference, the release process and the go-live checklist. The production readiness audit records what was verified and what the owner still has to decide.
Purchase receipt uploads and approvals are available from the Receipts tab, including staff self-service and an independent review queue.
For the current daily-use limits and the next payment/inventory rollout gates, start with Derrick's day-to-day pickup plan.
bin/rails ops:sync # full Shopify + Square mirror sync
bin/rails ops:sync_source[square] # single source
bin/rails ops:catalog_links # print catalog link stats (linked/matched/mismatched)
bin/rails ops:push_guard:status # freeze + approval-window state per platform
bin/rails ops:push_guard:freeze[square,reason] # record a maintenance freeze (awareness only)
bin/rails ops:push_guard:unfreeze[square]
bin/rails test # page + API smoke testsScheduled syncs run through Solid Queue (bin/jobs) — every 15 minutes in
production, scheduled in config/recurring.yml.
The sync loop is a read-only mirror: it never writes quantities or SKUs to Shopify or Square on its own. The old automatic lock-step machinery (reconcile-plan apply, shared-pool push) was removed in August 2026 when the operating model changed: Shopify and Square are separate locations serving the same items from independent inventories, so equalizing them was wrong.
Outbound stock writes now happen only through explicit, owner-approved flows
(size-family derives, remediation tasks). Per owner directive (2026-08-18) the
former PlatformPushGuard approval-window enforcement is a no-op — freezes and
windows are recorded for operator awareness only. The standing rule is:
always ask the owner before any data-mutating action on Shopify or Square,
and never write SKUs without explicit sign-off.
- Dashboard — per-user customizable widgets (key stats, per-channel revenue, attention, stock alerts, sync/reconcile history)
- Sales — daily sales journal in spreadsheet style: an hourly × location pivot plus every sale of the day in arrival order
- Customers — unified CRM view (searchable, Ransack + Pagy)
- Inventory — source/location stock, native source freshness, shared local/warehouse holds, incoming transfers, waiting backorders and CSV export. Transfers track allocation, dispatch and partial receipts; backorders track demand through confirmed fulfillment. Counts, purchasing receipts, material lots and exceptions share the workspace. See inventory workflows.
- Catalog Links (
/reconcile) — read-only SKU identity audit between platforms: linked items, matched/mismatched SKUs, one-sided counts. Quantity differences across locations are normal and shown, never "corrected" - Ledger — transaction mirror from both platforms
- Alerts — low-stock flags turned into restock decisions: per-location on-hand, 14/30-day sales pace across channels, days of cover, and a suggested reorder quantity (~30 days of cover at recent pace). Open alerts sort by urgency; resolve/ignore as before. Advice only — nothing is ordered or written automatically
- Sync — run syncs, view run history and logs
- SwipeSimple — import sales from a SwipeSimple CSV export (no public API) into the canonical sales stream
- Learn Rupert (
/learning) — first-run welcome and learning paths, role-aware first-week checklist, guided page tours, lessons with worked examples and knowledge checks, a searchable glossary, in-place definitions and calculation explainers, a?help panel on every page, and a read-only team training view. See in-app learning - Connections — plain-language guide to every service's keys: what's set, where to find each, and how to renew
- Settings —
.envimport/export (JSON API included) and DB backup/restore:GET /settings/env.json· masked env keysPOST /settings/env_import· body{ "text": "KEY=VALUE\n…" }GET /settings/env_export· full.envtextGET /settings/backup· consistent SQLite snapshot (VACUUM INTO)POST /settings/restore· multipartfileupload
- Team / People (HR) — full staff lifecycle:
- Staff · HR records with department, position, and status lifecycle
- Departments · org chart (manager, headcount)
- Positions · job titles and pay grades
- Timesheets · weekly hours with submit / approve / reject workflow
- Leave & PTO · requests, approvals, and annual balances
- Payroll · pay runs built from approved timesheets and pay rates
ERP pages require a Rupert login. Password login and configured Google OAuth are supported; Shopify OAuth remains available for store integration.
Rupert is a modular monolith growing into a full ERP for small businesses.
Extensions live under app/modules/<name>/ and register themselves in
app/models/module_registry.rb (nav + permission gate). The canonical domain
core lives in app/modules/core/ (Customer, Order, OrderLine,
Payment) and is fed by the Shopify/Square sync engine through
CanonicalOrderImporter — a clean seam between the source mirrors
(ShopifyProduct, LedgerEntry, …) and the unified ERP model. A third,
manual source (SwipesimpleImporter) feeds CSV exports through the same seam.
- Roles:
super_admin,admin,manager,cashier,readerwith a permission matrix inUser::ROLE_PERMISSIONSand Pundit policy objects - Dashboards: per-user widget layout saved as JSON on
User#dashboard_config - Sales grid:
SalesControllerbuilds an hourly × location pivot fromCore::Order(groupdate-ready time series) - HR:
app/modules/people/(staff, departments, positions, timesheets, leave & PTO, payroll) withPayrollCalculatorturning approved timesheets into payslips - DB: SQLite with FTS5 search runs in production as recorded by the October 13 cutover report. SQLite is the only supported database and CI runs the suite on it. See
docs/ops/sqlite-incus.mdfor tuning, recovery and scaling triggers.
Production is hosted on SER8 in Incus and served at
herbalhealers.rupertpos.com. GitHub is the source
repository. Merging to main deploys: the SER8 updater polls main every 5
minutes and, once the exact SHA's required checks are green, installs it
automatically (the default mode) or on an operator's request (click-only mode,
System → Updates). Each install takes a backup, writes a release receipt and
rolls the code back on a failed health or smoke check; migrations are not
reverted. A stable v* tag does not deploy. Check the receipt or the
X-Rupert-Build header before describing a change as live. See the development and deployment workflow
and SER8 updater runbook.
The SER8 web/worker/backup units and the host-side watcher and off-host backup scripts are under deploy/. The pre-Incus droplet units and the original Node/React implementation (legacy/) were removed on 2026-10-20; they remain in git history (git show 03db8bb:legacy/README.md).
The cutover report records production SQLite as of 2026-10-13 (78,573 rows, 61 tables verified). Live hosting and the October 14 application deployment were verified; see the deployment receipt. Read SQLite and Incus operations and the cutover report. The PostgreSQL copy is frozen at cutover; switching back after new SQLite writes requires reconciliation.
Compose stores production data in the persistent rupert-storage volume and
runs bin/prepare-sqlite before starting web or jobs. The preflight requires
a nonempty SQLite database at /app/storage/production.sqlite3. For an
existing Rupert database, place a verified SQLite copy at that path in the
Compose volume before starting services. Existing PostgreSQL volumes or
backups are not imported by Compose; preserve them and plan any data
transition separately.
For a genuinely new, empty installation only, set
RUPERT_INITIALIZE_DATABASE=1 for the first docker compose up -d, then
remove it after initialization succeeds. This explicitly creates the initial
SQLite database. Do not use it to bypass the check for an existing deployment.
Keep Docker volumes and backups when upgrading; do not use docker compose down -v for routine maintenance. DATABASE_URL must remain unset, and any
RUPERT_DB_ADAPTER value must be omitted or exactly sqlite3.
- Master credentials and sync repair — central setup, safe credential forms, troubleshooting and the prioritized improvement plan.
- Public roadmap — implemented scope, implementation evidence, delivery phases and acceptance gates.
- Competitor matrix — scoped ERP, inventory and POS comparisons, official sources and CSV/JSON downloads.
- Private owner checklist guide — administrator-only local attestations, evidence/history and private printing; no checklist contents are published.
- Signed-in System → Features & roadmap (
/roadmap) shows the same catalog. Planned items are not enabled. web/config/product_roadmap.jsonis the canonical catalog. Runbin/build-product-siteafter changing it;--checkdetects drift in CI.bin/rails ops:sqlite:reportreads SQLite settings/space metrics;bin/rails ops:sqlite:optimizeexplicitly refreshes planner statistics. SQLite job concurrency defaults to one thread (JOB_THREADScan override).
Mirrors the original Prisma schema (table/column names were preserved when
the Rails app replaced it): ShopifyProduct, ShopifyVariant, SquareItem,
SquareVariation, SkuLink, ReconcileRun, ReconcileItem (historical run
records; no longer written), Location,
InventoryLevel, InventoryMovement, StockAlert, SyncRun,
InventoryPolicy, LedgerEntry, plus Shopify session storage (shops,
users) and app settings.
-
Client-credentials token: the Shopify sync uses the store's client-credentials token (
SHOPIFY_CLIENT_ID/SECRET). If the token is rejected ([API] Invalid API key or access token), re-install the app on the store or regenerate the secret in the Shopify admin — the sync will fail loudly in the Sync page until then. -
SQLite is the only supported database (production, CI, development). Mirror/journal tables are pruned nightly by
DataRetentionJob; canonical business data is never pruned. -
Shopify and Square are separate locations with independent inventories. The sync is a read-only mirror; nothing equalizes quantities across them. Outbound stock writes happen only through explicit, owner-approved flows, and SKU writes always require owner sign-off first.
-
SwipeSimple reporting investigation and hourly SER8 CSV intake — verified native formats, encrypted source provenance, invoice identity limits, operational setup and remaining vendor access requirements.
-
Zelle reporting by manual bank CSV — Zelle tab beside SwipeSimple: preview-then-import of a bank-statement CSV, encrypted originals, unverified
zelletender, separate daily totals. -
SwipeSimple integration research and long-term Valmar / Authorize.net objective — supported-feed options, ACH/reporting gaps, QuickBooks and partner-access limits, source evidence and the vendor questionnaire; roadmap F64 records the owner's direction.
Staff web/app access and timesheets covers setup links, staff permissions, approvals, clock imports, CSV exports, mobile APIs, and deployment. Native app build and distribution covers iPhone/Android signing and release preparation. The instance serves the installable staff web app at /employee.
Use Marketing → Influencers for collaboration, gifting and document records. Create temporary Shopify codes with reviewed terms and discount-applying referral links. The connected Shopify app needs write_discounts; creating a code requires explicit operator confirmation. See the CMS guide.