PanelControl: a 65-file business app, in vanilla JavaScript with no framework

PanelControl: a 65-file business app, in vanilla JavaScript with no framework

PanelControl is an internal business app I designed and maintain solo for an official myPOS reseller: sales, onboarding, shipping, staff shifts, HR, and administration, all in one multi-role Progressive Web App. It started as a single HTML file. It's now 65+ source files and about thirty serverless functions — and it's still framework-free, with no build step. Here's what kept it standing as it grew. The problem Before PanelControl, the business ran on spreadsheets, WhatsApp, email, and phone calls. Dozens of orders a day, hundreds of sales leads, contract activations, shipments, shifts, time off, payroll, plus a steady stream of leads from an external CRM — no single view, no reliable history, no way to know in real time what a colleague was doing. I built a single PWA that centralizes all of it, installable on desktop and mobile, used daily on tablets and phones by the team. A hand-written rendering engine No React, no Vue. A global state object holds every piece of application data. Every state change triggers a debounced render() (80ms, to avoid cascading re-renders during closely spaced writes), plus a renderNow() for cases that need an immediate synchronous update. The part I'm proudest of: a user-interaction guard. Before every re-render, the engine checks whether focus is on an input field, and if so, postpones the render by a few seconds so a realtime update from Firebase never wipes out a form someone is filling in — with explicit exceptions for controls that must stay reactive (month selectors, checkboxes). Not every module loads on first launch either. Heavy modules load on-demand when the user navigates to that section, with a retry loop if the module hasn't arrived over the network yet — important since the app also runs on tablets over spotty mobile connections. From on('value') to granular listeners The single most impactful optimization in the project: migrating Firebase Realtime Database listeners from on('value') to granular child_added / child_changed / child_removed. Before: every single write to a growing node caused every connected client to re-download the entire history. After: only the changed delta gets transmitted. Measured result: an estimated 40–60 MB/day saved in Firebase bandwidth, with a direct impact on pay-as-you-go costs. The same pattern applied to the activity log cut the initial page load from 200 to 50 records via once('value'), followed by a child_added listener for new events only — about 75% bandwidth saved on that section alone. Authentication without shared credentials The login system was rebuilt to eliminate shared Firebase credentials on the client. A Netlify Function verifies username and password with PBKDF2-SHA256 hashing, applies server-side rate limiting against brute force, and returns a server-minted Firebase Custom Token. The client exchanges it for an authenticated session, with permissions mapped via custom claims and checked in the database's security rules. For granular, per-operator permissions on top of the base role, I settled on one non-negotiable pattern for every access check: access = legacy_hardcoded_list.includes(operator) OR hasPermission(operator, feature) Enter fullscreen mode Exit fullscreen mode Never the dynamic check alone. This avoids breaking access for operators not yet explicitly migrated to the new permission system — a rule I've applied across every permission change in the project since: new rules OR with the old ones, they never replace them outright. A few modules that raised real problems Internal chat. A floating bubble UI hit the classic position:fixed bug: it stops working correctly once an ancestor has a CSS transform applied, which is common when nesting modals. Fixed by making the chat a direct sibling of the main container instead of a descendant. Mail. Polls the Gmail API instead of using push, with a denormalized schema split across two nodes to balance list speed against detail speed. Listeners here are always granular — the nodes hold potentially heavy email bodies, and on('value') would redownload the whole mailbox on any tiny change. Call history. A two-speed architecture: the current month stays on the Realtime Database with a capped listener, past months move to Firestore with block pagination. Searches always route to Firestore to avoid saturating the realtime database. CRM reconciliation. A scheduled function checks each lead against the external CRM one call at a time (not a bulk dump) to stay within rate limits, rechecking recent leads every 24 hours and permanently skipping older ones once they've settled. Fail-soft serverless functions Backend functions are pure ESM, deliberately dependency-free. A few shared patterns across every webhook integration: Deduplication via a business key (order number), not a CRM-generated ID Every webhook logs to both RTDB and Firestore, for realtime visibility and historical queries Critical alerts go out via Telegram with per-category throttling Webhooks always respond HTTP 200, even on an internally handled error — so the external CRM doesn't retry the same request forever One operational constraint worth knowing: Netlify environment variables cap out at 4KB per function. Large credentials like private keys end up hardcoded in the function file instead, with a note for manual rotation, rather than blowing the limit and breaking the deploy. Takeaway "No framework" doesn't mean "no discipline." It means writing by hand the rules a framework would otherwise give you for free — a sensible debounce, a user-interaction guard, a non-destructive permission pattern — and sticking to them as the codebase grows from one file to sixty-five. I wrote a longer, more detailed version of this case study, plus related deep-dives on the Custom Token migration and a cross-app HMAC token, on my site: roversia.it/blog-21.

Original Source

Read the full article at Dev →

KhanList aggregates and links to publicly available news content. We do not host full articles from third-party sources. Always verify important information with original sources.