Build an Attendance App with Express
Build an Express check-in app that saves fictional attendance data in Supabase.
Introduction
30 Second Summary
Workplace check-ins are only useful when yesterday's record is still there tomorrow. A history that vanishes after a restart cannot support a dependable attendance process.
In this project, you will build a fictional employee check-in page backed by an Express API. You will store each event in Supabase PostgreSQL so it survives a server restart.
What You'll Build
Your finished page shows a timestamped fictional check-in that remains in the attendance history after a full server restart.
By the end of this project, you'll have:
- Record check-ins for fictional employees from a responsive browser page.
- See immediate feedback when a check-in succeeds or the submitted name fails validation.
- Restart the server without losing the timestamped history stored in PostgreSQL.
- Secret Mission: Block the same fictional employee from checking in twice within 60 seconds.
Are there any prerequisites?
You'll need a Windows 10 or 11 computer with internet access plus an email address for a free Supabase project. The guide includes every software setup step.
Before We Start
This checkpoint locks the project to a fictional attendance check-in flow before any hands-on work begins. Authentication, real employee data, roles, geofencing, reporting, and deployment stay outside this project.
Set Up the Clean Foundation
The unfinished attendance repository mixed a legacy MySQL adapter with optional features. That clutter made the core request path difficult to follow.
A clean workspace gives the fictional check-in flow one visible foundation. You will verify Node.js plus npm before installing the pinned project dependencies.
You will also create a Supabase project for the database work ahead. A temporary Express health route proves your local server can answer requests first.
In this step, get ready to:
- Verify Node.js 26.13.0 LTS plus npm 11.20.0.
- Create the clean workspace with its pinned dependencies.
- Create the Supabase project and run the Express health route.
Verify the Node.js runtime
Node.js runs the local server from server.js. The matching npm release installs the exact dependency versions declared by the project.
- Press the Windows key to open Windows search.
- Type Visual Studio Code into Windows search.
- Press Enter to open Visual Studio Code.
You will see the Visual Studio Code start screen.
- Select Terminal from the top menu.
- Select New Terminal to open the integrated terminal.
- Check both installed versions by running these commands:
node --version
npm --version
What do these commands show?
- The first command prints the installed Node.js version.
- The second command prints the installed npm version.
- Each command reads your installed runtime without changing any files.
✔️ I see the required versions
You should see v26.13.0 for Node.js. You should also see 11.20.0 for npm.
ⓧ I see an older Node.js version
Your existing installation needs an in-place upgrade to Node.js 26.13.0 LTS. The official Windows installer includes npm 11.20.0.
Windows may request administrator permission when the installer starts. This allows the installer to replace the older runtime.
- Visit the official Node.js download page.
- Download the installer that matches your Windows processor.
The x64 installer is named node-v26.13.0-x64.msi. The ARM64 installer is named node-v26.13.0-arm64.msi.
- Open the downloaded installer.
- Complete the installation with the default options.
- Close every Visual Studio Code window after the installation finishes.
- Reopen Visual Studio Code from Windows search.
- Select Terminal from the top menu.
- Select New Terminal.
- Repeat both version checks by running:
node --version
npm --version
Why repeat the checks?
A newly opened terminal reads the upgraded runtime from Windows. The output confirms that Visual Studio Code can access both installed commands.
You should now see v26.13.0 followed by 11.20.0.
ⓧ Command not found
Node.js is not installed yet. The official Node.js 26.13.0 LTS installer also installs npm 11.20.0.
Windows may request administrator permission when the installer starts. This permission allows the runtime to become available across your computer.
- Visit the official Node.js download page.
- Download the installer that matches your Windows processor.
The x64 installer is named node-v26.13.0-x64.msi. The ARM64 installer is named node-v26.13.0-arm64.msi.
- Open the downloaded installer.
- Complete the installation with the default options.
- Close every Visual Studio Code window after the installation finishes.
- Reopen Visual Studio Code from Windows search.
- Select Terminal from the top menu.
- Select New Terminal.
- Verify the installation by running:
node --version
npm --version
What confirms the installation?
The first output identifies the Node.js runtime. The second output identifies the bundled npm release.
You should see v26.13.0 followed by 11.20.0.
Your runtime layer is ready. The project can now install its pinned packages without an older Node.js release getting in the way.
Still unable to run the version checks?
Close every Visual Studio Code window after installing Node.js. Reopening the app gives its terminal access to the updated Windows environment.
Confirm that you used the installer matching your x64 or ARM64 processor.
Help me troubleshoot my Node.js installation on Windows.
Create the workspace and install dependencies
The knooviq-attendance-core workspace keeps this rebuild separate from the unfinished repository. Its configuration pins Express 5.3.0 plus node-postgres 8.23.1.
- Select File from the Visual Studio Code menu.
- Select Open Folder.
Windows opens a folder picker where you can choose the workspace location.
- Select your Desktop in the folder picker.
- Use the new-folder control to create knooviq-attendance-core.
You will see the new empty folder listed on your Desktop.
- Select the knooviq-attendance-core folder.
- Click Select Folder to use it as your workspace.
- Confirm workspace trust if Visual Studio Code asks about the folder you created.
The Explorer sidebar now shows knooviq-attendance-core as an empty workspace.
- Select the new-file control in the Explorer sidebar.
- Enter package.json as the file name.
- Define the project scripts plus dependencies by adding this code:
{
"name": "knooviq-attendance-core",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "node --env-file=.env server.js"
},
"dependencies": {
"express": "5.3.0",
"pg": "8.23.1"
}
}
What does this configuration do?
- The private setting protects this local project from accidental publication as an npm package.
- The start script loads values from .env before running server.js.
- The dependency entries pin Express to 5.3.0 plus pg to 8.23.1.
- Save package.json.
The saved package.json appears in the Explorer sidebar.
- Select the new-file control in the Explorer sidebar.
- Enter .gitignore as the file name.
- Protect generated dependencies plus local environment values by adding:
node_modules/
.env
What stays outside Git?
The node_modules folder is generated by npm. The local .env file later contains the database connection credential.
- Save .gitignore.
The Explorer sidebar now lists package.json plus .gitignore.
- Select the new-file control in the Explorer sidebar.
- Enter .env as the file name.
- Keep the temporary environment file secret-free by adding this comment:
# Add DATABASE_URL after creating the Supabase database schema.
Why create the environment file now?
The start script expects .env to exist. This comment keeps the file valid until a later step adds the database connection string.
- Save .env.
The temporary .env file now exists without holding a credential.
- Select the new-file control in the Explorer sidebar.
- Enter .env.example as the file name.
- Create the secret-free connection template by adding:
DATABASE_URL=your-supabase-session-pooler-connection-string-with-sslmode-require
Why keep an example file?
The tracked template records the required DATABASE_URL key without exposing a real credential. The local .env remains ignored.
- Save .env.example.
- Return to the integrated terminal from earlier.
- Install the pinned project dependencies by running:
npm install
What does this command create?
npm reads the dependency versions from package.json. It installs those packages inside node_modules.
The command also creates package-lock.json for repeatable dependency resolution.
The first installation can take a little time while npm downloads the packages. The completed output includes a package summary.
You will see node_modules plus package-lock.json in the Explorer sidebar. This confirms the pinned dependencies are installed.
Did the dependency installation fail?
Check that the terminal is inside the knooviq-attendance-core workspace. The same folder must contain package.json.
A JSON syntax error usually points to a missing comma or quote in package.json.
Help me fix my npm installation.
Launch the Supabase and Express foundation
The Supabase project provides the PostgreSQL database used in later steps. This step only prepares access while the health route still reports memory storage.
Project creation asks for a database password. This credential stays private throughout the project.
- Press the Windows key to open Windows search.
- Type Microsoft Edge into Windows search.
- Press Enter to open Microsoft Edge.
- Visit the Supabase Dashboard.
- Create a Supabase account or sign in to your existing account.
The dashboard gives you access to hosted database projects.
- Start a new Supabase project from the dashboard.
- Select the Free Plan.
- Enter knooviq-attendance-core as the project name.
- Enter a strong database password.
- Store the database password in your password manager.
- Create the project with the entered details.
The database can take a few minutes to launch. A short wait here is expected.
- Wait until the project dashboard shows knooviq-attendance-core as ready.
- Keep the project free of real employee records.
Your Supabase project is ready for the persistence work in a later step. No attendance data has been added.
- Switch back to Visual Studio Code.
- Select the new-file control in the Explorer sidebar.
- Enter server.js as the file name.
- Create the temporary memory-backed health server by adding this code:
const express = require('express');
const app = express();
const port = 3000;
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', storage: 'memory' });
});
app.listen(port, () => {
console.log(`Knooviq Attendance Core is running at http://localhost:${port}`);
});
What does this server do?
- The require('express') call loads the installed Express package.
- The app listens on port 3000.
- The /api/health route returns a small JSON response that identifies memory as the current storage layer.
- The startup message gives you the local server address.
- Save server.js.
Before you start the server, what do you expect the terminal to show when Express begins listening on port 3000?
- Start the temporary Express server by running:
npm start
What does this command do?
npm runs the start script from package.json. Node.js loads the temporary .env file before starting server.js.
The terminal remains attached to the running server so it can keep accepting requests.
You will see Knooviq Attendance Core is running at http://localhost:3000 in the terminal.
Before you check the endpoint, which status value plus storage value do you expect the server to return?
- Switch back to Microsoft Edge.
- Enter http://localhost:3000/api/health in the address bar.
- Press Enter to request the health route.
You will see {"status":"ok","storage":"memory"} in Microsoft Edge. This proves the browser can reach your local Express server.
That is the clean foundation working. Your pinned runtime now serves a visible health response from the new workspace.
Health route not loading?
Check that the Visual Studio Code terminal still shows the server startup message. Restart the server if the process has stopped.
Confirm that the browser address ends with /api/health on port 3000.
Help me reach my Express health route.
✔️ Awesome, I've got everything!
- Confirm every project file is saved in Visual Studio Code.
- Keep the Express server running for the checkpoint below.
ⓧ I'd like to double check the full code
node_modules/
.env
# Add DATABASE_URL after creating the Supabase database schema.
DATABASE_URL=your-supabase-session-pooler-connection-string-with-sslmode-require
{
"name": "knooviq-attendance-core",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "node --env-file=.env server.js"
},
"dependencies": {
"express": "5.3.0",
"pg": "8.23.1"
}
}
const express = require('express');
const app = express();
const port = 3000;
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', storage: 'memory' });
});
app.listen(port, () => {
console.log(`Knooviq Attendance Core is running at http://localhost:${port}`);
});
Your runtime, workspace, dependencies, cloud project, and local server are ready. Next, you will turn this health check into the first visible fictional employee check-in flow.
Build the First Working Check-In
Your Express health route proves the local server can answer a browser request. The next goal is a visible check-in flow that sends fictional attendance data through the same connection.
This step keeps events in volatile memory so you can isolate the browser-to-API flow before adding PostgreSQL. The temporary storage choice makes every request easy to follow.
In this step, get ready to:
- Build a responsive browser surface for fictional check-ins.
- Add in-memory attendance routes with server-side validation.
- Connect the page to the API with same-origin requests.
Build the browser surface
The browser needs a clear form for new check-ins. It also needs a history area where saved events can appear.
How the browser surface fits together
The HTML defines the form and history list. The CSS makes the page readable across desktop and narrow screens.
A separate JavaScript file handles requests and updates the page. This separation keeps each layer focused.
- Use the new-folder control in the Visual Studio Code Explorer sidebar to create public inside knooviq-attendance-core.
- Confirm that public appears in the Explorer sidebar.
- Select the public folder in the Explorer sidebar.
- Use the new-file control to create index.html inside public.
- Create the page shell by adding this code to public/index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Knooviq Attendance Core</title>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<main class="shell">
<section class="hero" aria-labelledby="page-title">
<p class="eyebrow">Knooviq Systems</p>
<h1 id="page-title">Attendance Core</h1>
<p class="subtitle">Use fictional names while authentication is being built.</p>
</section>
</main>
<script src="/app.js" defer></script>
</body>
</html>
What does this page shell do?
- The viewport setting lets the layout adapt to the width of the device.
- The hero identifies the attendance tool and keeps the fictional-data rule visible.
- The stylesheet link loads the visual rules you add later in this substep.
- The deferred script loads the browser logic after the document is available.
- Save public/index.html.
- Confirm that index.html appears under public in the Explorer sidebar.
Is index.html outside public?
Drag index.html into the public folder if both appear at the same level. The final path must be public/index.html.
Help me place the browser file correctly.
- Select server.js in the Explorer sidebar.
- Replace its contents with this static-file server:
const express = require('express');
const app = express();
const port = Number(process.env.PORT) || 3000;
app.use(express.static('public'));
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', storage: 'memory' });
});
app.listen(port, () => {
console.log(`Knooviq Attendance Core is running at http://localhost:${port}`);
});
What does this server change do?
The static middleware serves files from public through the same Express origin as the API. A request to the site root now receives public/index.html.
The port can use a future environment value. It falls back to 3000 for this local project.
- Save server.js.
- Switch back to the Visual Studio Code terminal from the previous step.
- Press Ctrl+C to stop the earlier server process.
- Start the updated Express server by running:
npm start
What does this command do?
The npm start command runs the existing start script from package.json. That script starts server.js with values from .env available to Node.js.
- Return to Microsoft Edge from the previous step.
- Open http://localhost:3000.
You should see the Knooviq Systems label above the Attendance Core heading. The fictional-data reminder confirms that Express is serving the page.
Do you still see the health response?
Check that the address ends at http://localhost:3000 with no /api/health suffix. Confirm the terminal is still running the updated server.
Help me serve public/index.html from Express.
- Switch back to public/index.html.
- Find the closing </main> line.
- Insert the check-in form immediately above that line:
<section class="panel" aria-labelledby="check-in-title">
<h2 id="check-in-title">Record a check-in</h2>
<form id="check-in-form">
<label for="employee-name">Fictional employee name</label>
<div class="form-row">
<input id="employee-name" name="employeeName" type="text" maxlength="80" autocomplete="off" required />
<button id="submit-button" type="submit">Check In</button>
</div>
</form>
<p id="feedback" class="feedback" role="status" aria-live="polite"></p>
</section>
How does the form support check-ins?
- The form groups the fictional employee field with its submit action.
- The input accepts up to 80 characters before the browser blocks additional typing.
- The feedback region gives request results a stable place on the page.
- The status attributes allow assistive technology to announce feedback changes.
- Save public/index.html.
- Return to Microsoft Edge.
- Refresh http://localhost:3000.
You should see a Fictional employee name field with a Check In button. The form now gives the browser a concrete place to collect a test name.
Is the form missing?
Confirm the new section sits inside <main class="shell"> and above </main>. Save the file before refreshing Microsoft Edge.
Help me place the check-in form in index.html.
- Find the closing </main> line again.
- Insert the attendance-history section immediately above that line:
<section class="panel" aria-labelledby="history-title">
<div class="section-heading">
<h2 id="history-title">Recent attendance</h2>
<span>Newest first</span>
</div>
<ul id="event-list" class="event-list" aria-live="polite"></ul>
</section>
What does the history section provide?
The empty list gives the browser script a safe container for attendance events. The Newest first label tells the learner how the eventual results are ordered.
The list begins empty because no fictional check-in has reached the server yet. The browser logic adds an explicit empty-state message later.
- Save public/index.html.
- Return to Microsoft Edge.
- Refresh the page.
You should see Recent attendance beneath the check-in form. The Newest first label appears beside its heading.
- Select the public folder in the Explorer sidebar.
- Use the new-file control to create styles.css inside public.
- Style the attendance page by adding this code to public/styles.css:
:root { color-scheme: dark; font-family: "Segoe UI", sans-serif; background: #07111f; color: #eaf2ff; }
* { box-sizing: border-box; }
body { min-height: 100vh; margin: 0; background: radial-gradient(circle at top left, #173c66, #07111f 42%); }
button, input { font: inherit; }
.shell { width: min(760px, calc(100% - 32px)); margin: 0 auto; padding: 48px 0; }
.eyebrow { margin-bottom: 8px; color: #6ee7ff; font-size: 0.8rem; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; }
h1, h2, p { margin-top: 0; }
h1 { margin-bottom: 8px; font-size: clamp(2rem, 8vw, 3.5rem); }
.subtitle, .section-heading span, .event-time, .empty-state { color: #9eb0c7; }
.panel { margin-top: 18px; padding: 24px; border: 1px solid #29435f; border-radius: 18px; background: rgb(8 24 42 / 88%); }
label { display: block; margin-bottom: 8px; font-weight: 700; }
.form-row, .section-heading, .event-item { display: flex; align-items: center; gap: 12px; }
.section-heading, .event-item { justify-content: space-between; }
.section-heading h2 { margin-bottom: 0; }
input { min-width: 0; flex: 1; padding: 12px 14px; border: 1px solid #476786; border-radius: 10px; background: #07111f; color: #ffffff; }
button { padding: 12px 18px; border: 0; border-radius: 10px; background: #6ee7ff; color: #062033; font-weight: 800; cursor: pointer; }
button:disabled { cursor: wait; opacity: 0.6; }
.feedback { min-height: 24px; margin: 14px 0 0; }
.feedback.success { color: #86efac; }
.feedback.error { color: #fda4af; }
.event-list { display: grid; gap: 10px; margin: 18px 0 0; padding: 0; list-style: none; }
.event-item, .empty-state { padding: 14px; border-radius: 12px; background: #0e2944; }
.event-name { display: block; margin-bottom: 4px; font-weight: 700; }
.event-badge { flex: none; padding: 5px 9px; border-radius: 999px; background: rgb(134 239 172 / 14%); color: #86efac; font-size: 0.8rem; font-weight: 700; }
@media (max-width: 560px) { .form-row, .event-item { align-items: stretch; flex-direction: column; } }
How does the styling work?
- The shell limits the content width while preserving space at narrow screen sizes.
- The panels separate the form from the attendance history.
- The feedback classes use distinct colours for successful and failed requests.
- The media query stacks form controls and event details on narrow screens.
- Save public/styles.css.
- Return to Microsoft Edge.
- Refresh the page.
You should see a dark blue page with two rounded panels. The input and cyan Check In button should share one row at the current desktop width.
Does the page still look unstyled?
Confirm the file path is public/styles.css. Check that index.html still links to /styles.css.
Help me load the attendance page stylesheet.
✔️ Awesome, I've got index.html
Great. Confirm that public/index.html is saved inside the public folder.
ⓧ I'd like to double check index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Knooviq Attendance Core</title>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<main class="shell">
<section class="hero" aria-labelledby="page-title">
<p class="eyebrow">Knooviq Systems</p>
<h1 id="page-title">Attendance Core</h1>
<p class="subtitle">Use fictional names while authentication is being built.</p>
</section>
<section class="panel" aria-labelledby="check-in-title">
<h2 id="check-in-title">Record a check-in</h2>
<form id="check-in-form">
<label for="employee-name">Fictional employee name</label>
<div class="form-row">
<input id="employee-name" name="employeeName" type="text" maxlength="80" autocomplete="off" required />
<button id="submit-button" type="submit">Check In</button>
</div>
</form>
<p id="feedback" class="feedback" role="status" aria-live="polite"></p>
</section>
<section class="panel" aria-labelledby="history-title">
<div class="section-heading">
<h2 id="history-title">Recent attendance</h2>
<span>Newest first</span>
</div>
<ul id="event-list" class="event-list" aria-live="polite"></ul>
</section>
</main>
<script src="/app.js" defer></script>
</body>
</html>
✔️ Awesome, I've got styles.css
Your page structure now has its complete responsive styling. Keep the browser available for the next API check.
ⓧ I'd like to double check styles.css
:root { color-scheme: dark; font-family: "Segoe UI", sans-serif; background: #07111f; color: #eaf2ff; }
* { box-sizing: border-box; }
body { min-height: 100vh; margin: 0; background: radial-gradient(circle at top left, #173c66, #07111f 42%); }
button, input { font: inherit; }
.shell { width: min(760px, calc(100% - 32px)); margin: 0 auto; padding: 48px 0; }
.eyebrow { margin-bottom: 8px; color: #6ee7ff; font-size: 0.8rem; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; }
h1, h2, p { margin-top: 0; }
h1 { margin-bottom: 8px; font-size: clamp(2rem, 8vw, 3.5rem); }
.subtitle, .section-heading span, .event-time, .empty-state { color: #9eb0c7; }
.panel { margin-top: 18px; padding: 24px; border: 1px solid #29435f; border-radius: 18px; background: rgb(8 24 42 / 88%); }
label { display: block; margin-bottom: 8px; font-weight: 700; }
.form-row, .section-heading, .event-item { display: flex; align-items: center; gap: 12px; }
.section-heading, .event-item { justify-content: space-between; }
.section-heading h2 { margin-bottom: 0; }
input { min-width: 0; flex: 1; padding: 12px 14px; border: 1px solid #476786; border-radius: 10px; background: #07111f; color: #ffffff; }
button { padding: 12px 18px; border: 0; border-radius: 10px; background: #6ee7ff; color: #062033; font-weight: 800; cursor: pointer; }
button:disabled { cursor: wait; opacity: 0.6; }
.feedback { min-height: 24px; margin: 14px 0 0; }
.feedback.success { color: #86efac; }
.feedback.error { color: #fda4af; }
.event-list { display: grid; gap: 10px; margin: 18px 0 0; padding: 0; list-style: none; }
.event-item, .empty-state { padding: 14px; border-radius: 12px; background: #0e2944; }
.event-name { display: block; margin-bottom: 4px; font-weight: 700; }
.event-badge { flex: none; padding: 5px 9px; border-radius: 999px; background: rgb(134 239 172 / 14%); color: #86efac; font-size: 0.8rem; font-weight: 700; }
@media (max-width: 560px) { .form-row, .event-item { align-items: stretch; flex-direction: column; } }
Add the in-memory API
The page now needs REST routes that can read and create attendance events. The create route receives JSON from the browser.
Server-side validation checks the trimmed name before an event enters memory. This keeps invalid input away from the attendance history.
- Switch back to server.js.
- Replace its contents with this in-memory read API:
const express = require('express');
const app = express();
const port = Number(process.env.PORT) || 3000;
const attendanceEvents = [];
let nextId = 1;
app.use(express.json());
app.use(express.static('public'));
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', storage: 'memory' });
});
app.get('/api/attendance', (req, res) => {
res.json(attendanceEvents.slice(0, 20));
});
app.listen(port, () => {
console.log(`Knooviq Attendance Core is running at http://localhost:${port}`);
});
How does the read API work?
- The attendanceEvents array holds events for the current server process.
- The JSON middleware parses matching request bodies into req.body.
- The GET /api/attendance route returns up to the first 20 events.
- The static middleware continues serving the browser files from public.
- Save server.js.
- Switch to the running terminal.
- Press Ctrl+C to stop the current process.
- Start a new process with the updated read route by running:
npm start
What happens when the server starts?
Node.js creates a new empty attendanceEvents array. Express then begins accepting requests on port 3000.
Before opening the attendance route, what do you expect it to return when no check-ins exist?
- Open http://localhost:3000/api/attendance in Microsoft Edge.
You should see []. The empty array proves that the new read route is running.
Does the route return an error?
Confirm the terminal shows that Knooviq Attendance Core is running on port 3000. Check the address for the exact /api/attendance path.
Help me run the in-memory attendance route.
- Return to server.js.
- Find the app.listen block.
- Insert the create route immediately above it:
app.post('/api/attendance', (req, res) => {
const employeeName =
typeof req.body.employeeName === 'string'
? req.body.employeeName.trim()
: '';
if (!employeeName || employeeName.length > 80) {
return res.status(400).json({
error: 'Enter an employee name between 1 and 80 characters.',
});
}
const event = {
id: nextId++,
employee_name: employeeName,
event_type: 'check_in',
occurred_at: new Date().toISOString(),
};
attendanceEvents.unshift(event);
return res.status(201).json(event);
});
How does the create route work?
- The route accepts a string from req.body.employeeName and removes surrounding spaces.
- The validation rejects an empty name or a name longer than 80 characters.
- The server assigns the event identifier and occurred_at timestamp.
- The new event enters the front of the array before the route returns HTTP 201.
- Save server.js.
- Switch to the running terminal.
- Press Ctrl+C to stop the current process.
- Load the new create route by starting the server again:
npm start
Why restart the process?
The current server does not reload files automatically. Restarting creates a process that includes the new POST /api/attendance route.
- Return to http://localhost:3000/api/attendance.
- Refresh the route.
You should still see []. The read route still works after adding the create route.
Did the server stop after the edit?
Read the terminal output for the file and line that caused the failure. Compare the route braces and parentheses with the complete file below.
Help me fix the attendance POST route.
✔️ Awesome, my API is ready
Your server now reads and creates attendance events in memory. Keep npm start running for the browser connection.
ⓧ I'd like to double check server.js
const express = require('express');
const app = express();
const port = Number(process.env.PORT) || 3000;
const attendanceEvents = [];
let nextId = 1;
app.use(express.json());
app.use(express.static('public'));
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', storage: 'memory' });
});
app.get('/api/attendance', (req, res) => {
res.json(attendanceEvents.slice(0, 20));
});
app.post('/api/attendance', (req, res) => {
const employeeName =
typeof req.body.employeeName === 'string'
? req.body.employeeName.trim()
: '';
if (!employeeName || employeeName.length > 80) {
return res.status(400).json({
error: 'Enter an employee name between 1 and 80 characters.',
});
}
const event = {
id: nextId++,
employee_name: employeeName,
event_type: 'check_in',
occurred_at: new Date().toISOString(),
};
attendanceEvents.unshift(event);
return res.status(201).json(event);
});
app.listen(port, () => {
console.log(`Knooviq Attendance Core is running at http://localhost:${port}`);
});
Connect the page to the API
The browser now needs a request helper and a safe rendering function. Same-origin fetch() calls can reach the Express routes without separate cross-origin configuration.
The page creates result elements through the DOM and assigns names through textContent. Each server timestamp is formatted in the browser's local time zone.
- Select the public folder in the Explorer sidebar.
- Use the new-file control to create app.js inside public.
- Add the page references and request helper to public/app.js:
const form = document.querySelector('#check-in-form');
const employeeNameInput = document.querySelector('#employee-name');
const submitButton = document.querySelector('#submit-button');
const feedback = document.querySelector('#feedback');
const eventList = document.querySelector('#event-list');
function setFeedback(message, kind = '') {
feedback.textContent = message;
feedback.className = kind ? `feedback ${kind}` : 'feedback';
}
async function requestJson(url, options) {
const response = await fetch(url, options);
const data = await response.json();
if (!response.ok) throw new Error(data.error || 'The request failed.');
return data;
}
What do these helpers manage?
- The page references connect the script to the form controls and result areas.
- The setFeedback() helper changes the feedback text and visual state.
- The requestJson() helper sends a request and converts the response into data.
- An unsuccessful response becomes an error that the page can display.
- Save public/app.js.
- Return to Microsoft Edge.
- Refresh http://localhost:3000.
You should see the complete form with an empty feedback area. The page should remain responsive while the helper layer loads.
- Return to the end of public/app.js.
- Add the event-rendering function below requestJson():
function renderEvents(events) {
eventList.replaceChildren();
if (events.length === 0) {
const emptyItem = document.createElement('li');
emptyItem.className = 'empty-state';
emptyItem.textContent = 'No fictional check-ins have been recorded yet.';
eventList.append(emptyItem);
return;
}
for (const event of events) {
const item = document.createElement('li');
const details = document.createElement('div');
const name = document.createElement('span');
const time = document.createElement('time');
const badge = document.createElement('span');
item.className = 'event-item';
name.className = 'event-name';
name.textContent = event.employee_name;
time.className = 'event-time';
time.dateTime = event.occurred_at;
time.textContent = new Date(event.occurred_at).toLocaleString();
badge.className = 'event-badge';
badge.textContent = 'Checked in';
details.append(name, time);
item.append(details, badge);
eventList.append(item);
}
}
How are events rendered safely?
The function clears the previous list before drawing the current server response. An empty response creates a visible empty-state item.
Each saved event gets dedicated name and time elements. Assigning the employee name through textContent treats the value as text.
- The occurred_at value remains available as the machine-readable time.
- The toLocaleString() call creates the date and time you read on screen.
- Save public/app.js.
- Confirm that Visual Studio Code no longer shows an unsaved indicator on the app.js tab.
- Add the initial history loader below renderEvents():
async function loadEvents() {
try { renderEvents(await requestJson('/api/attendance')); }
catch (error) { setFeedback(error.message, 'error'); }
}
loadEvents();
What does the initial load prove?
The loadEvents() function requests the current attendance history when the script starts. A failed request is routed into the existing feedback area.
The final call starts the browser-to-server read path as soon as the page is ready. The returned array determines which history state you see.
- Save public/app.js.
- Return to Microsoft Edge.
- Refresh the attendance page.
You should see No fictional check-ins have been recorded yet inside the history panel. That message proves the browser loaded [] from the Express API.
Is the empty-state message missing?
Confirm the server is still running. Check that app.js is inside public and that index.html loads /app.js.
Help me load attendance events into the page.
- Return to the end of public/app.js.
- Add the form submission handler below loadEvents():
form.addEventListener('submit', async (event) => {
event.preventDefault();
submitButton.disabled = true;
setFeedback('Saving check-in...');
try {
const savedEvent = await requestJson('/api/attendance', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ employeeName: employeeNameInput.value.trim() }),
});
setFeedback(`${savedEvent.employee_name} checked in successfully.`, 'success');
form.reset();
await loadEvents();
} catch (error) {
setFeedback(error.message, 'error');
} finally {
submitButton.disabled = false;
employeeNameInput.focus();
}
});
How does submission complete the flow?
- The handler keeps the browser on the current page when the form is submitted.
- The button stays disabled while the request is running.
- The request sends a trimmed employeeName value as JSON to the create route.
- A successful response clears the form and reloads the newest server history.
- Save public/app.js.
- Return to Microsoft Edge.
- Refresh http://localhost:3000.
Before submitting the form, what do you expect the empty history to show after the server accepts one fictional name?
- Enter Amina Okafor in the Fictional employee name field.
- Click Check In.
You should see Amina Okafor checked in successfully above the history. A new Amina Okafor entry should show a Checked in badge with your browser-local date and time.
That is the first working vertical slice complete. Your browser can now create an event through Express and display the server-recorded result without reloading the page.
Does the check-in fail?
Confirm the terminal still shows the running server. Check that the form sends employeeName and that the server reads the same property from req.body.
Help me debug the browser-to-Express check-in flow.
✔️ Awesome, the page works
Your saved public/app.js now loads history and creates fictional check-ins through the API.
ⓧ I'd like to double check app.js
const form = document.querySelector('#check-in-form');
const employeeNameInput = document.querySelector('#employee-name');
const submitButton = document.querySelector('#submit-button');
const feedback = document.querySelector('#feedback');
const eventList = document.querySelector('#event-list');
function setFeedback(message, kind = '') {
feedback.textContent = message;
feedback.className = kind ? `feedback ${kind}` : 'feedback';
}
async function requestJson(url, options) {
const response = await fetch(url, options);
const data = await response.json();
if (!response.ok) throw new Error(data.error || 'The request failed.');
return data;
}
function renderEvents(events) {
eventList.replaceChildren();
if (events.length === 0) {
const emptyItem = document.createElement('li');
emptyItem.className = 'empty-state';
emptyItem.textContent = 'No fictional check-ins have been recorded yet.';
eventList.append(emptyItem);
return;
}
for (const event of events) {
const item = document.createElement('li');
const details = document.createElement('div');
const name = document.createElement('span');
const time = document.createElement('time');
const badge = document.createElement('span');
item.className = 'event-item';
name.className = 'event-name';
name.textContent = event.employee_name;
time.className = 'event-time';
time.dateTime = event.occurred_at;
time.textContent = new Date(event.occurred_at).toLocaleString();
badge.className = 'event-badge';
badge.textContent = 'Checked in';
details.append(name, time);
item.append(details, badge);
eventList.append(item);
}
}
async function loadEvents() {
try { renderEvents(await requestJson('/api/attendance')); }
catch (error) { setFeedback(error.message, 'error'); }
}
form.addEventListener('submit', async (event) => {
event.preventDefault();
submitButton.disabled = true;
setFeedback('Saving check-in...');
try {
const savedEvent = await requestJson('/api/attendance', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ employeeName: employeeNameInput.value.trim() }),
});
setFeedback(`${savedEvent.employee_name} checked in successfully.`, 'success');
form.reset();
await loadEvents();
} catch (error) {
setFeedback(error.message, 'error');
} finally {
submitButton.disabled = false;
employeeNameInput.focus();
}
});
loadEvents();
Your first check-in now travels from the browser to Express and back into the visible history. Next up, you will test what happens to that event when the server process ends.
Expose the Memory Problem
Your browser can now send a fictional check-in to the Express API. The page displays the server response immediately.
The working page currently treats Node.js volatile process memory as its attendance store. A complete restart tests whether that store can support persistent attendance history.
In this step, get ready to:
- Create two fictional check-ins as a clear before state.
- Inspect the active API response while the current Express process is running.
- Test the in-memory history across a complete Express restart.
Establish the before state
A durability test needs a before state that you can recognize after the server changes. Two distinct fictional names make any missing records easy to spot.
- Return to the attendance page in Microsoft Edge from the previous step.
- Enter Amina Okafor in the Fictional employee name field.
- Click Check In.
You'll see Amina Okafor appear in Recent attendance with a browser-local date and time.
- Enter Mateo Silva in the Fictional employee name field.
- Click Check In.
You'll see Mateo Silva above Amina Okafor in the history list. Both entries remain visible while the same server process is running.
The browser list proves the interface has state. The API response confirms that the same state lives in the current server process.
- Open a new tab in Microsoft Edge.
- Visit http://localhost:3000/api/attendance to inspect the current process state.
You'll see a JSON array containing both fictional names. This proves the GET /api/attendance route can read events while the current Node.js process remains alive.
Reproduce the failure
A server restart crosses a process boundary. The new process runs server.js from the beginning.
- Switch back to the Visual Studio Code terminal running Express from the previous step.
- Press Ctrl+C to stop the current Express process.
Before you start a new process, do you think the two attendance records will still appear after a refresh?
- Restart Express as a new process by running this command:
npm start
What does this command do?
The npm start command runs the start script from package.json. That script launches server.js in a fresh Node.js process.
- Wait until the terminal prints Knooviq Attendance Core is running at http://localhost:3000.
- Return to the Microsoft Edge attendance tab from earlier.
- Refresh the page using the browser's Refresh button.
You'll see No fictional check-ins have been recorded yet. The previous entries are gone.
That empty list is the intended result. attendanceEvents is created as an empty array every time server.js starts.
Its contents belonged only to the process you stopped. Volatile process memory cannot preserve records across a restart.
Still see the old records?
- Confirm the first Express process stopped after you pressed Ctrl+C.
- Check that npm start printed the startup message for the new process.
- Refresh the attendance page after the new startup message appears.
Help me reproduce the in-memory data loss.
You have proved that the current attendance history cannot survive an Express restart. Next, you'll replace volatile memory with Supabase PostgreSQL so a new process can retrieve earlier check-ins.
Replace Memory with Supabase PostgreSQL
The restart proved that your Express server loses every event with its Node.js process. The page works until that volatile memory disappears.
Now Supabase PostgreSQL will store each event outside the local process. Your existing browser flow stays in place while the API gains durable storage.
In this step, get ready to:
- Create the protected attendance table in Supabase.
- Store the database connection string outside your code.
- Replace the memory routes with parameterized PostgreSQL queries.
Create the protected attendance table
A database table defines the shape of every saved attendance event. Its constraints also reject records that do not fit the fictional check-in model.
- Create a file named schema.sql inside the knooviq-attendance-core folder using the Visual Studio Code Explorer sidebar.
- Define the attendance table by pasting this code into schema.sql:
create table if not exists public.attendance_events (
id bigint generated always as identity primary key,
employee_name text not null,
event_type text not null default 'check_in'
check (event_type = 'check_in'),
occurred_at timestamptz not null default now()
);
alter table public.attendance_events enable row level security;
What Does This Schema Do?
- The id column gives each event an automatically generated identity.
- The employee_name column requires a fictional employee name.
- The event_type constraint allows only check_in records.
- The occurred_at column records the insertion time as a timestamp with time zone.
- Row Level Security blocks public Data API access because this project does not create a public policy.
- Save schema.sql.
- Copy the complete contents of schema.sql.
- Return to the Supabase project Dashboard from earlier.
- Open SQL Editor in the project Dashboard.
- Paste the copied schema into the SQL editor.
- Click Run.
- Open Table Editor to check the new table.
You should see attendance_events with zero rows. This confirms that Supabase created the protected table before the API writes any fictional events.
Table Missing After Running the Schema?
Return to SQL Editor to check whether the query reported a syntax error. Confirm that the final alter table statement targets public.attendance_events.
Refresh Table Editor after the query succeeds.
Help me troubleshoot why my attendance_events table does not appear after I ran schema.sql in the Supabase SQL Editor.
✔️ Awesome, I've got everything!
Your protected attendance_events table is ready for the Express API.
ⓧ I'd like to double check the full code
create table if not exists public.attendance_events (
id bigint generated always as identity primary key,
employee_name text not null,
event_type text not null default 'check_in'
check (event_type = 'check_in'),
occurred_at timestamptz not null default now()
);
alter table public.attendance_events enable row level security;
Use this complete file as a reference when comparing your saved schema.sql.
Store the database connection safely
The API needs a connection string that identifies your Supabase database. The existing .env file keeps that credential outside server.js.
This part handles your database password. Keep the completed connection string private because it grants direct database access.
- Return to your Supabase project Dashboard.
- Click Connect.
- Choose Session pooler as the connection method.
- Copy the exact connection string.
The copied value points your persistent local Express process to the shared Session pooler. You will complete its password and encryption settings inside the untracked environment file.
- Switch back to Visual Studio Code.
- Select the existing .env file in the Explorer sidebar.
- Replace its temporary comment with DATABASE_URL= followed by the copied connection string.
- Percent-encode any reserved characters in your database password when necessary.
- Replace the password placeholder in the connection string with your database password.
Your private line should now begin with DATABASE_URL=. The password should no longer appear as a placeholder.
- Append ?sslmode=require to the connection string.
- Save .env.
- Select the existing .env.example file in the Explorer sidebar.
- Confirm that .env.example still contains only the secret-free connection-string template.
- Select the existing .gitignore file in the Explorer sidebar.
- Confirm that .env remains listed in .gitignore.
Why Use the Session Pooler?
The Session pooler supports a persistent backend on an IPv4-only network. That fits the local Express process used in this project.
The sslmode=require setting encrypts the database connection. Your password remains outside the committed application code.
- Switch back to the Visual Studio Code terminal from earlier.
- Stop the running Express process with Ctrl+C.
- Confirm that Node.js can load the updated environment file by starting the existing server:
npm start
You should see the existing Knooviq server startup message. This confirms that Node.js can read .env without exposing its value.
Server Not Starting After the Environment Update?
Confirm that the file is named .env inside knooviq-attendance-core. Check that its only line starts with DATABASE_URL=.
Keep the connection string on one line. Remove any surrounding quotation marks that were not part of the copied value.
Help me troubleshoot why npm start cannot load my DATABASE_URL from .env without asking me to reveal the credential.
Replace memory routes with PostgreSQL
The node-postgres library gives the server a reusable connection pool. Each route can send one query through that pool without managing individual database connections.
Parameterized SQL sends employee input separately through $1 plus $2. This keeps fictional names out of the SQL text itself.
Why node-postgres Instead of the Data API?
This project focuses on the boundary between a browser, an Express API, and PostgreSQL. Direct SQL keeps every part of that data path visible.
The Supabase Data API remains useful when browser authentication becomes part of the design. This server currently owns every database request.
The current GET and POST handlers still need attendanceEvents during the first connection test. Keep the memory declarations temporarily while you add the database setup.
- Stop the running server with Ctrl+C.
- Replace the opening section of server.js above app.use(express.json()); with the code below:
const express = require('express');
const { Pool } = require('pg');
if (!process.env.DATABASE_URL) {
throw new Error(
'DATABASE_URL is missing. Add your Supabase Session pooler connection string to .env.'
);
}
const app = express();
const port = Number(process.env.PORT) || 3000;
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
What Does This Setup Do?
- The Pool import gives Express access to PostgreSQL connections.
- The environment check stops the server when DATABASE_URL is missing.
- The module-scope pool is shared by every request handler.
- Keep the temporary attendanceEvents and nextId declarations below the new pool setup.
- Save server.js.
- Test the database configuration guard by starting the server:
npm start
You should see the Knooviq server startup message. This confirms that the environment check passed before any route queries the database.
Seeing a DATABASE_URL Message?
The server intentionally stops when DATABASE_URL is unavailable. Confirm that .env sits beside server.js.
Confirm that package.json still starts Node.js with the environment-file option.
Help me diagnose why process.env.DATABASE_URL is missing in server.js without asking me to reveal the database credential.
A health check should prove that the server can execute a real database query. The returned database time gives you a visible signal from PostgreSQL.
- Stop the running server with Ctrl+C.
- Replace the existing /api/health handler in server.js with this database-backed version:
app.get('/api/health', async (req, res) => {
const result = await pool.query('SELECT NOW() AS database_time');
res.json({ status: 'ok', databaseTime: result.rows[0].database_time });
});
What Does This Health Check Prove?
The route waits for PostgreSQL to run SELECT NOW() AS database_time. A response can only be sent after the database returns a row.
The databaseTime field comes from that returned row. It proves that Express reached Supabase.
- Save server.js.
Before you test the endpoint, do you expect the new database connection to succeed or expose a connection-string problem?
- Start the database-backed health route by running:
npm start
The running process now waits for requests that can exercise the Supabase connection.
- Open http://localhost:3000/api/health in Microsoft Edge.
You should see JSON containing "status":"ok" plus a databaseTime value. Your local API has now completed a live PostgreSQL query.
Health Request Not Reaching PostgreSQL?
Check the Visual Studio Code terminal for the database connection error. A password issue usually means the connection string still contains its placeholder or includes an unencoded reserved character.
Confirm that the connection string ends with ?sslmode=require.
Help me troubleshoot why the Supabase PostgreSQL health query fails without asking me to paste my DATABASE_URL or password.
The GET route now needs to read the newest saved rows from attendance_events. PostgreSQL will apply the ordering before Express returns the JSON array.
- Stop the running server with Ctrl+C.
- Replace the existing GET /api/attendance handler with this query-backed version:
app.get('/api/attendance', async (req, res) => {
const result = await pool.query(`
SELECT id, employee_name, event_type, occurred_at
FROM public.attendance_events
ORDER BY occurred_at DESC
LIMIT 20
`);
res.json(result.rows);
});
What Does This Read Query Do?
- The query selects the four fields needed by the existing browser client.
- The descending timestamp order puts the newest event first.
- The limit keeps the response to the 20 most recent records.
- The route returns result.rows as JSON.
- Save server.js.
- Start the updated GET route by running:
npm start
The server is ready to retrieve attendance events from the table you created.
- Open http://localhost:3000/api/attendance in Microsoft Edge.
You should see [] because the new PostgreSQL table still has zero rows. The response now comes from Supabase instead of attendanceEvents.
Attendance Request Returning an Error?
Confirm that schema.sql created public.attendance_events. Check that the table name in server.js matches it exactly.
Help me troubleshoot why my GET attendance query cannot read public.attendance_events from Supabase.
The POST route completes the storage swap. It keeps the existing server-side name validation while sending accepted input through parameter values.
- Stop the running server with Ctrl+C.
- Replace the existing POST /api/attendance handler with this parameterized insert:
app.post('/api/attendance', async (req, res) => {
const employeeName = typeof req.body.employeeName === 'string' ? req.body.employeeName.trim() : '';
if (!employeeName || employeeName.length > 80) {
return res.status(400).json({ error: 'Enter an employee name between 1 and 80 characters.' });
}
const result = await pool.query(`
INSERT INTO public.attendance_events (employee_name, event_type)
VALUES ($1, $2)
RETURNING id, employee_name, event_type, occurred_at
`, [employeeName, 'check_in']);
return res.status(201).json(result.rows[0]);
});
What Does This Insert Do?
- The validation accepts a trimmed fictional name between 1 and 80 characters.
- The $1 placeholder receives employeeName separately from the SQL text.
- The $2 placeholder receives the fixed check_in event type.
- The RETURNING clause sends the saved PostgreSQL row back to the browser.
- The HTTP status 201 confirms that the request created a record.
- Delete the obsolete memory declarations for attendanceEvents plus nextId from server.js.
- Save server.js.
Before you run the finished server, do you expect the existing page to display a row returned by PostgreSQL?
- Start the completed database-backed API by running:
npm start
The server now uses Supabase for its health check plus both attendance routes.
- Open http://localhost:3000 in Microsoft Edge.
- Enter Amina Okafor in the fictional employee name field.
- Click Check In.
You should see a new Checked in entry with a browser-local date and time. That row came back from PostgreSQL through the existing browser feedback path.
Check-In Not Appearing?
Check the terminal for a database error after clicking Check In. Confirm that the insert names the same four columns returned by the GET route.
Refresh the page after the request succeeds. The GET route should retrieve the saved row from public.attendance_events.
Help me troubleshoot why my PostgreSQL-backed check-in does not appear in the attendance page.
✔️ Awesome, I've got everything!
Your health check plus attendance routes now use Supabase PostgreSQL. Keep the server running for the final checkpoint.
ⓧ I'd like to double check the full code
const express = require('express');
const { Pool } = require('pg');
if (!process.env.DATABASE_URL) {
throw new Error(
'DATABASE_URL is missing. Add your Supabase Session pooler connection string to .env.'
);
}
const app = express();
const port = Number(process.env.PORT) || 3000;
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
app.use(express.json());
app.use(express.static('public'));
app.get('/api/health', async (req, res) => {
const result = await pool.query('SELECT NOW() AS database_time');
res.json({ status: 'ok', databaseTime: result.rows[0].database_time });
});
app.get('/api/attendance', async (req, res) => {
const result = await pool.query(`
SELECT id, employee_name, event_type, occurred_at
FROM public.attendance_events
ORDER BY occurred_at DESC
LIMIT 20
`);
res.json(result.rows);
});
app.post('/api/attendance', async (req, res) => {
const employeeName = typeof req.body.employeeName === 'string' ? req.body.employeeName.trim() : '';
if (!employeeName || employeeName.length > 80) {
return res.status(400).json({ error: 'Enter an employee name between 1 and 80 characters.' });
}
const result = await pool.query(`
INSERT INTO public.attendance_events (employee_name, event_type)
VALUES ($1, $2)
RETURNING id, employee_name, event_type, occurred_at
`, [employeeName, 'check_in']);
return res.status(201).json(result.rows[0]);
});
app.use((error, req, res, next) => {
console.error(error);
return res.status(500).json({ error: 'The server could not complete the request.' });
});
app.listen(port, (error) => {
if (error) throw error;
console.log(`Knooviq Attendance Core is running at http://localhost:${port}`);
});
Use this complete file as a reference when comparing your database-backed server.js.
That is the storage swap complete. Your browser can now create and display fictional attendance events through Supabase PostgreSQL.
Next, you will restart the local server again and prove that these database records survive the process boundary.
Prove the Records Survive
Your check-in page now sends fictional attendance events through Express to Supabase PostgreSQL. The saved entries appear in Microsoft Edge after the database returns them.
A successful insert proves that one request reached the database. Persistence is proven when a brand-new Node.js process retrieves records created by the previous process.
In this step, get ready to:
- Create two distinct fictional attendance records as a clear before state.
- Restart Express in a brand-new process.
- Cross-check the surviving records in the browser and Supabase.
Test across a process boundary
Two fresh records give you an unmistakable before state. Distinct fictional names make the restart comparison easy to follow.
- Switch back to the attendance page in Microsoft Edge.
- Select the Fictional employee name field.
- Enter Amina Okafor.
- Click Check In.
You should see a success message for Amina Okafor. Her new Checked in entry appears at the top of the attendance history.
- Enter Mateo Silva in the same field.
- Click Check In.
You should see Mateo Silva above Amina Okafor. Each entry shows the time formatted for your browser.
- Record the timestamp beside Amina Okafor: Amina Okafor's displayed timestamp.
- Record the timestamp beside Mateo Silva: Mateo Silva's displayed timestamp.
Good work. These two fresh records give your restart test a clear baseline.
- Switch back to the Visual Studio Code terminal that is running Express.
- Press Ctrl+C to stop the current process.
- Start a brand-new Express process by running this command:
npm start
What does this restart prove?
- The npm start command runs the start script from package.json.
- The start script runs node --env-file=.env server.js in a new process.
- The new process creates its own Pool before querying PostgreSQL.
You should see Knooviq Attendance Core is running at http://localhost:3000 in the terminal. This confirms the replacement process is ready to serve requests.
Before you reload, predict whether the same names and timestamps will still be present.
- Switch back to Microsoft Edge.
- Reload http://localhost:3000.
You should see Mateo Silva and Amina Okafor return without submitting another check-in. The newer entry remains first.
- Compare Amina Okafor's reloaded time with Amina Okafor's displayed timestamp.
- Compare Mateo Silva's reloaded time with Mateo Silva's displayed timestamp.
The matching entries prove that the new process retrieved data created by the old process. Your attendance history now survives a complete local server restart.
Records missing after the restart?
- Check that the Visual Studio Code terminal is still inside knooviq-attendance-core.
- Confirm that .env still contains DATABASE_URL without displaying or sharing its value.
- Check the terminal for a database connection error before refreshing Microsoft Edge again.
If the records are still missing, help me diagnose why my PostgreSQL attendance records disappeared after restarting Express.
Cross-check both system views
The browser proves that the new Express process can retrieve the records. Supabase Table Editor gives you a second view of the same stored rows.
Your Row Level Security setting remains enabled throughout this check. The table still has no public Data API policy.
- Switch back to your signed-in Supabase project from earlier.
- Select Table Editor in the project sidebar.
- Select attendance_events.
Before you compare the two views, predict which fields will connect each browser entry to its database row.
- Confirm that the browser lists the two test entries in descending timestamp order.
- Find the matching employee_name rows for Amina Okafor and Mateo Silva.
- Compare each row's occurred_at value with the corresponding browser entry.
- Use only fictional names for any further checks.
You should see matching rows for both fictional employees in public.attendance_events. The browser shows the newer record first because GET /api/attendance orders records by occurred_at in descending order.
That's persistence proven from both sides. A fresh Express process and Supabase now agree on the same saved attendance history.
Secret mission
Block Rapid Duplicate Check-Ins
Your attendance flow now keeps every successful check-in across server restarts. In this mission, you will add a 60-second duplicate guard that rejects an immediate repeat without adding another database row.
Clean Up Your Resources
Clean Up Your Resources
Your local workspace and Supabase Free Plan cost $0 per month for this project. Choose the cleanup option that matches your next move.
Resources you used:
- The local knooviq-attendance-core workspace containing your project files, installed node_modules dependencies, and untracked .env credential file.
- The local Express server process using port 3000.
- The Supabase Free Plan project containing the protected public.attendance_events PostgreSQL table and its fictional records.
Keep everything running
No action is needed. Choose this option while you are actively building or testing the attendance flow.
- Keep the knooviq-attendance-core workspace in its current location.
- Keep the Supabase project available for testing with fictional data.
- Stop the local server with Ctrl+C whenever you finish coding.
- Resume the Supabase project from its dashboard if inactivity causes it to pause.
What happens after inactivity?
Supabase may pause a Free project after one week of inactivity. Your database remains available for you to resume later.
Pause - I'll come back to this later
Shut down the local server to free its terminal. Pause the cloud project while preserving its database for your return.
- Switch back to the Visual Studio Code terminal from earlier.
- Press Ctrl+C to stop the local Express server.
- Return to your signed-in Supabase project.
- Select Settings in the project sidebar.
- Select General.
- Select Project availability.
- Select Pause Project.
Your cloud project is now paused. Supabase preserves its database for a later return.
Delete - I don't want to use this again
Removing everything is permanent, so pause here to confirm you no longer need the code or fictional records. This option also removes the local database credential stored in the workspace.
- Switch back to the Visual Studio Code terminal from earlier.
- Press Ctrl+C if the local Express server is still running.
- Remove the local workspace from its parent folder by running these commands in a Command Prompt terminal:
cd ..
rmdir /s /q knooviq-attendance-core
What do these commands remove?
- The first command moves the terminal out of the workspace so Windows can remove the folder.
- The second command permanently removes knooviq-attendance-core with its project files, dependencies, and local .env file.
- Confirm the knooviq-attendance-core folder no longer appears in the Visual Studio Code Explorer.
- Return to your signed-in Supabase project.
- Select Settings in the project sidebar.
- Select General.
- Select Delete project.
- Select Delete Project.
- Enter knooviq-attendance-core as the exact project name.
- Select Delete to confirm permanent deletion.
Your Supabase project is now permanently removed. Its database and fictional attendance records are gone with it.
Nice Work!
Nice Work!
You did it! You built a fictional attendance flow that carries browser requests through Express into Supabase PostgreSQL. Its records survive a full local server restart.
You've learned how to:
- Connected a responsive check-in page to Express REST routes with browser fetch() requests. Added server-side validation for fictional employee names.
- Reproduced data loss from volatile memory across a server restart. Replaced the temporary array with Supabase PostgreSQL queries through connection pooling. Proved the saved rows survived a brand-new server process.
- Protected the database credential with an environment variable. Used parameterized SQL to separate employee input from query text. Enabled Row Level Security for the SQL-created table.
- Secret Mission: Added an application-level rapid duplicate guard that rejects a repeated fictional check-in within 60 seconds. Preserved the browser's existing error-feedback path.
Ready to quiz yourself?