/storeName), full product & order management, customer-account portals,
a drag-and-drop section editor, and optional custom domain provisioning with automated SSL via Certbot.
The platform is monetized through an annual subscription billed via Stripe, with a 90-day free trial on signup.
Secondary revenue exists via a CSS theme & layout marketplace served by third-party designers.
An additional payment rail — MooChedda — enables merchants to accept digital-asset payments.
The system comprises 24 database tables, 32 API route namespaces, five distinct user roles,
and is deployed on a single VPS behind an Apache reverse proxy with TLS termination.
A built-in operational dashboard (/SystemCheck.html) now continuously validates database, filesystem,
Stripe configuration, and MooChedda TLS verification status via /api/system/check.
js.stripe.com for checkoutv1.moochedda.com:3002src/server.jssrc/routes/public/SystemCheck.html + /api/system/checknode --watch for devlaunchcartshopifyinitSchema()/var/www/html/torvali/websites/etc/apache2/sites-availableexecFile() + Apache vhosts
merchants is the central root entity — all merchant-owned tables carry a merchant_id FK with ON DELETE CASCADE to ensure clean multi-tenant data isolation.
customers are scoped per merchant with a composite unique key (merchant_id, email).
orders retain items via ON DELETE CASCADE but preserve product references on deletion with ON DELETE SET NULL.
The designers table is a peer entity to merchants — designers upload assets but do not own storefronts.
admins is entirely independent with no FK dependencies.
| Namespace | Module | Access | Role(s) | Key Endpoints |
|---|---|---|---|---|
| /api/auth | routes/auth.js | Public | — | POST /signup · /login · /logout · /totp/setup · /forgot-password · /reset-password |
| /api/billing | routes/billing.js | Protected | Merchant | GET /status · POST /checkout · /portal · /redeem · /webhook (raw, Stripe sig) |
| /api/me | routes/me.js | Protected | Merchant | GET / · PATCH / · PATCH /password · PATCH /stripe-keys · PATCH /moochedda |
| /api/me/email | routes/email.js | Protected | Merchant | GET,PATCH /config · GET,PUT /templates/:type · POST /test |
| /api/products | routes/products.js | Paywall | Merchant | GET,POST / · GET,PATCH,DELETE /:id · POST /:id/images · PATCH /:id/images/:imgId |
| /api/orders | routes/orders.js | Paywall | Merchant | GET,POST / · GET,PATCH /:id · PATCH /:id/status · POST /:id/invoice |
| /api/categories | routes/categories.js | Paywall | Merchant | GET,POST / · PATCH,DELETE /:id |
| /api/shipping | routes/shipping.js | Paywall | Merchant | GET,PUT /zones · GET,PUT /rates · GET /config |
| /api/pages | routes/pages.js | Paywall | Merchant | GET,POST / · GET,PATCH,DELETE /:id |
| /api/sections | routes/sections.js | Paywall | Merchant | GET,POST / · PATCH /:id · DELETE /:id · POST /reorder |
| /api/domain | routes/domain.js | Paywall | Merchant | GET,POST,DELETE / |
| /api/customers | routes/customers.js | Paywall | Merchant | GET / · GET /:id · DELETE /:id |
| /api/employees | routes/employees.js | Paywall + Owner | Merchant Owner | GET,POST / · PATCH,DELETE /:id · PATCH /:id/status · POST /:id/reinvite |
| /api/employee | routes/employees.js | Protected | Employee | GET /me · PATCH /me/password · GET,POST /me/totp |
| /api/employee/invite | routes/employees.js | Public | — | GET /:token · POST /:token/activate |
| /api/themes | routes/themes.js | Mixed | Merchant (write) | GET / · POST /apply · GET /marketplace · GET /preview/:slug |
| /api/layouts | routes/layouts.js | Mixed | Merchant (apply) | GET / · POST /apply · GET /preview/:slug |
| /api/designer-auth | routes/designer-auth.js | Public | — | POST /signup · /login · GET /me · PATCH /me/password · GET,POST /me/totp |
| /api/designer | routes/designer.js | Protected | Designer | GET,PATCH /profile · GET,POST /themes · PATCH,DELETE /themes/:id · GET,POST /layouts |
| /api/admin-auth | routes/admin-auth.js | Mixed | — | GET /check · POST /bootstrap · POST /login · GET /me · PATCH /me/password · GET,POST /me/totp |
| /api/admin | routes/admin.js | Protected | Admin | GET /merchants · PATCH /merchants/:id/status · /custom-js · GET /support · GET /layouts · PATCH /layouts/:id/status · GET /themes · PATCH /themes/:id/status · GET /codes · POST /codes |
| /api/public/:store | routes/public.js | Public | — | GET /info · /products · /products/:id · /categories · /featured · /search · POST /orders · /cart/shipping · /reviews |
| /api/public/:store/customer | routes/customer-auth.js | Public | — | POST /signup · /login · GET /me · PATCH /me · GET /orders · /totp: setup, verify, disable |
| /api/moochedda | routes/moochedda.js | Protected | Merchant | POST /wallet/create · GET /wallet · GET /transactions · GET /token-price · POST /checkout |
| /api/support | routes/support.js | Public | — | POST / (submit ticket) |
| /api/system | routes/system.js | Public | — | GET /check (API, DB, FS, Stripe config, MooChedda TLS) |
| Library | jsonwebtoken ^9.0.2 |
| Algorithm | HS256 (default) |
| Secret source | JWT_SECRET env var |
| Merchant expiry | 7 days (JWT_EXPIRES_IN) |
| Admin expiry | 12 hours |
| Designer expiry | 30 days |
| Customer expiry | 30 days |
| Transport | Authorization: Bearer header |
| Token type claim | role (admin/designer) or type (employee/customer) |
| Library | bcryptjs ^2.4.3 |
| Cost factor | 12 (all user types) |
| Minimum length | 8 characters (validated server-side) |
| Timing attacks | bcrypt.compare (constant-time) |
| Password reset | crypto.randomBytes(32) token, stored hashed (customers) |
| Rate limiting | ✓ 10 req / 15 min on auth endpoints |
role:'admin' claim. 12h token. Admin-only routes: merchant management, theme approval, support tickets, redemption codes.merchant_id scoping. Subscription / trial gated by paywall middleware.type:'employee' token. Carries merchant_id + JSON permissions array. requireMerchantOnly blocks employees from owner-only actions.role:'designer' claim. Access only to own themes/layouts. Cannot access merchant or admin endpoints. Blocked by requireAuth role check.type:'customer' + store claim. Isolated to specific merchant store. Used for order history, review authorship, and account management.server.js refuses to start if JWT_SECRET is missing or shorter than 32 characters — hardcoded fallback secrets removed.'unsafe-inline' removed from script-src. TLS via Apache. Auth endpoints have strict rate limiting (10/15m). Remaining gaps: style-src retains 'unsafe-inline' for inline style attributes; no HSTS/CORS middleware in Express layer.src/crypto.js with key stored outside the DB. Remaining gaps: TOTP secrets stored as plaintext base32 strings; no full DB-level encryption.res.json interceptor in server.js strips internal error details from all 5xx responses platform-wide, replacing them with a generic message and a short correlation ID logged server-side. 4xx validation errors pass through unchanged. The existing global next(err) handler was also updated to not leak err.message.child_process.exec() calls replaced with execFile(). Commands (a2ensite, a2dissite, systemctl, certbot) now receive arguments as discrete arrays — no shell interpolation possible even if domain regex were bypassed.rejectUnauthorized: false setting was removed. Outbound MooChedda calls now validate certificates using the system CA bundle by default, with optional CA pinning via MOOCHEDDA_CA_CERT for private/self-signed PKI environments./api/system/check and /SystemCheck.html include dedicated MooChedda TLS mode and reachability checks.express-rate-limit policy is now enforced for authentication-sensitive routes across merchant, admin, designer, and customer flows. The configured threshold is 10 requests per 15 minutes per IP with standard rate-limit headers enabled./api/system/check now includes auth_rate_limit to verify limiter configuration coverage.|| 'changeme' and || 'dev_secret_change_me' fallback JWT secrets have been removed from source code. src/server.js now includes a startup guard that calls process.exit(1) if JWT_SECRET is missing or shorter than 32 characters, preventing any misconfigured deployment from starting.FATAL: JWT_SECRET env var is missing or too short. Refusing to start. if the env var is absent or weak.merchant_email_config.smtp_pass) and Stripe secret keys (merchants.stripe_secret_key) are now encrypted with AES-256-GCM before every INSERT/UPDATE and decrypted transparently after SELECT. The ENCRYPTION_KEY is stored in the environment outside the database. A startup guard in server.js refuses to start if the key is missing or malformed. Both DB columns were widened from VARCHAR(255) to VARCHAR(512) to accommodate the encrypted format (iv:authTag:ciphertext hex). A one-time migration script (scripts/encrypt-existing-secrets.js) handles existing plaintext rows — it widens the columns, skips already-encrypted values, and re-encrypts plaintext ones idempotently.src/crypto.js enforces a 32-byte key; auth tag verification catches key mismatch or data corruption at read time. Column-widening migrations are also included in src/database.js so they apply automatically on server start.res.json response interceptor middleware was added to src/server.js that fires before every route handler. For any response with an HTTP 5xx status, it replaces the JSON body with { error: 'Internal server error', id: 'ERR-XXXXXXXX' } and logs the original error and detail fields server-side with the correlation ID. All 100+ existing res.status(500).json({ error: err.message }) catch blocks across 18+ route files are protected without any per-file changes. 4xx validation error messages are intentionally passed through unchanged.{ "error": "Internal server error", "id": "ERR-XXXXXXXX" }. The real detail appears in server logs as [ERR-XXXXXXXX] METHOD /path originalError.child_process.exec() calls in domain.js were replaced with execFile(). The run(cmd, args) helper now accepts a command string and a discrete argument array — no string is ever passed through a shell. The five affected invocations (a2ensite, a2dissite ×2, systemctl reload apache2 ×2, certbot certonly, certbot delete) all pass domain values as array elements, not interpolated strings.require('child_process').execFile is used exclusively; exec is no longer imported in domain.js.crypto.randomBytes(16).toString('base64'), attached to req.cspNonce, and injected into all 23 inline <script> blocks across 14 HTML files via a single regex — without modifying any HTML file. The CSP script-src directive was changed from 'unsafe-inline' to 'nonce-${nonce}'. sendPage() and sendHTML() were rewritten to read from an in-memory HTML cache (_htmlCache) and send via res.type('html').send(). The style-src directive intentionally retains 'unsafe-inline' because inline style= attributes are used throughout the HTML — nonces apply only to <script> blocks.<script> tags. External scripts (with src=) are unaffected.cors package and restrict allowed origins to the platform domain.Strict-Transport-Security, X-Content-Type-Options, X-Frame-Options, or Referrer-Policy headers are set. These protect against downgrade attacks, MIME sniffing, clickjacking, and information leakage.helmet package for comprehensive security header coverage with a single app.use(helmet()).speakeasy has had no releases since 2018. While the TOTP algorithm is stable, the library receives no security patches. Future vulnerabilities in its dependencies would go unaddressed.@otplib/preset-default (actively maintained) or otpauth.custom_js_enabled = 1 by default. While admins can disable this per-merchant, customers of all merchants are exposed to merchant-controlled JS execution without explicit opt-in review.custom_js_enabled to 0 (disabled) and require admin approval before enabling custom JS per merchant.crypto.randomBytes(32)) are stored as plaintext in the database. If the database is breached, all pending invite tokens are exposed and can be used to activate employee accounts.JWT_SECRET Value Across All Deployment Environments P2 HighJWT_SECRET ≥ 64 characters stored as a secret — not a shared or guessable string.MOOCHEDDA_CA_CERT where required by environment policy.helmet package to automatically add HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy and a tightened CSP — replacing the manual header code in server.js.exec() has been removed from domain provisioning. As a preventive measure, add a lint rule or pre-commit hook to flag any future introduction of child_process.exec() in the codebase.ENCRYPTION_KEY and Back Up Encrypted Records P3 MediumENCRYPTION_KEY is backed up in a secret manager separate from the database backup.ERR-XXXXXXXX IDs reported by clients with the full server-side log entries for debugging without exposing internals.@otplib/preset-default which is actively maintained and RFC 6238 compliant, with a compatible API.cors package to only allow same-origin requests and the platform domain. This prevents cross-origin request forgery from third-party sites.| Package | Version Spec | Purpose | Risk Level | Notes |
|---|---|---|---|---|
| bcryptjs | ^2.4.3 | Password hashing for all user types | LOW | Pure-JS bcrypt. Stable. Cost factor 12 used throughout. Actively maintained. |
| dotenv | ^16.4.5 | Environment variable loading from .env | LOW | Dev convenience. Production deployments should use OS env vars directly. Latest stable branch. |
| express | ^4.19.2 | HTTP framework — routing, middleware, static serving | LOW | Express 4 LTS. Express 5 is available but breaking. No known CVEs in 4.19.x. |
| jsonwebtoken | ^9.0.2 | JWT signing and verification for all auth roles | LOW | Version 9 patched CVE-2022-23529. Using HS256 — RSA asymmetric (RS256) preferred for distributed systems. |
| multer | ^1.4.5-lts.1 | Multipart file upload handling (images, CSS, JS) | LOW | LTS version. File type filtering implemented. 8 MB limit for images, 2 MB for code files. MIME type validated. |
| mysql2 | ^3.9.8 | MariaDB/MySQL driver with prepared statements | LOW | Uses Promise API + execute() for parameterized queries preventing SQL injection. Connection pooling enabled. |
| nodemailer | ^8.0.4 | Transactional email sending (orders, invites, password resets) | LOW | Version 8 (major rewrite — ES modules). Per-merchant SMTP configuration. No platform SMTP relay configured. |
| qrcode | ^1.5.4 | TOTP QR code generation for 2FA setup flows | LOW | Generates SVG/data-URI QR codes for authenticator app enrollment. No network calls. |
| speakeasy | ^2.0.0 | TOTP/HOTP generation and verification (2FA) | MEDIUM | ⚠ Last release: 2018. Unmaintained. No security patches since then. Recommend replacing with @otplib/preset-default. |
| stripe | ^20.4.1 | Platform Stripe billing — subscriptions, checkout sessions, webhooks | LOW | v20 SDK. Webhook signature verified via stripe.webhooks.constructEvent() with raw body. Current checkout flow uses inline price_data; STRIPE_PRICE_ID is optional. |