Live Attendance Updates with Supabase
Stream database changes to your web app in real time with Supabase.
Introduction
30 Second Summary
Picture a school admin who has to keep refreshing a page just to check if teachers have submitted attendance. Every refresh is wasted time.
In this project, you will upgrade your school attendance PWA with real-time updates. When a teacher submits attendance, the admin's dashboard updates instantly with no page refresh needed.
What You'll Build
The admin opens the app to a live status board where each class card flips from red to green the instant a teacher submits attendance, and a toast notification slides in to announce the class name.
By the end of this project, you'll have:
- A live "Today's Status" tab where the admin sees every class with a submitted or waiting badge, updating automatically through a Supabase Realtime subscription.
- Toast notifications that slide in and auto-dismiss when new attendance arrives, showing exactly which class just submitted.
- A redeployed PWA on Cloudflare Pages with real-time capabilities, still running at $0/month.
- Secret Mission: Add Supabase Presence so the admin can see which teachers are currently online in the app.
Do I need to complete Part 1 first?
Yes. This project builds directly on the school attendance PWA from Part 1. You need your Supabase project and Cloudflare Pages deployment still active before starting.
Before We Start
Before diving into code, take a moment to lock in what you are adding to your attendance system and why it matters.
Enable Realtime on the Attendance Table
Right now, the admin has no way of knowing when a teacher submits attendance without manually refreshing the page. They have to keep reloading the Records tab and hope new entries appear.
Supabase Realtime can push database changes to the browser the instant they happen. Before any subscription will work, the attendance table needs to be registered with a PostgreSQL publication first.
In this step, get ready to:
- Enable Realtime on the attendance table through the Supabase SQL Editor.
- Verify that INSERT events stream to the browser console in real time.
- Clean up the temporary test code.
Verify Part 1 is running and enable Realtime
Your Supabase project already has a publication called supabase_realtime. Any table added to this publication broadcasts its changes to Realtime subscribers.
- Confirm your dev server is still running at http://localhost:5173.
- Log in as the admin to verify the app loads correctly.
- Switch to the Supabase dashboard and click SQL Editor in the left sidebar.
- Add the attendance table to the Realtime publication by running this SQL:
alter publication supabase_realtime add table attendance;
- Click Run.
- Confirm you see a success message.
Now verify the change took effect in the dashboard.
- Click Database in the left sidebar.
- Click Publications.
- Under supabase_realtime, confirm the attendance table is listed as enabled.
Not seeing the attendance table in Publications?
- Make sure you clicked Run (the green play button) after pasting the SQL.
- Check for typos in the table name attendance. It must match your table name exactly.
- If you see an error saying the table is already part of the publication, that means Realtime was previously enabled. You can continue to the next substep.
Why isn't my attendance table showing in the supabase_realtime publication?
Test the Realtime connection from the browser console
The publication is enabled, but you have not proven that events actually reach the browser. A quick console test will confirm the full pipeline works.
- In your browser with the app at localhost:5173, press Cmd+Option+I (macOS) or F12 (Windows) to open the developer tools.
- Click the Console tab.
- Paste this test snippet into the console and press Enter:
const testChannel = window.__supabase
? window.__supabase.channel('test-attendance')
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'attendance' }, payload => console.log('Realtime INSERT:', payload))
.subscribe(status => console.log('Subscription status:', status))
: console.log('supabase not found on window')
What does this test snippet do?
- It checks if window.__supabase exists. If it does, it creates a Realtime channel called test-attendance.
- The channel subscribes to INSERT events on the attendance table and logs the payload to the console.
- If window.__supabase is not found, it logs a message so you know the client is not exposed yet.
You should see supabase not found on window in the console. The Supabase client lives inside your app's modules, not on the global window object.
- Switch to your code editor and open src/supabase.js.
- Add this line at the very bottom of the file to temporarily expose the client:
// Temporarily expose the Supabase client for console testing
window.__supabase = supabase
- Save src/supabase.js.
- Switch back to the browser and refresh the page.
- Paste the same test snippet from above into the console again and press Enter.
This time you should see Subscription status: SUBSCRIBED in the console. The Realtime channel is now listening for attendance inserts.
Still seeing 'supabase not found on window'?
- Confirm you saved src/supabase.js after adding the window.__supabase = supabase line.
- Make sure you refreshed the browser page after saving the file.
- Check that the line is at the bottom of the file, after the export statement for the Supabase client.
Why is window.__supabase still undefined after I added the line?
Now prove the full pipeline by submitting real attendance data.
- Open a second browser tab and go to your app at the same localhost URL.
- Log in as the teacher.
- Submit attendance for a class.
Before you switch back to the admin tab, what do you think you will see in the console?
- Switch back to the admin tab and check the console.
You should see a Realtime INSERT: log entry containing the full attendance record payload. This proves the Realtime subscription is streaming changes from your database to the browser.
Not seeing the Realtime INSERT log?
- Make sure the console in the admin tab shows Subscription status: SUBSCRIBED before you submit attendance in the teacher tab.
- Go back to Database > Publications in the Supabase dashboard and confirm attendance is listed under supabase_realtime.
- Try refreshing the admin tab, pasting the test snippet again, and then submitting attendance from the teacher tab.
Why am I not seeing Realtime INSERT events in my browser console?
Clean up the test code
The window.__supabase line was only needed for the console test. Leaving it in exposes your Supabase client globally, which you do not want in a production app.
- In src/supabase.js, find this line at the bottom of the file:
// Temporarily expose the Supabase client for console testing
window.__supabase = supabase
- Delete both lines (the comment and the code).
- Save src/supabase.js. The file should now look exactly as it did before the test.
Realtime events are flowing from your database to the browser. Next up, you will build a live status board that puts this subscription to work.
Build the Today's Status Board
Your Supabase Realtime connection is live. INSERT events from the attendance table are streaming straight to the browser. Now you need somewhere useful for those events to land.
Right now, the admin has to open the Records tab and filter by today's date just to check which classes have submitted attendance. A dedicated "Today" status board would show every class at a glance, with a clear submitted or waiting indicator for each one.
In this step, get ready to:
- Add a Today tab to the admin dashboard navigation.
- Build a status board that fetches classes and today's attendance records.
- Style the status cards with submitted and waiting badges.
Add the Today tab to the admin dashboard
The admin dashboard currently has three tabs: Classes, Students, and Records. You need a fourth tab that loads the status board.
- In your code editor, open src/admin.js.
- Inside the renderAdminDashboard function, find the container.innerHTML template string with the three tab buttons.
- Find this line near the end of the nav:
<button class="tab" data-tab="records">Records</button>
</nav>
- Add a new Today button before the closing </nav> tag, so the two lines now look like this:
<button class="tab" data-tab="records">Records</button>
<button class="tab" data-tab="today">Today</button>
</nav>
- Now scroll down to the tab click handler (the forEach loop below the nav).
- Find the existing conditions that check which tab was clicked:
if (tabName === 'classes') renderClassesTab()
else if (tabName === 'students') renderStudentsTab()
else if (tabName === 'records') renderRecordsTab()
- Add a new condition for the today tab at the end, so the block now looks like this:
if (tabName === 'classes') renderClassesTab()
else if (tabName === 'students') renderStudentsTab()
else if (tabName === 'records') renderRecordsTab()
else if (tabName === 'today') renderTodayTab()
- Save src/admin.js and refresh your browser at http://localhost:5173.
- Log in as the admin. You should see a fourth Today button in the navigation bar alongside Classes, Students, and Records.
Don't see the Today button?
Make sure the new <button> is inside the <nav class="tabs"> element, before the closing </nav> tag. Also check you saved the file.
help me debug why my Today tab button is not showing up in the admin dashboard
Build the renderTodayTab function
Clicking the Today button calls renderTodayTab(), which does not exist yet. This function needs to fetch every class, check which ones have attendance records for today, and render a card for each.
- At the bottom of src/admin.js, add the first half of the renderTodayTab function by pasting this code:
async function renderTodayTab() {
const tabContent = document.getElementById('tab-content')
// Get today's date in YYYY-MM-DD format
const today = new Date().toISOString().split('T')[0]
// Fetch all classes with their assigned teacher names
const { data: classes } = await supabase
.from('classes')
.select('id, name, profiles(full_name)')
// Fetch attendance records submitted today
const { data: todayRecords } = await supabase
.from('attendance')
.select('class_id')
.eq('date', today)
// Collect the IDs of classes that already submitted
const submittedClassIds = new Set(
(todayRecords || []).map(r => r.class_id)
)
What does this code do?
- today captures the current date as a string like 2026-08-22 so you can filter attendance records to just today.
- The first query fetches all classes and joins the teacher's name from the profiles table via the foreign key relationship.
- The second query grabs just the class_id column from attendance rows matching today's date.
- submittedClassIds is a Set of class IDs that have attendance today. Using a Set makes the lookup fast when building the cards.
- Still inside the same renderTodayTab function, add the card-building and rendering code directly below the submittedClassIds Set:
// Build a status card for each class
const cards = (classes || []).map(c => {
const submitted = submittedClassIds.has(c.id)
return `
<div class="status-card ${submitted ? 'submitted' : 'waiting'}" data-class-id="${c.id}">
<strong>${c.name}</strong>
<span class="teacher-name">${c.profiles?.full_name || 'Unassigned'}</span>
<span class="status-badge">${submitted ? 'Submitted' : 'Waiting'}</span>
</div>
`
}).join('')
// Render the status board into the tab content area
tabContent.innerHTML = `
<h3>Today's Attendance \u2014 ${today}</h3>
<div id="status-board" class="status-grid">${cards || '<p>No classes created yet.</p>'}</div>
`
}
What does this code do?
- cards maps over every class and builds an HTML card. Each card gets a CSS class of either submitted or waiting based on whether its ID is in the submittedClassIds Set.
- Each card also stores a data-class-id attribute. This becomes important in the next step when Realtime events need to find and update the right card.
- The closing brace } at the bottom ends the renderTodayTab function. Make sure it sits at the same indentation level as async function renderTodayTab().
Before you click the Today tab, what do you think the cards will look like without any CSS styles applied?
- Save src/admin.js and refresh your browser.
- Click the Today tab.
You should see a heading with today's date and one block per class, each showing the class name, teacher name, and either "Submitted" or "Waiting." The layout is plain and unstyled for now. You will fix that next.
Seeing an error when you click Today?
If the console shows renderTodayTab is not defined, make sure the function is defined at the top level of src/admin.js (not nested inside another function).
If the tab shows "No classes created yet," confirm you created classes in Part 1. You can check by clicking the Classes tab.
help me debug why my Today tab is not loading status cards
✔️ Awesome, I've got everything!
Great. Make sure you have saved src/admin.js before moving on to the styles.
ⓧ I'd like to double check the full code
Scroll up through src/admin.js and confirm these changes are in place, in order:
- Inside the container.innerHTML nav, a fourth button: <button class="tab" data-tab="today">Today</button>.
- In the tab click handler, a new condition: else if (tabName === 'today') renderTodayTab().
- At the bottom of the file, the complete renderTodayTab function with the data-fetching chunk (queries + submittedClassIds Set) followed by the card-building chunk (card HTML + tabContent.innerHTML), ending with a closing }.
Add styles for the status board
The cards are rendering, but they look like plain text on a white page. A few CSS rules turn them into a proper status grid with color-coded badges.
- Open src/style.css and add the grid layout and card base styles at the end of the file:
/* Grid layout for status cards */
.status-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 0.75rem;
margin-top: 0.75rem;
}
/* Base card with red left border (waiting state) */
.status-card {
background: white;
border-radius: 8px;
padding: 1rem;
text-align: center;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border-left: 4px solid #ef4444;
}
/* Override border to green for submitted cards */
.status-card.submitted {
border-left-color: #22c55e;
}
What do these styles do?
- .status-grid arranges the cards in a responsive grid that wraps to fit the screen width.
- Every card starts with a red left border to indicate the default "waiting" state.
- Cards with the .submitted class override that border to green.
- Still in src/style.css, add the card text and badge styles directly below the block you just pasted:
/* Card text styles */
.status-card strong {
display: block;
font-size: 0.95rem;
margin-bottom: 0.25rem;
}
.status-card .teacher-name {
display: block;
font-size: 0.75rem;
color: #9ca3af;
margin-bottom: 0.5rem;
}
/* Badge pill shape */
.status-badge {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: bold;
}
.status-card.waiting .status-badge {
background: #fef2f2;
color: #ef4444;
}
.status-card.submitted .status-badge {
background: #f0fdf4;
color: #22c55e;
}
What do these badge styles do?
The .status-badge base class creates a small rounded pill shape. Then the .waiting and .submitted variants set the badge colors. Red background with red text for waiting. Green background with green text for submitted.
Think about which of your classes have attendance submitted for today and which do not. What color badges do you expect to see on each card?
- Save src/style.css and refresh your browser.
- Click the Today tab.
You should see one card per class in a neat grid. Classes with attendance submitted today have a green left border and a green "Submitted" badge. Classes without attendance have a red left border and a red "Waiting" badge.
Cards showing but no colors or badges?
Check that you pasted the CSS at the end of src/style.css, not inside an existing rule block. Open DevTools and inspect a status card to verify the .status-card class is present on the element.
If all cards show "Waiting" even though you submitted attendance in Part 1, the date may have changed. The board only checks today's date. Try submitting attendance again as the teacher to see a card flip to green.
help me troubleshoot why my status board cards are not styled correctly
✔️ Awesome, I've got everything!
Great. Double check you have saved both src/admin.js and src/style.css.
ⓧ I'd like to double check the full code
Scroll up and confirm these CSS blocks are at the end of your src/style.css, in order:
- .status-grid with the responsive grid layout.
- .status-card base styles with the red left border, followed by .status-card.submitted overriding the border to green.
- .status-card strong and .status-card .teacher-name text styles.
- .status-badge pill shape, then .status-card.waiting .status-badge (red) and .status-card.submitted .status-badge (green).
The status board works, but it has a problem. If a teacher submits attendance while you are looking at the Today tab, nothing changes on screen. You have to manually refresh the page to see the update. In the next step, you will wire up the Realtime subscription you tested in Step 1 so the cards flip from "Waiting" to "Submitted" the instant attendance arrives.
Wire Up Realtime and Toast Notifications
Your Today's Status board gives the admin a snapshot of which classes have submitted attendance today. But the board goes stale the instant it loads.
In this step, you will wire up a Supabase Realtime subscription that pushes attendance inserts straight to the browser. You will also build a toast notification that alerts the admin the moment a new submission arrives.
In this step, get ready to:
- Subscribe to attendance table inserts using Supabase Realtime Postgres Changes.
- Build a toast notification system that slides in when new attendance arrives.
- Test the full real-time flow across two browser tabs.
Add the Realtime subscription to the Today tab
Supabase Realtime Postgres Changes pushes database events to connected browsers the instant they happen. By subscribing to INSERT events on the attendance table inside renderTodayTab, you can flip each status card from "Waiting" to "Submitted" automatically.
- In src/admin.js, scroll to the bottom of the renderTodayTab function.
- Find the closing backtick of the tabContent.innerHTML template string. Add the following code below it, still inside the function body:
// Clean up any previous subscription
if (window._attendanceChannel) {
supabase.removeChannel(window._attendanceChannel)
}
// Subscribe to new attendance inserts
window._attendanceChannel = supabase
.channel('attendance-today')
.on(
'postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'attendance' },
(payload) => {
const classId = payload.new.class_id
const card = tabContent.querySelector(`.status-card[data-class-id="${classId}"]`)
if (card && card.classList.contains('waiting')) {
card.classList.remove('waiting')
card.classList.add('submitted')
card.querySelector('.status-badge').textContent = 'Submitted'
// Find the class name for the toast
const className = card.querySelector('strong').textContent
showToast(`${className} attendance submitted!`)
}
}
)
.subscribe()
What does this code do?
- The cleanup block checks for an existing window._attendanceChannel and removes it before creating a new one. This prevents duplicate subscriptions if the admin clicks the Today tab more than once.
- supabase.channel('attendance-today').on('postgres_changes', ...) creates a channel that listens for INSERT events on the attendance table. Every time a teacher submits attendance, the callback fires.
- Inside the callback, payload.new.class_id identifies which class was submitted. The code finds that card in the DOM, swaps its CSS class from waiting to submitted, and updates the badge text.
- showToast() fires a notification with the class name. You will build this function in the next chunk.
- Save src/admin.js.
- Refresh your browser, log in as the admin, and click the Today tab. Open the browser console (F12 or Ctrl+Shift+I). You should see no errors in the console.
Seeing a console error?
- Make sure the new code is inside the renderTodayTab function body, after the tabContent.innerHTML assignment and before the function's closing }.
- Check for mismatched backticks or brackets. The template literal inside querySelector uses backticks, not single quotes.
Help me debug my Realtime subscription code in admin.js
✔️ Awesome, I've got everything!
Great. Double check you have saved src/admin.js.
ⓧ I'd like to double check the full code
Since this file is long, scroll through src/admin.js and confirm these sections are present, in order:
- The import { supabase } from './supabase.js' line at the top.
- The renderAdminDashboard function with four tab buttons (Classes, Students, Records, Today) and their click handlers.
- The renderClassesTab, renderStudentsTab, renderRecordsTab, and loadRecordsRange functions from Part 1.
- The renderTodayTab function with class/attendance fetching, card rendering, tabContent.innerHTML assignment, AND the new Realtime subscription block (cleanup + supabase.channel('attendance-today')) at the bottom.
Build the toast notification system
The subscription calls showToast() when new attendance arrives, but that function does not exist yet. You need a lightweight toast system that creates a notification element, slides it onto the screen, and removes it after a few seconds.
- Switch to src/main.js in your editor.
- Find the line const signOutBtn = document.getElementById('sign-out-btn').
- Add the toast container and showToast function below that line by copying this code:
// Create toast container
const toastContainer = document.createElement('div')
toastContainer.id = 'toast-container'
document.body.appendChild(toastContainer)
// Global toast function
window.showToast = function showToast(message) {
const toast = document.createElement('div')
toast.className = 'toast'
toast.textContent = message
toastContainer.appendChild(toast)
// Trigger slide-in animation
requestAnimationFrame(() => toast.classList.add('visible'))
// Auto-dismiss after 4 seconds
setTimeout(() => {
toast.classList.remove('visible')
toast.addEventListener('transitionend', () => toast.remove())
}, 4000)
}
What does this code do?
- The first four lines create a <div id="toast-container"> and append it to the page. This container will hold all active toast notifications.
- window.showToast is attached to the global window object so that admin.js can call it without needing a circular import back to main.js.
- requestAnimationFrame waits one paint frame before adding the visible class. This lets the browser register the initial off-screen position before triggering the CSS slide-in transition.
- After 4 seconds, the visible class is removed to slide the toast back off-screen. Once the CSS transition finishes, transitionend fires and removes the element from the DOM entirely.
- Save src/main.js.
- Refresh your browser. Open DevTools (F12) and click the Elements tab. At the bottom of the <body> tag, you should see a <div id="toast-container"></div> element.
Don't see the toast container in the DOM?
- Make sure you saved src/main.js and refreshed the browser. Unsaved changes will not take effect.
- Confirm the toast code is placed after the signOutBtn line and before the getUserRole function, at the top level of the file (not inside another function).
Help me debug why my toast container is not appearing in the DOM
✔️ Awesome, I've got everything!
Great. Double check you have saved src/main.js.
ⓧ I'd like to double check the full code
Scroll through src/main.js and confirm these sections are present, in order:
- The four imports at the top (supabase, renderLogin, renderAdminDashboard, renderTeacherDashboard).
- The mainContent and signOutBtn DOM references.
- The new toast container creation and window.showToast function you just added.
- The getUserRole, handleAuthState, sign-out handler, and onAuthStateChange listener from Part 1.
The toast logic is ready, but without styles the notifications will not look right. Time to add the CSS.
- Switch to src/style.css in your editor. Add the following styles at the end of the file, below the existing status card styles:
#toast-container {
position: fixed;
top: 1rem;
right: 1rem;
z-index: 1000;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.toast {
background: #4f46e5;
color: white;
padding: 0.75rem 1.25rem;
border-radius: 8px;
font-size: 0.85rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transform: translateX(120%);
transition: transform 0.3s ease;
max-width: 300px;
}
.toast.visible {
transform: translateX(0);
}
What do these styles do?
- #toast-container is pinned to the top-right corner of the screen with position: fixed. Multiple toasts stack vertically thanks to flex-direction: column.
- Each .toast starts off-screen to the right (translateX(120%)). When the visible class is added, it smoothly slides to its natural position (translateX(0)).
- Save src/style.css.
✔️ Awesome, I've got everything!
Great. Double check you have saved src/style.css.
ⓧ I'd like to double check the full code
Scroll through src/style.css and confirm these sections are present at the end of the file, in order:
- All Part 1 styles (unchanged).
- The .status-grid and .status-card styles from Step 2.
- The new #toast-container, .toast, and .toast.visible rules you just added.
Test the full real-time flow
All three files are updated. Time to prove the subscription, DOM update, and toast notification all work together end to end.
- Open two browser tabs side by side.
- In Tab 1, log in as the admin and click the Today tab. You should see your class cards.
- In Tab 2, log in as the teacher.
Already submitted attendance today?
If the teacher already submitted attendance for today, you need to clear those records first so you can submit again. In the Supabase dashboard, open the Table Editor, select the attendance table, and delete the rows with today's date.
Then refresh the teacher's browser tab and the admin's Today tab so both start fresh.
Before you continue, think about what you expect to happen on the admin's Today tab when the teacher submits attendance. Will the card update? Will a notification appear?
- In Tab 2 (the teacher), submit attendance for a class.
- Switch to Tab 1 (the admin) and watch the Today tab.
The teacher's class card should flip from the red "Waiting" badge to the green "Submitted" badge instantly. A purple toast notification should slide in from the right showing the class name followed by "attendance submitted!" The toast auto-dismisses after 4 seconds.
Card not updating or toast not appearing?
- Open the admin tab's browser console. If you see an error about showToast not being defined, make sure you saved src/main.js and refreshed the admin tab.
- If the card does not update at all, confirm that the attendance table is still in the supabase_realtime publication. You enabled this in Step 1.
- If the toast appears but does not slide in, check that the toast CSS is saved in src/style.css. Without the transition styles, the toast element will appear but stay invisible (positioned off-screen).
Help me debug why my real-time updates are not working
Real-time updates are live. The admin can now watch attendance roll in without lifting a finger. Next up, you will clean up Realtime subscriptions when they are no longer needed and redeploy to Cloudflare Pages so the live PWA gets these powers too.
Clean Up Subscriptions and Redeploy
Your real-time dashboard works. Teachers submit attendance, and the admin's Today tab updates instantly with a toast notification sliding in.
There is one loose end to tie up before going live. Supabase Realtime subscriptions consume server connections. If the admin clicks away from the Today tab without cleaning up, the subscription keeps running in the background and wastes a connection. In this step, you will add cleanup code to both tab switches and sign-out, then rebuild and deploy the updated app to Cloudflare Pages.
In this step, get ready to:
- Clean up the Realtime subscription when the admin switches tabs.
- Clean up the Realtime subscription when the admin signs out.
- Rebuild and redeploy to Cloudflare Pages.
Add subscription cleanup on tab switch
Right now, clicking the Today tab creates a Realtime channel. But when the admin clicks a different tab, that channel stays alive because nothing removes it. The fix is to call supabase.removeChannel() at the very top of the tab click handler, before any new tab loads.
- In src/admin.js, find the tab click handler inside the renderAdminDashboard function.
- Look for this line inside the click callback:
tabs.forEach(t => t.classList.remove('active'))
- Add the following cleanup code directly above that line, so it runs before any tab switch:
// Clean up Realtime subscription when leaving Today tab
if (window._attendanceChannel) {
supabase.removeChannel(window._attendanceChannel)
window._attendanceChannel = null
}
- The top of your click handler should now look like this:
tab.addEventListener('click', () => {
// Clean up Realtime subscription when leaving Today tab
if (window._attendanceChannel) {
supabase.removeChannel(window._attendanceChannel)
window._attendanceChannel = null
}
tabs.forEach(t => t.classList.remove('active'))
tab.classList.add('active')
Why set it to null?
Setting window._attendanceChannel to null after removing the channel prevents a stale reference. Without it, the next tab click would try to remove an already-removed channel.
Add cleanup on sign out
The same leak happens if the admin signs out while the Today tab is open. The channel stays alive even after the session ends.
- In src/main.js, find the sign-out click handler.
- Find this code:
signOutBtn.addEventListener('click', async () => {
await supabase.auth.signOut({ scope: 'local' })
})
- Add the channel cleanup before the signOut call, so the handler now looks like this:
signOutBtn.addEventListener('click', async () => {
if (window._attendanceChannel) {
supabase.removeChannel(window._attendanceChannel)
window._attendanceChannel = null
}
await supabase.auth.signOut({ scope: 'local' })
})
- Save both src/admin.js and src/main.js.
In the browser, log in as the admin and click the Today tab. Switch to the Classes tab, then switch back to Today. The status cards should still load correctly and real-time updates should still work.
Today tab stops updating after switching away?
- Make sure the cleanup code is above the tabs.forEach(t => t.classList.remove('active')) line, not below it. The channel must be removed before the new tab renders.
- Confirm you saved both src/admin.js and src/main.js. Unsaved changes will not take effect.
- Why is my Today tab not reconnecting to Realtime?
✔️ Awesome, I've got everything!
Great. Double check you have saved both files before moving on to the deploy.
ⓧ I'd like to double check the full code
Both files are longer than a quick glance, so scroll up and confirm these two edits are in place:
- In src/admin.js, the tab click handler callback starts with the 4-line if (window._attendanceChannel) block, directly above tabs.forEach(t => t.classList.remove('active')).
- In src/main.js, the sign-out handler has the same 4-line if (window._attendanceChannel) block directly above await supabase.auth.signOut({ scope: 'local' }).
Build and redeploy to Cloudflare Pages
The cleanup code works locally. Time to push the full Part 2 update to your live Cloudflare Pages site so the real-time features work for anyone who opens the URL.
- Stop the dev server by pressing Ctrl+C in your terminal.
- Build the production files by running:
npm run build
You should see output confirming the build completed, with the optimized files written to the dist folder.
Build failed?
- Check the error message for a file name and line number. A missing closing brace or bracket from the cleanup edits is the most common cause.
- My npm run build is failing after adding the cleanup code. Help me find the syntax error.
- Open https://dash.cloudflare.com in your browser.
- Click Workers and Pages in the left sidebar.
- Select your school-attendance Pages project.
- Upload the updated dist folder using direct upload. If you set up Git-based deployment in Part 1, push your changes via Git instead.
After deployment completes, it is time to test. Before you open the live URL, predict: do you think the real-time card flip and toast will work the same on the deployed site as they did locally?
- Open your live Cloudflare Pages URL.
- Log in as the admin and click the Today tab. The status board should load with your class cards.
- Open the live URL on a second device or in an incognito window.
- Log in as the teacher and submit attendance.
- Switch back to the admin's browser tab.
The admin's status card should flip from red "Waiting" to green "Submitted" instantly. A purple toast notification should slide in from the right showing the class name. This is the same behavior you saw locally, now running on your deployed site.
Real-time updates not working on the deployed site?
- Make sure you uploaded the latest dist folder from after running npm run build. An older build will not include the Realtime or toast code.
- Open the browser's developer console on the deployed site and check for errors. A misconfigured Supabase URL or anon key in src/supabase.js will prevent the Realtime connection.
- Confirm the attendance table is still in the supabase_realtime publication. In the Supabase dashboard, go to Database > Publications and verify.
- My deployed attendance PWA is not receiving real-time updates. Help me debug the Realtime connection on Cloudflare Pages.
Secret mission
Show Which Teachers Are Online with Presence
Your live dashboard shows submitted vs. waiting, but you cannot tell if a teacher is even online right now. Add Supabase Presence tracking so the admin sees a green dot next to any teacher currently connected to the app.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on free tiers, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
What are the Supabase Realtime free tier limits?
The Supabase Free plan includes generous Realtime limits:
- 200 concurrent connections.
- 2 million messages per month.
- 100 messages per second.
For a weekend school with under 50 users, you will stay well within these limits.
Resources you used:
- Supabase project (database, authentication, Realtime, API) on the Free tier.
- Cloudflare Pages site (static hosting) on the Free tier.
- Local school-attendance project folder on your Desktop.
Keep everything running
No action needed. Choose this if you are still actively building or plan to use the app regularly.
- Both Supabase and Cloudflare Pages are running at $0/month.
- The Supabase project stays active as long as it receives a request at least once every 7 days. Regular weekend use keeps it alive.
- The Cloudflare Pages site remains deployed and accessible at no cost.
Pause - I'll come back to this later
Shut down running processes to free up local resources. Your cloud services remain active at no cost.
- Stop the Vite dev server by pressing Ctrl+C in the terminal.
- If unused for over a week, Supabase auto-pauses the project. Restore it from the dashboard when you are ready.
- Your Cloudflare Pages site stays live and costs nothing while paused.
Delete - I don't want to use this again
Remove all project resources and start fresh. These are the same cleanup steps from Part 1.
Delete your Supabase project:
- Open the Supabase dashboard and select your school-attendance project.
- Click Settings in the left sidebar.
- Click General.
- Scroll to the bottom and click Delete project. Enter the project name to confirm.
Delete your Cloudflare Pages site:
- Open https://dash.cloudflare.com.
- Go to Workers & Pages and select your school-attendance project.
- Click Settings.
- Click Delete project.
Delete the local project folder:
macOS
- Run this command in your terminal:
rm -rf ~/Desktop/school-attendance
Windows
- Run this command in your terminal:
rmdir /s /q %USERPROFILE%\Desktop\school-attendance
Nice Work!
Nice Work!
Nice work! You just upgraded your school attendance PWA with real-time capabilities powered by Supabase Realtime.
You learned how to:
- Enabled Supabase Realtime Postgres Changes by adding the attendance table to the supabase_realtime publication and subscribing to INSERT events from the JavaScript client.
- Built a live Today's Status dashboard that shows every class with a submitted or waiting badge, updating automatically without any page refresh.
- Created a toast notification system that alerts the admin the instant a teacher submits attendance, and managed Realtime channel cleanup on tab switches and sign-outs to avoid wasting server connections.
- Secret Mission: Added Supabase Presence to show which teachers are currently online, combining two Realtime features (Postgres Changes and Presence) on the same page.
Ready to quiz yourself?