Learn how to easily add a contactless check-in system to your web app for seamless, safe, and efficient user access.

Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
The Rise of Contactless: More Than Just a Pandemic Response
Contactless check-in has evolved from a pandemic necessity to a customer expectation. Whether you're running a hotel, event venue, medical practice, or co-working space, implementing this feature delivers tangible benefits beyond health safety: reduced wait times, streamlined operations, and data collection that can power personalization.
1. Pre-arrival Communication
2. Digital Check-In Interface
3. Backend Processing
4. Staff Notification System
5. Guest Communication
Frontend Technologies
A responsive, intuitive interface is critical for successful adoption. Here's what works well:
// React component for QR code scanner implementation
import React, { useState } from 'react';
import { QrReader } from 'react-qr-reader';
const QRCheckIn = () => {
const [scanResult, setScanResult] = useState('');
const [checkInStatus, setCheckInStatus] = useState('waiting');
const handleScan = async (result) => {
if (result) {
setScanResult(result);
// Process the scanned QR code
try {
const response = await fetch('/api/check-in', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ checkInCode: result }),
});
const data = await response.json();
if (data.success) {
setCheckInStatus('success');
} else {
setCheckInStatus('error');
}
} catch (error) {
console.error('Check-in failed:', error);
setCheckInStatus('error');
}
}
};
return (
<div className="qr-scanner-container">
<h2>Scan Your Check-In Code</h2>
{checkInStatus === 'waiting' && (
<QrReader
constraints={{ facingMode: 'environment' }}
onResult={handleScan}
style={{ width: '100%', maxWidth: '500px' }}
/>
)}
{checkInStatus === 'success' && (
<div className="success-message">
<h3>Welcome! You're checked in.</h3>
<p>A staff member will be with you shortly.</p>
</div>
)}
{checkInStatus === 'error' && (
<div className="error-message">
<h3>Something went wrong</h3>
<p>Please try again or ask for assistance.</p>
<button onClick={() => setCheckInStatus('waiting')}>
Try Again
</button>
</div>
)}
</div>
);
};
export default QRCheckIn;
Backend Implementation
Your server needs to handle user authentication, data storage, and notifications:
// Node.js Express backend for handling check-ins
const express = require('express');
const router = express.Router();
const { v4: uuidv4 } = require('uuid');
const nodemailer = require('nodemailer');
const twilio = require('twilio');
// Database models (using Mongoose for MongoDB example)
const Reservation = require('../models/Reservation');
const CheckIn = require('../models/CheckIn');
// Create a check-in link for a reservation
router.post('/generate-checkin', async (req, res) => {
try {
const { reservationId, email, phone } = req.body;
// Generate unique check-in code
const checkInCode = uuidv4();
// Store the check-in code in the database
await Reservation.findByIdAndUpdate(reservationId, {
checkInCode,
checkInStatus: 'pending'
});
// Generate QR code data (typically a URL to your check-in page with the code)
const checkInUrl = `${process.env.APP_URL}/check-in/${checkInCode}`;
// Send email with check-in instructions
if (email) {
await sendCheckInEmail(email, checkInUrl);
}
// Send SMS if phone number is provided
if (phone) {
await sendCheckInSMS(phone, checkInUrl);
}
res.json({
success: true,
message: 'Check-in link generated successfully',
checkInUrl
});
} catch (error) {
console.error('Error generating check-in:', error);
res.status(500).json({
success: false,
message: 'Failed to generate check-in link'
});
}
});
// Process check-in when user scans QR code
router.post('/check-in', async (req, res) => {
try {
const { checkInCode } = req.body;
// Find the reservation with this check-in code
const reservation = await Reservation.findOne({ checkInCode });
if (!reservation) {
return res.status(404).json({
success: false,
message: 'Invalid check-in code'
});
}
// Record the check-in
const checkIn = new CheckIn({
reservationId: reservation._id,
timestamp: new Date(),
status: 'completed'
});
await checkIn.save();
// Update reservation status
reservation.checkInStatus = 'completed';
reservation.checkInTime = new Date();
await reservation.save();
// Notify staff about the check-in
await notifyStaff(reservation);
res.json({
success: true,
message: 'Check-in successful',
reservation: {
id: reservation._id,
name: reservation.guestName,
// Include other relevant details...
}
});
} catch (error) {
console.error('Check-in error:', error);
res.status(500).json({
success: false,
message: 'Check-in process failed'
});
}
});
// Helper function to send email
async function sendCheckInEmail(email, checkInUrl) {
// Email sending logic using nodemailer
// ...
}
// Helper function to send SMS
async function sendCheckInSMS(phone, checkInUrl) {
// SMS sending logic using Twilio
// ...
}
// Helper function to notify staff
async function notifyStaff(reservation) {
// Staff notification logic - could be WebSockets, push notifications, etc.
// ...
}
module.exports = router;
Database Schema
Designing the right data structure is crucial for a smooth check-in flow:
// MongoDB schema using Mongoose
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
// Reservation Schema
const ReservationSchema = new Schema({
guestName: {
type: String,
required: true
},
email: {
type: String,
required: true
},
phone: {
type: String
},
reservationDate: {
type: Date,
required: true
},
checkInCode: {
type: String
},
checkInStatus: {
type: String,
enum: ['pending', 'completed', 'cancelled'],
default: 'pending'
},
checkInTime: {
type: Date
},
// Other reservation details...
createdAt: {
type: Date,
default: Date.now
}
});
// Check-in Schema
const CheckInSchema = new Schema({
reservationId: {
type: Schema.Types.ObjectId,
ref: 'Reservation',
required: true
},
timestamp: {
type: Date,
default: Date.now
},
status: {
type: String,
enum: ['completed', 'failed', 'cancelled'],
required: true
},
location: {
type: String // For multi-location businesses
},
checkInMethod: {
type: String,
enum: ['qr', 'link', 'manual', 'kiosk'],
default: 'qr'
},
// Optional additional data collected during check-in
additionalData: {
type: Schema.Types.Mixed
},
// Useful for audit trails
completedBy: {
type: String // User ID or 'self' if customer completed it
}
});
const Reservation = mongoose.model('Reservation', ReservationSchema);
const CheckIn = mongoose.model('CheckIn', CheckInSchema);
module.exports = { Reservation, CheckIn };
For staff to immediately know when guests check in, implement a WebSocket solution:
// Server-side WebSocket implementation with Socket.io
const http = require('http');
const socketIo = require('socket.io');
const express = require('express');
const app = express();
const server = http.createServer(app);
const io = socketIo(server);
// Store staff connections
const staffSockets = {};
io.on('connection', (socket) => {
// Handle staff login
socket.on('staff-login', (staffId) => {
staffSockets[staffId] = socket.id;
console.log(`Staff ${staffId} connected`);
});
// Handle staff disconnect
socket.on('disconnect', () => {
// Remove staff from active connections
for (const [staffId, socketId] of Object.entries(staffSockets)) {
if (socketId === socket.id) {
delete staffSockets[staffId];
console.log(`Staff ${staffId} disconnected`);
break;
}
}
});
});
// Function to notify staff of new check-ins
function notifyStaffOfCheckIn(checkInData) {
// Determine which staff should be notified based on roles, location, etc.
const staffToNotify = determineStaffForNotification(checkInData);
staffToNotify.forEach(staffId => {
const socketId = staffSockets[staffId];
if (socketId) {
io.to(socketId).emit('new-check-in', checkInData);
}
});
// Also broadcast to all staff with 'check-in-manager' role
io.to('check-in-manager').emit('new-check-in', checkInData);
}
// Helper function to determine which staff should be notified
function determineStaffForNotification(checkInData) {
// This would contain your business logic for routing notifications
// ...
return ['staff1', 'staff2']; // Example return
}
// This would be called from your check-in API endpoint
module.exports = { notifyStaffOfCheckIn };
QR codes provide a seamless way for guests to access their check-in:
// Generate QR codes for check-in links
const QRCode = require('qrcode');
const fs = require('fs');
const path = require('path');
// Function to generate a QR code for a reservation
async function generateCheckInQR(reservation) {
const checkInUrl = `${process.env.APP_URL}/check-in/${reservation.checkInCode}`;
// Options for QR code generation
const options = {
errorCorrectionLevel: 'H', // High - allows for 30% damage without loss of data
type: 'image/png',
quality: 0.92,
margin: 1,
color: {
dark: '#000000',
light: '#ffffff'
}
};
// Generate QR code
try {
// Option 1: Return QR as data URL for embedding in emails
const qrDataUrl = await QRCode.toDataURL(checkInUrl, options);
// Option 2: Save to file (useful for batch processing)
const qrFilename = `check-in-${reservation._id}.png`;
const qrFilePath = path.join(__dirname, '../public/qrcodes', qrFilename);
await QRCode.toFile(qrFilePath, checkInUrl, options);
return {
dataUrl: qrDataUrl,
filePath: `/qrcodes/${qrFilename}`, // Path relative to your public directory
checkInUrl
};
} catch (error) {
console.error('QR code generation failed:', error);
throw error;
}
}
module.exports = { generateCheckInQR };
Connecting to Your Property Management System (PMS) or CRM
// Example integration with a hotel PMS system
const axios = require('axios');
class PMSIntegration {
constructor(apiKey, apiUrl) {
this.apiKey = apiKey;
this.apiUrl = apiUrl;
this.axiosInstance = axios.create({
baseURL: apiUrl,
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
}
});
}
// Fetch reservation details from PMS
async getReservation(reservationId) {
try {
const response = await this.axiosInstance.get(`/reservations/${reservationId}`);
return response.data;
} catch (error) {
console.error('Failed to fetch reservation from PMS:', error);
throw error;
}
}
// Update reservation status in PMS after check-in
async updateCheckInStatus(reservationId, status, timestamp) {
try {
const response = await this.axiosInstance.post(`/reservations/${reservationId}/status`, {
status,
timestamp,
source: 'web_check_in'
});
return response.data;
} catch (error) {
console.error('Failed to update reservation status in PMS:', error);
throw error;
}
}
// Sync room assignment from PMS
async getRoomAssignment(reservationId) {
try {
const response = await this.axiosInstance.get(`/reservations/${reservationId}/room`);
return response.data;
} catch (error) {
console.error('Failed to get room assignment from PMS:', error);
throw error;
}
}
// Generate room key (for hotels with digital locks)
async generateRoomKey(reservationId, guestInfo) {
try {
const response = await this.axiosInstance.post(`/rooms/keys`, {
reservationId,
guestInfo,
keyType: 'digital',
validFrom: new Date(),
validUntil: new Date(guestInfo.departureDate)
});
return response.data;
} catch (error) {
console.error('Failed to generate room key:', error);
throw error;
}
}
}
module.exports = PMSIntegration;
Secure Your Check-In Process
// Middleware for validating check-in tokens
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
// Generate a secure check-in token
function generateCheckInToken(reservationData) {
// Create a signed JWT with expiration
const token = jwt.sign(
{
reservationId: reservationData._id,
email: reservationData.email,
exp: Math.floor(Date.now() / 1000) + (24 * 60 * 60) // 24 hours expiry
},
process.env.JWT_SECRET
);
return token;
}
// Validate check-in token middleware
function validateCheckInToken(req, res, next) {
const token = req.body.token || req.query.token || req.headers['x-check-in-token'];
if (!token) {
return res.status(401).json({ success: false, message: 'No check-in token provided' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.reservation = decoded;
next();
} catch (error) {
return res.status(401).json({ success: false, message: 'Invalid or expired check-in token' });
}
}
// Generate a one-time check-in code
function generateOneTimeCode() {
// Create a 6-digit code
return crypto.randomInt(100000, 999999).toString();
}
// Verify one-time code
async function verifyOneTimeCode(code, reservationId) {
const reservation = await Reservation.findById(reservationId);
if (!reservation || reservation.oneTimeCode !== code) {
return false;
}
// Check if code is expired (codes valid for 15 minutes)
const codeGeneratedAt = reservation.oneTimeCodeGeneratedAt;
const fifteenMinutesAgo = new Date(Date.now() - 15 * 60 * 1000);
if (codeGeneratedAt < fifteenMinutesAgo) {
return false;
}
return true;
}
module.exports = {
generateCheckInToken,
validateCheckInToken,
generateOneTimeCode,
verifyOneTimeCode
};
Phase 1: Basic Check-In System
Phase 2: Enhanced Features
Phase 3: Advanced Capabilities
Common Edge Cases to Test
// Example Jest test cases for your check-in system
const request = require('supertest');
const app = require('../app');
const mongoose = require('mongoose');
const { Reservation } = require('../models/Reservation');
describe('Check-In System Tests', () => {
beforeAll(async () => {
// Connect to test database
await mongoose.connect(process.env.TEST_MONGODB_URI);
// Clear test data
await Reservation.deleteMany({});
});
afterAll(async () => {
await mongoose.connection.close();
});
test('Should generate a valid check-in code', async () => {
// Create a test reservation
const reservation = new Reservation({
guestName: 'Test User',
email: '[email protected]',
phone: '+11234567890',
reservationDate: new Date('2023-12-01')
});
await reservation.save();
const response = await request(app)
.post('/api/generate-checkin')
.send({
reservationId: reservation._id,
email: reservation.email,
phone: reservation.phone
});
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.checkInUrl).toBeDefined();
// Verify the reservation was updated
const updatedReservation = await Reservation.findById(reservation._id);
expect(updatedReservation.checkInCode).toBeDefined();
expect(updatedReservation.checkInStatus).toBe('pending');
});
test('Should successfully process a check-in', async () => {
// Create a reservation with check-in code
const checkInCode = 'test-code-123';
const reservation = new Reservation({
guestName: 'Check-in Tester',
email: '[email protected]',
reservationDate: new Date('2023-12-01'),
checkInCode,
checkInStatus: 'pending'
});
await reservation.save();
const response = await request(app)
.post('/api/check-in')
.send({ checkInCode });
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
// Verify check-in status updated
const updatedReservation = await Reservation.findById(reservation._id);
expect(updatedReservation.checkInStatus).toBe('completed');
expect(updatedReservation.checkInTime).toBeDefined();
});
test('Should reject invalid check-in code', async () => {
const response = await request(app)
.post('/api/check-in')
.send({ checkInCode: 'invalid-code' });
expect(response.statusCode).toBe(404);
expect(response.body.success).toBe(false);
});
test('Should handle duplicate check-ins', async () => {
// Create a reservation that's already checked in
const checkInCode = 'already-used-code';
const reservation = new Reservation({
guestName: 'Already Checked In',
email: '[email protected]',
reservationDate: new Date('2023-12-01'),
checkInCode,
checkInStatus: 'completed',
checkInTime: new Date()
});
await reservation.save();
const response = await request(app)
.post('/api/check-in')
.send({ checkInCode });
// System should identify this is already checked in
expect(response.statusCode).toBe(200);
expect(response.body.message).toContain('already checked in');
});
// Additional test cases...
});
Making Check-In Smooth for All Users
// Example analytics implementation
const AnalyticsService = {
// Track check-in completion rate
trackCheckInCompletion: async (reservationId, completed, timeSpent, device) => {
await Analytics.create({
eventType: 'check_in_attempt',
reservationId,
properties: {
completed,
timeSpent, // in seconds
device,
timestamp: new Date()
}
});
},
// Track where users abandon the process
trackCheckInStep: async (reservationId, step, completed, timeSpent) => {
await Analytics.create({
eventType: 'check_in_step',
reservationId,
properties: {
step,
completed,
timeSpent,
timestamp: new Date()
}
});
},
// Generate check-in performance report
generateCheckInReport: async (startDate, endDate) => {
const stats = await Analytics.aggregate([
{
$match: {
eventType: 'check_in_attempt',
'properties.timestamp': {
$gte: new Date(startDate),
$lte: new Date(endDate)
}
}
},
{
$group: {
_id: null,
totalAttempts: { $sum: 1 },
completedAttempts: {
$sum: { $cond: [{ $eq: ['$properties.completed', true] }, 1, 0] }
},
avgTimeSpent: { $avg: '$properties.timeSpent' },
mobileAttempts: {
$sum: { $cond: [{ $in: ['$properties.device', ['mobile', 'tablet']] }, 1, 0] }
}
}
},
{
$project: {
_id: 0,
totalAttempts: 1,
completedAttempts: 1,
completionRate: {
$multiply: [
{ $divide: ['$completedAttempts', '$totalAttempts'] },
100
]
},
avgTimeSpent: 1,
mobilePercentage: {
$multiply: [
{ $divide: ['$mobileAttempts', '$totalAttempts'] },
100
]
}
}
}
]);
return stats[0] || {
totalAttempts: 0,
completedAttempts: 0,
completionRate: 0,
avgTimeSpent: 0,
mobilePercentage: 0
};
},
// Identify problem steps in the process
identifyProblemSteps: async (startDate, endDate) => {
return await Analytics.aggregate([
{
$match: {
eventType: 'check_in_step',
'properties.timestamp': {
$gte: new Date(startDate),
$lte: new Date(endDate)
}
}
},
{
$group: {
_id: '$properties.step',
totalAttempts: { $sum: 1 },
completedAttempts: {
$sum: { $cond: [{ $eq: ['$properties.completed', true] }, 1, 0] }
},
avgTimeSpent: { $avg: '$properties.timeSpent' }
}
},
{
$project: {
step: '$_id',
_id: 0,
totalAttempts: 1,
completedAttempts: 1,
dropoffRate: {
$multiply: [
{ $subtract: [1, { $divide: ['$completedAttempts', '$totalAttempts'] }] },
100
]
},
avgTimeSpent: 1
}
},
{
$sort: { dropoffRate: -1 } // Highest dropoff rate first
}
]);
}
};
module.exports = AnalyticsService;
Key Performance Indicators
A contactless check-in system is more than just a convenience—it's a complete reimagining of the arrival experience. When properly implemented, it transforms a traditionally high-friction moment into a seamless transition that delights guests and frees your staff to focus on more meaningful interactions.
The code examples above provide a foundation you can adapt to your specific business needs. Remember that the best implementations prioritize simplicity and reliability over flashy features. Start with the core functionality, gather user feedback, and iterate toward a solution that feels invisible—because the best technology is the kind users barely notice they're using.
Explore the top 3 practical use cases of contactless check-in systems for seamless web app integration.
A digital system allowing hotel guests to complete the entire check-in process via mobile device before arrival, receive digital room keys, and bypass the front desk entirely—reducing wait times by up to 70% while minimizing physical contact points during pandemic conditions.
Enables patients to pre-register for appointments, verify insurance, complete required forms, and receive notification when an exam room is ready—streamlining administrative workflows while reducing crowded waiting rooms and cross-contamination risks in medical facilities.
Provides attendees with digital credentials, venue maps, and personalized agendas through pre-registration, while enabling organizers to track attendance patterns in real-time and manage capacity limits without creating entry bottlenecks at large-scale professional gatherings.
From startups to enterprises and everything in between, see for yourself our incredible impact.
Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We’ll discuss your project and provide a custom quote at no cost.Â