Build an online booking system for a small business

Customers pick a free time slot, book it and get an email confirmation; the owner sees and cancels bookings in a simple admin page.

28 時間 intermediate PythonFlaskSQLite/PostgreSQLResendRender

1 Friday evening: rules before code

Booking apps fail on the rules, not the code. Write them down for one real business (a barber, a tutor, a repair shop):

  • Opening hours per weekday (e.g. Mon–Fri 09:00–17:00, Sat 10:00–14:00).
  • Slot length (30 min) and buffer between bookings (0–10 min).
  • How far ahead customers may book (e.g. 30 days) and the minimum notice (e.g. 2 hours).
  • What the customer must give: name, email, phone, a note.

Done when: a friend can read your rules and tell you whether "Saturday 13:45" can be booked.

2 Saturday morning: data model and slots

python -m venv .venv && source .venv/bin/activate
pip install flask flask-sqlalchemy python-dotenv
# models.py
from datetime import datetime
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()

class Booking(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    starts_at = db.Column(db.DateTime, nullable=False, unique=True)   # one booking per slot
    name = db.Column(db.String(100), nullable=False)
    email = db.Column(db.String(120), nullable=False)
    note = db.Column(db.String(500))
    cancelled = db.Column(db.Boolean, default=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)

Generate the free slots for a day from the rules, then remove the booked ones:

# slots.py
from datetime import datetime, timedelta, time
HOURS = {0: (9, 17), 1: (9, 17), 2: (9, 17), 3: (9, 17), 4: (9, 17), 5: (10, 14)}   # Mon=0
SLOT = timedelta(minutes=30)

def free_slots(day, booked, now):
    if day.weekday() not in HOURS:
        return []
    open_h, close_h = HOURS[day.weekday()]
    t, end = datetime.combine(day, time(open_h)), datetime.combine(day, time(close_h))
    out = []
    while t + SLOT <= end:
        if t not in booked and t >= now + timedelta(hours=2):
            out.append(t)
        t += SLOT
    return out

Done when: free_slots returns the right list for a weekday, a Saturday, a Sunday and "today after 15:00" in a quick test.

3 Saturday afternoon: the booking page

  • GET /book?day=2026-11-03 shows the free slots as buttons.
  • POST /book re-checks the slot is still free inside the request (two people can click the same slot), then inserts. The unique=True on starts_at is the final guard: catch IntegrityError and show "Sorry, that slot was just taken".
  • Validate the email, cap the note length, add CSRF protection (Flask-WTF) and a rate limit (Flask-Limiter, e.g. 10 bookings/hour per IP).

Done when: two browser windows trying to book the same slot end with exactly one booking.

4 Sunday morning: confirmations and cancelling

Send a confirmation email with Resend and a cancel link that carries a signed token, not the booking id:

from itsdangerous import URLSafeTimedSerializer
s = URLSafeTimedSerializer(app.config["SECRET_KEY"], salt="cancel")
token = s.dumps(booking.id)                       # in the email: /cancel/<token>
booking_id = s.loads(token, max_age=60 * 60 * 24 * 30)   # in the route

Add an owner page (/admin, behind a password from an environment variable) listing upcoming bookings with a Cancel button (POST form).

Done when: the customer gets the email, the cancel link frees the slot, and a tampered token is rejected.

5 Sunday afternoon: deploy

  • Move to PostgreSQL on Render (DATABASE_URL), keep secrets in environment variables.
  • Store times in UTC and show them in the business's time zone (zoneinfo.ZoneInfo("Europe/London")).
  • Add the booking link to the business's Google profile and website.

Done when: a real customer books from their phone and both of you get the email.

プロジェクトは完成しましたか?リンクをレビューに提出しましょう。 レビューに提出(50 ポイント)

ほかのプレイブック