Geo-Verified QR Attendance System
All Roles & Technical Reference
Version 2.0 · May 2026
GeoMark is a web-based attendance management platform that combines cryptographically-signed rotating QR codes with GPS geofencing to guarantee that students mark attendance only when they are physically present inside the classroom. No dedicated mobile app is required — everything runs in a modern browser.
Core security model
Navigate to the app URL and click Sign In. You can sign in with Google or use a local account. When registering as a student, your Matric Number is your login identifier — no email address is needed. Enter it in the format BU24CSC1021 (2 letters + 2-digit year + 3-letter department code + 4 digits).
Signing in without email
Tip — scanning speed
Click Check In → Enter Code on the dashboard, or tap the Code tab on the Attendance page. Your lecturer will display or read out a 6-character code (e.g. XKPR7W). Type it in and tap Submit Code.
Click Check In → Bluetooth on the dashboard, or tap the Bluetooth tab on the Attendance page. Tap Scan for Beacon and select the GeoMark device from the browser prompt. The app reads the session token from the beacon and submits your attendance automatically.
How it works
Go to History in the sidebar. The page shows a card per course you have attended, each with:
How the rate is calculated
The bell icon in the top bar shows your personal notifications. Each successful check-in, late mark, or status change generates a notification visible only to you. Clicking Mark all read clears the unread badge.
Open Settings from the sidebar. You can:
You can also click your name in the topbar and select Change Icon to pick an emoji avatar, which appears in place of a photo.
CS101).After creation, a large QR code appears on screen. Project it on the classroom screen or share your screen via video conferencing. The code refreshes automatically every 15–30 seconds (depending on server configuration) — students must scan the current code. Old codes are cryptographically invalidated when they expire.
Display tip
Below the QR code, a 6-character attendance code(e.g. XKPR7W) is displayed in large monospace text. Read this code aloud or write it on the board for students who cannot scan the QR. The verbal code remains valid for the full duration of the session (it does not rotate like the QR token).
Code character set
0 O 1 I Lare excluded) to prevent reading errors.The session panel shows a live list of students who have checked in, including their name, email, status, timestamp, and distance from the classroom. The list refreshes every 10 seconds automatically.
Each row in the attendee list has a status dropdown. You can change a student's status to Present, Late, Absent, or Flagged. Every change is:
Click End Session. The QR code and verbal code become immediately invalid — any subsequent scan or code submission will be rejected with "Session not found or already ended." A notification is sent to you summarising the total number of students who attended.
The late threshold is configured via two datetime pickers in the session creation form: Session Start and Late after. The difference between them is the grace period. The form displays the computed gap as a live preview (e.g. "Students arriving after 15 min are marked Late") so you can see the exact value before starting the session.
| Configuration | Behaviour |
|---|---|
| Late time = Start time | All arrivals are marked Late immediately (0-minute grace period) |
| Late time 15 min after start | Students who check in within 15 minutes are Present; after that, Late |
| Late time not set | Late threshold disabled — all students marked Present regardless of when they arrive |
Tip — large classes
By default, a session is open to students from all departments. To restrict attendance to specific cohorts, tick one or more departments in the Departments checkbox list in the session creation form.
Department codes
CSC from BU24CSC1021). Ensure students register with the correct matric format so their department is parsed correctly.Navigate to Users in the sidebar. Admins can:
The Security page shows all high-severity events: invalid QR tokens, location anomalies, and manual attendance overrides. Each entry records the user, event type, IP address, user-agent, and full details.
The Analytics page aggregates attendance across all courses and sessions. Use the filters at the top of the page to narrow the data:
Click Export CSV to download the filtered data as a spreadsheet. The export respects the currently selected period and course filters. All fields are sanitised to prevent spreadsheet formula injection (cells beginning with =, +, or @ are prefixed with a tab before export).
CSV columns
Each QR code embeds a signed token (not a plain session ID). The token is structured as:
<base64url(JSON payload)>.<HMAC-SHA256 signature> Payload fields: sessionId — UUID for the QR session courseId — course identifier courseName — human-readable course name lat / lon — classroom GPS coordinates radius — allowed geofence radius in metres iat — issued-at timestamp (ms) exp — expiry timestamp (ms)
The HMAC is computed with the server-side QR_SECRETenvironment variable using timing-safe comparison (crypto.timingSafeEqual) to prevent timing-oracle attacks. Tokens are validated for signature and expiry on every scan — an expired token is rejected even if the signature is valid.
Secret rotation
QR_SECRET immediately invalidates all outstanding QR codes. Plan rotation during off-hours or between sessions.Distance is computed with the Haversine formula:
const R = 6371000; // Earth radius in metres const dLat = toRad(lat2 - lat1); const dLon = toRad(lon2 - lon1); const a = sin²(dLat/2) + cos(lat1)·cos(lat2)·sin²(dLon/2); const distance = 2·R·atan2(√a, √(1−a));
Accuracy depends on the device's GPS hardware. Indoor GPS can be off by 10–50 m on some devices. Set radiusMeters generously (100–150 m) for multi-storey buildings or venues with poor GPS reception.
| Scenario | Recommended radius |
|---|---|
| Small seminar room | 30–50 m |
| Standard lecture hall | 80–120 m |
| Large auditorium or campus venue | 150–200 m |
| Multi-building campus area | 300–500 m |
All submission endpoints are protected by an in-memory sliding-window rate limiter:
| Endpoint | Limit | Window |
|---|---|---|
| POST /api/qr/validate | 30 attempts | per user per minute |
| POST /api/code/validate | 10 attempts | per user per minute |
| POST /api/qr/generate | 20 sessions | per instructor per hour |
| POST /api/auth/register | 5 registrations | per IP per 15 minutes |
Every manual attendance status change writes aSecurityLog document:
{
userId: "<modifier's user ID>",
eventType: "attendance_status_modified",
severity: "medium",
details: {
attendanceId: "<record ID>",
studentId: "<student's user ID>",
studentEmail: "<student email>",
courseId: "<course ID>",
previousStatus: "present" | "late" | "absent" | "flagged",
newStatus: "present" | "late" | "absent" | "flagged",
modifiedBy: "<modifier's email>",
},
ipAddress: "<requester IP>",
userAgent: "<browser string>",
createdAt: <timestamp>
}Logs are never deleted automatically — retain them according to your institution's data governance policy.
All responses include these HTTP security headers:
| Header | Value |
|---|---|
| X-Frame-Options | DENY |
| X-Content-Type-Options | nosniff |
| Strict-Transport-Security | max-age=63072000; includeSubDomains; preload |
| Permissions-Policy | camera=self, geolocation=self, microphone=() |
| Referrer-Policy | strict-origin-when-cross-origin |
| Content-Security-Policy | default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src 'self' wss:; img-src 'self' data: blob: https://lh3.googleusercontent.com |
The following environment variables must be set before deploying. Missing required variables cause the server to refuse to start.
| Variable | Required | Description |
|---|---|---|
| MONGODB_URI | Yes | MongoDB connection string |
| NEXTAUTH_SECRET | Yes | Long random string for session encryption |
| NEXTAUTH_URL | Yes | Public URL of the deployment (e.g. https://geomark.example.com) |
| QR_SECRET | Yes | Long random string for HMAC-signing QR tokens. Never expose publicly. |
| LECTURER_CODE | Yes | Registration code that grants instructor role |
| ADMIN_CODE | Yes | Registration code that grants admin role |
| FOUNDER_PASSWORD | Yes (seed only) | Password used by the admin seed route |
| QR_TTL_SECONDS | No (default: 30) | QR token lifetime in seconds — clamped to 15–300 |
| GOOGLE_CLIENT_ID | No | Google OAuth client ID (needed for Google sign-in) |
| GOOGLE_CLIENT_SECRET | No | Google OAuth client secret |
Problem: Camera does not start / "Could not start scanner"
Ensure the site is served over HTTPS (camera access is blocked on HTTP). Check that you granted camera permission in your browser. On iOS, use Safari — other browsers on iOS share Safari's camera engine and may behave inconsistently.
Problem: QR code scans slowly or not at all
Ensure the QR code is well-lit and fills the square viewport. Chrome and Edge on Android use hardware-accelerated decoding — use these for the best experience. On slower devices, the pure-JS fallback (jsQR) may take 1–2 seconds per frame.
Problem: "You are Xm away from the classroom"
Move closer to the lecturer's position. If you are physically inside the room but still rejected, the instructor may need to increase the radius setting. Indoor GPS drift of 20–50 m is normal on some devices.
Problem: "Code not found or session has ended"
The instructor may have ended the session, or the QR token has expired before you submitted. Ask the instructor to confirm the session is still active. If using the verbal code, check you typed it correctly (no 0, O, 1, I, or L in valid codes).
Problem: "Attendance already recorded"
You have already checked in to this session. If you believe the recorded status is wrong, contact your instructor — they can change it manually from the session panel.
Problem: "Too many attempts" (rate limit)
You have exceeded the scan/code submission limit. Wait 60 seconds and try again. If this recurs, check that the QR scanning loop is not submitting duplicate requests.
Problem: Notification bell is empty for a new account
This is correct. Notifications are generated by system events (check-ins, session starts, status changes). No pre-existing notifications are shown for newly registered users.
Problem: Attendance rate shows 0% despite attending sessions
The rate is relative to all sessions held for a course. If sessions were created under a different course ID, your check-ins will not be counted. Confirm with the instructor that the same Course ID is used consistently.
Problem: Bluetooth — "No beacon found" or scan completes with no result
Ensure you are within approximately 10 m of the lecturer's device. BLE range drops significantly through thick walls or in crowded rooms. Also confirm the lecturer has an active session running — the beacon is only discoverable while a session is live. Try moving closer and tapping Scan for Beacon again.
Problem: Bluetooth — browser shows no device picker / permission denied
Bluetooth access requires the user to interact with a browser permission prompt. If you dismissed it or previously denied access, open your browser's site settings and re-enable Bluetooth. On mobile devices, ensure both the device Bluetooth toggle and the browser permission are enabled in system settings.
Problem: Bluetooth check-in accepted but attendance still rejected
Bluetooth check-in still requires a valid GPS geofence. Even if the beacon is detected, you must be within the classroom radius set by the lecturer. If you are inside the room but still rejected, ask the lecturer to check the radius setting (see §3.1).