Build a Coworking Space Booking System
Build a coworking booking system by managing resource calendars (desks and meeting rooms with capacity and hourly/daily rates), requiring Paystack payment before issuing an access code or QR pass, and offering both day passes (one-time payment) and monthly memberships (Paystack Plans). Webhook confirmation triggers the access credential generation.
Resource Booking and Payment
Tables: resources (name, type, capacity, hourly_rate, daily_rate, amenities[]), bookings (member_id, resource_id, start_time, end_time, duration_hours, total_amount, status, access_code, paystack_ref), memberships (member_id, plan_type, status, meeting_room_hours_remaining, paystack_subscription_code).
async function bookResource(memberEmail, memberId, resourceId, startTime, endTime) {
var resource = await db.resources.findById(resourceId);
var hours = Math.ceil((new Date(endTime) - new Date(startTime)) / (1000 * 60 * 60));
// Check availability
var conflicts = await db.bookings.findConflicts(resourceId, startTime, endTime);
if (conflicts.length > 0) throw new Error('Resource not available for selected time');
// Check if member has a monthly bundle with remaining hours
var membership = await db.memberships.findActiveByMember(memberId);
if (membership && resource.type === 'meeting_room' && membership.meeting_room_hours_remaining >= hours) {
// Deduct from bundle — no payment needed
await db.memberships.decrement(memberId, 'meeting_room_hours_remaining', hours);
return await createBookingWithAccess(memberId, resourceId, startTime, endTime, hours, 0, null);
}
var amount = resource.type === 'hot_desk' ?
(hours >= 8 ? resource.daily_rate : hours * resource.hourly_rate) :
hours * resource.hourly_rate;
var reference = 'CWK-' + resourceId + '-' + Date.now();
var booking = await db.bookings.create({
member_id: memberId, resource_id: resourceId, start_time: startTime, end_time: endTime,
duration_hours: hours, total_amount: amount, status: 'pending_payment', paystack_ref: reference,
});
var res = await fetch('https://api.paystack.co/transaction/initialize', {
method: 'POST',
headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
email: memberEmail, amount, currency: 'NGN', reference,
metadata: { booking_id: booking.id, resource_id: resourceId, start_time: startTime },
}),
});
return (await res.json()).data.authorization_url;
}
// Webhook: confirm booking and issue access
if (event.event === 'charge.success') {
var meta = event.data.metadata;
if (!meta.booking_id) return res.sendStatus(200);
var accessCode = Math.random().toString(36).slice(2, 8).toUpperCase(); // 6-char code
await db.bookings.update(meta.booking_id, { status: 'confirmed', access_code: accessCode });
await sendAccessCode(meta.booking_id, accessCode, meta.start_time);
}
Monthly Memberships and Access Control
// Monthly membership subscription
async function subscribeToMembership(memberEmail, memberId, planName) {
var PLANS = {
hot_desk_monthly: { price: 2500000, meeting_room_hours: 0, paystack_plan_code: process.env.HOT_DESK_PLAN },
pro_monthly: { price: 4500000, meeting_room_hours: 10, paystack_plan_code: process.env.PRO_PLAN },
};
var plan = PLANS[planName];
var res = await fetch('https://api.paystack.co/transaction/initialize', {
method: 'POST',
headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
email: memberEmail, amount: plan.price,
plan: plan.paystack_plan_code,
metadata: { member_id: memberId, plan_name: planName, meeting_room_hours: plan.meeting_room_hours },
}),
});
return (await res.json()).data.authorization_url;
}
if (event.event === 'subscription.create') {
var meta = event.data.metadata;
await db.memberships.upsert({
member_id: meta.member_id,
plan_type: meta.plan_name,
status: 'active',
meeting_room_hours_remaining: meta.meeting_room_hours,
paystack_subscription_code: event.data.subscription_code,
});
}
// Renew meeting room hours each billing cycle
if (event.event === 'invoice.create' && event.data.status === 'success') {
var code = event.data.subscription.subscription_code;
var membership = await db.memberships.findBySubscription(code);
if (membership.plan_type === 'pro_monthly') {
await db.memberships.update(membership.id, { meeting_room_hours_remaining: 10 }); // reset monthly
}
}
// Validate access at the door (QR scan or code entry)
async function validateEntry(accessCode, resourceId) {
var booking = await db.bookings.findByAccessCode(accessCode);
if (!booking || booking.resource_id !== parseInt(resourceId)) throw { status: 403, message: 'Invalid access code.' };
var now = new Date();
var start = new Date(booking.start_time);
var end = new Date(booking.end_time);
if (now < new Date(start.getTime() - 15 * 60000)) throw { status: 403, message: 'Too early — entry allowed 15 min before booking.' };
if (now > end) throw { status: 403, message: 'Booking has expired.' };
return { valid: true, member_id: booking.member_id, resource_name: booking.resource.name };
}
Learn More
See build a hotel booking and deposit system for a similar resource booking pattern with deposit collection and balance at checkout.
Key Takeaways
- ✓Separate resource types: hot desks (first-come-first-served) vs meeting rooms (reserved slots).
- ✓Generate an access QR code or PIN only after the Paystack charge.success webhook fires.
- ✓Offer day passes (one-time) and monthly memberships (Paystack Plans) for different use cases.
- ✓Meeting room bundles (10 hours/month) reduce friction for regular users — track against their monthly allowance.
- ✓Monitor resource utilization rates — if a desk is occupied less than 50% of the time, it may not be worth reserving.
Frequently Asked Questions
- How do I handle last-minute cancellations for meeting rooms?
- Define a cancellation policy: full refund if cancelled 2+ hours before start; 50% refund if 1-2 hours before; no refund if less than 1 hour. Process refunds via the Paystack Refund API with the original reference. For monthly members, return the hours to their meeting_room_hours_remaining instead of a cash refund.
- Can I support multiple coworking locations under one platform?
- Yes. Add a locations table and link resources to locations. Members browse by location, then book resources. Monthly memberships can be location-specific (home location) or multi-location (access to all branches). For corporate clients who want dedicated desks across multiple offices, create a corporate plan with higher pricing and multi-location access.
- How do I handle walk-in members who have not pre-booked?
- Build a kiosk or front-desk interface where staff can initiate a booking on behalf of a walk-in and send a Paystack payment link to their phone. Alternatively, accept cash for walk-ins and manually mark the booking as "walk_in_cash" in the system. Track walk-in volume — if it exceeds 20% of occupancy, it is worth adding a day-pass QR code that members can buy via PayStack without talking to staff.
Ready to build real-world apps?
Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.
Apply to the McTaba Marathon