Bonaventure OgetoBy Bonaventure Ogeto|

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.

Sign up for the McTaba newsletter

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