Lightweight self-hosted calendly alternative.
  • Python 37.8%
  • HTML 30.2%
  • JavaScript 20.6%
  • CSS 11%
  • Dockerfile 0.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-24 22:46:42 +02:00
app fix instant logout after first deploy bug 2026-09-24 22:46:42 +02:00
.dockerignore first version 2026-09-24 20:40:25 +02:00
.gitignore first version 2026-09-24 20:40:25 +02:00
docker-compose.yml first version 2026-09-24 20:40:25 +02:00
Dockerfile fix instant logout after first deploy bug 2026-09-24 22:46:42 +02:00
prompt.txt fix instant logout after first deploy bug 2026-09-24 22:46:42 +02:00
README.md first version 2026-09-24 20:40:25 +02:00
requirements.txt first version 2026-09-24 20:40:25 +02:00
run.py first version 2026-09-24 20:40:25 +02:00

Appointments 📅

A lightweight, self-hosted scheduling and calendar web application.

  • Stack: Python 3 (Flask, SQLAlchemy, SQLite), Gunicorn, Vanilla JavaScript, Debian 13 (trixie-slim).
  • Container First: Fully containerized with Dockerfile and docker-compose.yml.
  • Zero Frontend Dependencies: No npm, no external JS CDNs or frameworks.
  • Vibe-coded: Built with Gemini Flash.

Key Features

  1. Self-Hosted Public Calendar & Availability:

    • Public-facing booking calendar.
    • Admin month-view dashboard with individual slot creation and flexible recurring slot schedules.
    • Recurring Slot Unrolling: Recurring availability schedules are unrolled into individual database rows at creation time for complete autonomy over each individual slot.
  2. Visitor Timezone Detection & Translation:

    • Detects the visitor's local timezone via vanilla JavaScript (Intl.DateTimeFormat).
    • Translates all displayed slot times into the visitor's timezone.
    • Displays a prominent notification informing the user that timezone translation is occurring.
    • Allows visitors to manually change or inspect timezones.
  3. Proof-of-Work (PoW) CAPTCHA:

    • In-browser SHA-256 hash-collision solver using the native Web Crypto API (window.crypto.subtle.digest).
    • Invisible or near-instant for real human users (~100–300 ms), but computationally expensive for spam bots.
    • Zero third-party CAPTCHA cookies or external network requests.
  4. Optimistic Concurrency Control (OCC):

    • Eliminates double-booking race conditions during simultaneous reservations.
    • Implemented via a version column and SQLAlchemy OCC (version_id_col).
    • If two visitors attempt to reserve the same slot concurrently, the second transaction is safely rolled back and rejected with an informative conflict notification.
  5. Lazy Evaluation for Pending Reservations:

    • Submitting a booking puts the slot into a pending state for 10 minutes and emails a confirmation link to the guest.
    • If unconfirmed after 10 minutes, the backend automatically treats the slot as available again via lazy evaluation during database queries, without requiring external cron daemons.
  6. iCalendar (.ics) Integration:

    • Upon confirmation, both the administrator and the guest receive an email with an .ics attachment for calendar import.
    • All .ics event timestamps strictly use UTC (Z format).
    • Cancellations by either party trigger an .ics cancellation payload (METHOD:CANCEL, STATUS:CANCELLED) to remove the event from calendars.
  7. Cryptographically Secure Booking Management:

    • Confirmation emails contain a unique, cryptographically secure token link to cancel or reschedule appointments.
    • Rescheduling safely locks the new slot with OCC, releases the previous slot, and dispatches updated .ics notifications.
  8. Admin-Only Invite Link Tracking:

    • Administrators can generate unique invite links with custom descriptions (e.g. "Sent to Client X").
    • Visiting via an invite link sets a cookie that expires upon booking or after 2 days (whichever is earlier).
    • The invite link attribution is recorded on the booking and is visible exclusively to the administrator.
  9. Security & Configuration:

    • Passwords hashed with Argon2 (argon2-cffi).
    • Flask secret key read from a 32-byte binary file in persistent storage or generated via secrets.token_bytes(32).
    • Comprehensive in-app configuration UI for SMTP credentials, application URL, notification recipients, and site branding.

Data Persistence Definition

The application strictly defines what data needs to persist and what is ephemeral:

Persistent Data (Mounted to ./appointments_data):

  • appointments.db: SQLite database storing slots, bookings, invite links, and application configuration.
  • secret_key: 32-byte secret key for Flask session signing.
  • initial_otp.txt: Temporary record of the initial administrator one-time password generated on first deploy (deleted automatically when the administrator sets a permanent password).

Ephemeral Data (Inside container only):

  • Python runtime, packages, and virtual system files.
  • Application codebase and static assets.
  • Temporary files and runtime cache.

Quick Start with Docker Compose

  1. Clone the repository:

    git clone https://git.vitapavlik.cz/vitapavlik/appointments.git
    cd appointments
    
  2. Start the application:

    docker compose up --build -d
    
  3. Retrieve the One-Time Administrator Password (OTP): On first deployment, the backend outputs an OTP to stdout:

    docker compose logs appointments
    

    Note: A copy is also saved to ./appointments_data/initial_otp.txt and is deleted automatically after your first password change.

  4. Log in as Administrator:

    • Open: http://localhost:8080/admin/login
    • Username: admin
    • Password: <your one-time password>
    • You will be prompted to set a permanent password immediately.
  5. Configure SMTP & URLs:

    • Navigate to /admin/settings.
    • Set your Application URL (e.g., http://localhost:8080 or your reverse proxy domain).
    • Enter your SMTP Server Credentials to enable automated email confirmations and .ics attachments.
    • Use the Send Test Email tool to verify your connection.
  6. Create Availability:

    • Visit /admin/recurring to generate recurring weekly time slots unrolled into individual records.
    • Or click Add Single Slot directly on the dashboard.

Reverse Proxy Setup (e.g. Nginx, Caddy, Traefik)

The container serves pure HTTP over Gunicorn on internal port 8000 (mapped to host port 8080 by default):

server {
    listen 80;
    server_name appointments.yourdomain.com;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

License & Credits