Open-source scheduling for people and teams. A Python/Django alternative to hosted appointment tools, packaged as an Adios template. MIT licensed; no SaaS subscription, external scheduling service, or JavaScript build is required.
- Share a public workspace page and individual booking links.
- Offer one-on-one, round-robin, or collective meetings with multiple hosts.
- Set weekly hours, meeting lengths, buffers, minimum notice, and booking windows.
- Book across IANA time zones and daylight saving changes.
- Read busy times and write bookings to Google Calendar, Microsoft Outlook, and Apple iCloud Calendar. Each user can connect one calendar per provider.
- Cancel or reschedule through a private confirmation link. A failed reschedule preserves the original reservation.
- Invite teammates through private links that expire after seven days.
- Download
.icsevents, optionally email confirmations, and retry failed calendar updates automatically.
The host UI and public booking pages are server-rendered Django templates with
local CSS and a small progressive enhancement script. Booking works without
JavaScript. Pipenv manages dependencies through Pipfile and Pipfile.lock,
which records exact versions and package hashes, including transitive
dependencies. The runtime is Python 3.13 with Django 5.2 LTS.
Install Python 3.13 and Pipenv 2026.8.0 (for example,
pipx install pipenv==2026.8.0), then:
export PIPENV_VENV_IN_PROJECT=1
export PIPENV_DONT_LOAD_ENV=1
pipenv verify
pipenv sync --python 3.13
export DJANGO_DEBUG=true
export PUBLIC_BASE_URL=http://localhost:8080
pipenv run python manage.py migrate
pipenv run python manage.py bootstrap --username alex --email alex@example.com \
--team "Alex's workspace" --slug alex --timezone Europe/Paris
pipenv run python manage.py runserver 127.0.0.1:8080The bootstrap command prompts for a password and creates a workspace owner,
weekday 09:00–17:00 availability, and three editable booking links. There are no
default accounts or passwords. For automated installation, supply the password
through a temporary GATHER_ADMIN_PASSWORD environment variable and unset it
after bootstrap. The command refuses to overwrite an existing workspace or user.
Open /login/ to manage the workspace and /alex/ to try the booking flow. The
default meeting notice is two hours. SQLite is available only for local
development and basic tests; use PostgreSQL to verify concurrent reservations.
Environment files are examples only: export variables explicitly. Keep
PIPENV_DONT_LOAD_ENV=1 set to prevent Pipenv from loading a local .env file.
runserver does not start the retry worker. In another terminal with the same
environment, run pipenv run python manage.py sync_calendars --loop. The
production command python -m scheduler.serve supervises both Gunicorn and this
worker.
Copy this directory into your own project and run:
adios upThe manifest follows the Django template contract: Python runtime, managed
PostgreSQL 16, generated secrets, lockfile verification with
python -m pipenv verify, locked dependency installation with
python -m pipenv install --system --deploy, and collectstatic during build,
a separate migrate --noinput release command, and a read-only
database health probe at /healthz. Deployment identity is supplied by Adios.
Adios installs dependencies into the runtime's Python environment, so its
release, start, and terminal commands use python directly. The build fails if
Pipfile.lock is missing or out of date.
After deployment:
- Set
PUBLIC_BASE_URLto the exact public HTTPS origin, such ashttps://meet.example.com, with no path. This supplies absolute invitation, email, and OAuth callback URLs. The service can start without it so the initial deployment can complete; OAuth becomes available after configuration. - Run
python manage.py bootstrapwith the arguments above in the app's terminal, with your real owner email, workspace name, slug, and time zone. The password is chosen interactively. - Sign in, adjust the initial hours and booking links, connect calendars, and share your public workspace page.
- Configure SMTP if you want email confirmations. The confirmation page and calendar download remain usable without email.
Keep DJANGO_DEBUG disabled in production. The application requires PostgreSQL,
DJANGO_SECRET_KEY, and CALENDAR_ENCRYPTION_KEY; Adios supplies these using the
manifest. Back up PostgreSQL and retain the encryption key across deployments.
Changing that key without re-encrypting existing credentials requires every
calendar to be reconnected. For multiple replicas, all must share the same
PostgreSQL database, origin, and keys.
Create a Google Cloud OAuth client of type Web application, enable the Google
Calendar API, and configure its consent screen for your intended users. Set
GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in the deployment; store the secret
in Adios secrets and reference it through secret://GOOGLE_CLIENT_SECRET.
Register this exact authorized redirect URI:
https://meet.example.com/calendars/google/callback/
The app requests offline access with PKCE and session-bound state. It uses the
calendar.events, calendar.events.freebusy, and calendar.calendarlist.readonly
scopes. In the host UI, connect Google and select a writable calendar. Google
consent-screen testing restrictions and any verification requirements depend on
your app's audience; configure the OAuth application for your actual deployment.
References: OAuth web-server flow, free/busy API, event creation.
Register a Microsoft Entra web application. To support both work/school accounts
and Outlook.com, choose the account type allowing organizational directories and
personal Microsoft accounts. Set MICROSOFT_CLIENT_ID and
MICROSOFT_CLIENT_SECRET, using Adios secrets for the client secret.
Register this exact web redirect URI:
https://meet.example.com/calendars/microsoft/callback/
Grant delegated Calendars.ReadWrite access. Gather requests offline_access,
uses the common OAuth endpoint with PKCE, and refreshes credentials server-side.
An organization's administrator may need to approve access under its tenant
policy. Connect Microsoft in the host UI and select a writable calendar.
References: Microsoft OAuth authorization-code flow, calendarView, event creation.
Enable two-factor authentication on your Apple Account and generate an app-specific password under Sign-In and Security → App-Specific Passwords. In Gather, select Calendars → Connect Apple, enter your Apple Account email and that generated password, then choose an iCloud calendar.
The integration uses CalDAV at https://caldav.icloud.com and discovered iCloud
calendar endpoints. It does not use Sign in with Apple or request your primary
Apple Account password. Credentials are encrypted at rest. Calendars kept only
under “On My Mac” or “On My iPhone” must first be moved to iCloud. This adapter
targets iCloud; it is not a generic arbitrary-server CalDAV connector.
References: Apple app-specific passwords, Python CalDAV library.
| Variable | Purpose |
|---|---|
PUBLIC_BASE_URL |
Exact public HTTPS origin; HTTP is allowed only in development/tests. |
DATABASE_URL |
PostgreSQL URL. URL-encoded credentials and query options such as sslmode are supported. |
DJANGO_SECRET_KEY |
Stable signing secret, generated by Adios. |
CALENDAR_ENCRYPTION_KEY |
Independent stable secret for authenticated credential encryption, generated by Adios. |
DJANGO_ALLOWED_HOSTS |
Optional comma-separated hosts. Defaults to * for Adios internal health probes; absolute links use the configured origin. |
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET |
Google OAuth application credentials. |
MICROSOFT_CLIENT_ID, MICROSOFT_CLIENT_SECRET |
Microsoft OAuth application credentials. |
SEND_BOOKING_EMAIL |
true enables confirmation/cancellation emails; default false. |
EMAIL_HOST, EMAIL_PORT |
SMTP server and port; default port 587. |
EMAIL_HOST_USER, EMAIL_HOST_PASSWORD |
SMTP authentication. Store the password as an Adios secret. |
EMAIL_USE_TLS |
STARTTLS; default true. |
DEFAULT_FROM_EMAIL |
Verified sender address for your SMTP provider. |
GUNICORN_WORKERS, GUNICORN_THREADS |
Defaults: two workers, four threads each. |
Adios terminates HTTPS. Only expose the application through your trusted ingress,
which must overwrite X-Forwarded-Proto. Sessions and CSRF cookies are secure in
production. A database-backed limiter covers login, booking, and connection
attempts. It uses the direct peer address rather than trusting forwarded IP
headers; configure visitor-aware limits at your trusted ingress for larger sites.
- PostgreSQL profile-row locks serialize booking attempts for each host across event types and workspaces. Collective meetings reserve all hosts. Round robin assigns an available host with the fewest confirmed bookings for that event.
- Times are offered in 15-minute increments within each visitor's day. Hours repeat in the host's time zone; nonexistent DST times are not offered and repeated times retain distinct offsets. Hours are one same-day interval per weekday. Block holidays or one-off absences in a connected calendar.
- Every selected provider calendar is checked live before a reservation. A provider outage stops booking rather than treating unknown availability as free. Calendar IDs are saved with each delivery, so changing the selected calendar does not move old meetings.
- Reservations remain confirmed if a provider write fails. The dashboard and
confirmation page show a pending update. A supervised worker retries calendar
writes every minute; stable Google event IDs, Microsoft extended properties
plus transaction IDs, and deterministic iCloud resource URLs prevent duplicate
writes after a lost response. Manual retry:
python manage.py sync_calendarsin the deployed app's terminal, orpipenv run python manage.py sync_calendarslocally. - Cancellation releases the local reservation and queues external cancellation. Until the external update succeeds, its busy event can still block that time. Rescheduling creates a new booking and cancels the old one atomically.
- Provider events are host appointments. Gather sends its own optional guest
emails and offers
.icsdownloads; it does not send provider attendee invites. Email failures are shown in the confirmation page and logs. Failed or rejected email sends are not automatically retried, to avoid repeated mail. A process crash after SMTP accepts a message but before its database commit can still produce a duplicate email; SMTP cannot guarantee exactly-once delivery. - Private booking and invitation URLs are bearer links. Anyone holding a booking
link can view its guest details and cancel/reschedule it. Keep these URLs out
of analytics and public sharing. Pages use
no-storeand asame-originreferrer policy, which suppresses these URLs on cross-origin navigation while preserving same-origin CSRF verification.
External calendar edits are not imported into Gather booking records: moving or
deleting a provider event does not reschedule/cancel its local booking. Perform
those actions through Gather. Provider busy reads and writes cannot be one
atomic operation, so an event created directly in another calendar client after
the final availability check can still race a booking. This initial template
does not include video-conference provisioning, payments, reminders, webhook
subscriptions, or an embedded iframe widget. Account recovery is available to
the operator through python manage.py changepassword USERNAME in the deployed
app's terminal (prefix with pipenv run locally).
pipenv verify
pipenv sync --dev
pipenv run python -m pip check
pipenv run python manage.py test scheduler
pipenv run python manage.py check
pipenv run python manage.py makemigrations --check --dry-run
pipenv run python manage.py collectstatic --noinputFor production locking tests, point DATABASE_URL at a disposable PostgreSQL
instance with permission to create a test database, then run the same test
command. SQLite skips the PostgreSQL concurrency test. Provider tests use fake
HTTP and CalDAV responses; live OAuth consent and calendar writes require your
own configured provider accounts. Never put real credentials or guest data in
fixtures or exported template repositories.
The included GitHub Actions workflow installs with the same Pipenv deployment command as Adios and runs the suite in Python 3.13 against an isolated PostgreSQL 16 service, including the concurrency test.
To change dependencies, edit the pins in Pipfile, run pipenv lock, then
pipenv sync --dev and the checks above. Commit both Pipfile and Pipfile.lock.
CI and Adios consume the committed lockfile without updating it. See the
Pipenv command reference for the
dependency-management commands.
Source lives in config/ and scheduler/. Add migrations for model changes,
preserve the host and event lock order for booking mutations, and keep tokens
out of templates, logs, and Django admin. This directory is the canonical source
for template-python-gather in the parent publication map. GitHub publication
uses the repository's existing staging/digest gate.
MIT. You can use, customize, deploy, and redistribute Gather under the terms of that license.