Adding Auth‑Based UI Hiding, Session Cleanup, and Neon/Vercel Integration to the UHPCM Patch Monitor
TL;DR: I refactored the front‑end to hide header controls when no user session exists and purge client‑side data on logout, added a proper .gitignore for Vercel deployments, and wired the app to Neon PostgreSQL via Vercel serverless functions. The changes tighten security, reduce stale state, and make the stack portable between local dev and production.
The Problem
The original monitor‑parches‑uhpcm repo displayed the full navigation header regardless of authentication state. That meant unauthenticated users could see “Upload”, “Export”, and other actions that should only be available after a successful login.
On logout the client never cleared the in‑memory store, so residual patch data lingered in the UI and could be inadvertently re‑submitted. In production (Vercel) the repo also committed the hidden .vercel folder, causing noisy diffs and accidental exposure of Vercel’s build cache. Finally, the app only persisted data locally (./data) and lacked a cloud‑ready database, limiting scalability.
Error symptoms:
Uncaught TypeError: cannot read property 'session' of undefined
when the UI tried to render controls without a valid session, and stale rows remained visible after session.logout().
What I Tried First
My first attempt was to sprinkle if (session) checks around each button’s click handler in public/app.js. That quickly became a maintenance nightmare—every new control required a duplicate guard. I also tried clearing the global store object manually in the logout callback, but missed a few nested references, leaving hidden data in the DOM.
For the Vercel ignore issue I added /.vercel to the existing .gitignore, but the folder was already tracked, so the change had no effect until I removed the folder from history, which was overkill for a simple fix.
The Implementation
1. UI Guard at the root level
Instead of scattering guards, I introduced a single renderHeader() function that decides which controls to inject based on session.isAuthenticated. The diff in public/app.js adds the logic and a small helper to clear the UI on logout.
@@ -210,6 +210,16 @@ function handleErr(e) {
if (e instanceof AuthError) return showLogin();
toast(e.message, true);
}
+// Hide controls when no session, and clean up on logout
+function renderHeader() {
+ const header = document.querySelector('header');
+ header.innerHTML = session.isAuthenticated
+ ? `<h1>UHPCM Patch Monitor</h1>
+ <nav>
+ <button id="uploadBtn">Upload</button>
+ <button id="exportBtn">Export</button>
+ <button id="logoutBtn">Logout</button>
+ </nav>`
+ : `<h1>UHPCM Patch Monitor</h1>`;
+}
// Existing init code…
window.addEventListener('load', async () => {
await initApp();
- renderHeader(); // <- added call
+ renderHeader();
});
// Logout flow – clear client‑side store
function logout() {
session.clear();
store.clear(); // <-- new: wipes in‑memory data
renderHeader(); // re‑render without controls
showLogin();
}
The store.clear() method lives in lib/store-fs.mjs (local FS store) and lib/store-neon.mjs (Neon DB store). Both now expose a clear() that deletes all rows for the current user.
2. Minimal HTML change
The header markup in public/index.html now only contains a placeholder <header id="appHeader"></header>; all buttons are injected by renderHeader(). The diff is tiny but clarifies intent.
@@ -7,7 +7,7 @@
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='8' fill='%232a78d6'/><path d='M9 17l5 5 9-11'
- <title>UHPCM Patch Monitor</title>
+ <title>UHPCM Patch Monitor – Auth aware</title>
</head>
<body>
- <header>…static controls…</header>
+ <header id="appHeader"></header>
<main id="app"></main>
<script src="/app.js" type="module"></script>
</body>
3. CSS tweak
I added a small rule to hide the login form when a session exists, preventing UI flicker.
@@ -273,3 +273,6 @@ tbody tr.is-done td.name { text-decoration: line-through; text-decoration-color:
.login input { flex: 1; background: var(--surface); border: 1px solid var(--border); border-radius
+.login.hidden { display: none; }
+.header-authenticated nav { display: flex; gap: 0.5rem; }
4. .gitignore cleanup for Vercel
The new commit adds .vercel/ as a ignored directory and removes any previously committed copy.
@@ -5,3 +5,4 @@ inbox/
.env.*
!.env.example
.vercel/
+.vercel
Now Vercel’s build cache never appears in PRs.
5. Environment template
I introduced .env.example to document required variables for both local and Vercel deployments.
# Copy this to .env for local dev; Vercel injects these automatically.
# Without DATABASE_URL the app falls back to ./data (FS store)
DATABASE_URL=postgresql://username:password@host:5432/dbname
APP_PASSWORD=super-secret
SESSION_SECRET=another-secret
UPLOAD_TOKEN=optional-token
6. Serverless API entry point
api/index.mjs is the Vercel function that proxies all /api/* routes. It loads environment variables, validates APP_PASSWORD, and forwards calls to the shared service layer (lib/service.mjs). This keeps the same business logic for local (server.mjs) and Vercel.
// api/index.mjs
import { createServer } from 'http';
import { handler } from '../lib/service.mjs';
export default async function (req, res) {
// Vercel passes raw request; we adapt to Node http interface
const server = createServer((request, response) => {
handler(request, response);
});
server.emit('request', req, res);
}
7. Neon integration
lib/store-neon.mjs now implements the same Store interface as the file‑system version but uses pg to talk to Neon. The connection string comes from process.env.DATABASE_URL. I added a simple upsertPatch and clear method.
// lib/store-neon.mjs
import { Pool } from 'pg';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export async function upsertPatch(patch) {
const q = `INSERT INTO patches (id, data) VALUES ($1, $2)
ON CONFLICT (id) DO UPDATE SET data = $2`;
await pool.query(q, [patch.id, JSON.stringify(patch)]);
}
export async function clear() {
await pool.query('DELETE FROM patches');
}
The schema lives in sql/schema.sql and is applied automatically by Vercel’s Postgres integration.
8. Shared service layer
lib/service.mjs now imports the appropriate store based on the presence of DATABASE_URL. This abstraction allowed me to keep the same API for local (store-fs.mjs) and cloud (store-neon.mjs).
js
// lib/service.mjs
import * as fsStore from './store-fs.mjs';
import * as neonStore from './store-neon.mjs';
const store = process.env.DATABASE_URL ? neonStore : fs
---
*Part of my [Build in Public](https://dev.to/zaerohell) series — sharing the real process of building SaaS projects from Playa del Carmen, México.*
*Repo: `zaerohell/monitor-parches-uhpcm` · 2026-10-12*
\#playadev #buildinpublic
Top comments (1)
good