Google OAuth setup
1. Create a Google Cloud project, enable the Calendar API, create an OAuth client
Section titled “1. Create a Google Cloud project, enable the Calendar API, create an OAuth client”- Open Google Cloud Console and create (or select) a project.
- Go to APIs & Services → Library, search for Google Calendar API, and click Enable.
- Navigate to APIs & Services → Credentials → Create Credentials → OAuth 2.0 Client ID.
- Set the application type to Web application.
2. Register the redirect URIs
Section titled “2. Register the redirect URIs”Add both of the following as Authorized redirect URIs in your OAuth client. Replace https://book.example.com with your actual APP_BASE_URL:
${APP_BASE_URL}/api/google/callback${APP_BASE_URL}/api/google/login/callbackBoth URIs must be registered — one is used for the per-user Calendar connection flow, the other for Google sign-in.
3. Set the environment variables
Section titled “3. Set the environment variables”Copy the Client ID and Client Secret from the Credentials page, then set:
GOOGLE_OAUTH_CLIENT_ID=your-client-idGOOGLE_OAUTH_CLIENT_SECRET=your-client-secret# Strong random string shared by ALL replicasGOOGLE_OAUTH_STATE_SECRET=<openssl rand -hex 32>GOOGLE_OAUTH_STATE_SECRET must be the same value on every replica. Generate it with:
openssl rand -hex 324. Secure tokens at rest
Section titled “4. Secure tokens at rest”TOKEN_ENCRYPTION_KEY encrypts stored Google OAuth tokens with AES-256-GCM. See the Configuration reference for details.
5. Connect accounts and use
Section titled “5. Connect accounts and use”Each user connects their own Google account from the owner console (/me). Once connected, every new booking automatically:
- Creates a Google Calendar event on the user’s calendar.
- Generates a Google Meet link included in the booking confirmation.
Choosing calendars
Section titled “Choosing calendars”Once an account is connected, /me/google lists its calendars with two independent choices per row:
- Read busy — block your availability against that calendar’s existing events. Tick as many calendars as you like, across every connected account.
- Write events here — the one calendar, across all your connected accounts, that new bookings are created on by default. Only one calendar can hold it at a time.
Every meeting type writes to that calendar — its write target — unless the type says otherwise.
Sending one meeting type to a different calendar
Section titled “Sending one meeting type to a different calendar”A meeting type can write its own events to a different calendar than your write target. Open Meeting types, pick the type, and under Basics choose a calendar under Calendar for new events — the same picker also appears on the Location step when you first create a meeting type. The blank option, My write target, is the default — leave it alone and that type keeps writing to your write target, exactly as before. Only calendars you’ve selected on /me/google appear in the list.
On a shared meeting type, each host picks their own: the creator sets it on the meeting-type page above, and every co-host sets theirs from Shared, under that type, next to their buffers. Whichever host’s calendar a given booking’s event actually lands on is the one whose choice took effect.
If the calendar a meeting type points at is later unticked on /me/google, or the connected account it belongs to is disconnected, bookings keep working — they fall back to your write target, and the meeting-type page shows a warning that the choice is no longer in effect. Your pick is not erased: saving other fields on that page leaves it exactly as it was, so re-ticking the calendar (or making it the write target again) on /me/google puts it straight back to work. Reconnecting a disconnected account doesn’t heal it on its own — reconnecting creates a new connection, so you’ll need to pick the calendar again there.
Choosing Google Meet as a location depends on whether the calendar a type actually writes to can generate Meet links, not on your write target. If a meeting type is pointed at a calendar that can’t, Google Meet is not offered as a location for it. On a shared meeting type this check always follows the creator’s own calendar — a co-host’s calendar choice doesn’t change whether Google Meet is offered.
Changing a meeting type’s calendar only affects bookings made after the change — events already created keep the calendar they were created on. When you save a new calendar, calit tells you how many upcoming bookings that leaves behind.
OAuth verification
Section titled “OAuth verification”Until your OAuth app is verified, Google shows users an “Google hasn’t verified this app” warning and caps the app at 100 new users (a per-project lifetime cap that cannot be reset). To remove the warning and lift the cap, complete verification in the Google Cloud Console:
- Set
OPERATOR_NAMEandPRIVACY_CONTACT_EMAIL(see configuration). calit then serves a complete privacy policy at${APP_BASE_URL}/privacyand terms at${APP_BASE_URL}/terms, including the required Google Limited Use disclosure. - On the OAuth consent screen, set the privacy-policy link to
${APP_BASE_URL}/privacy(and, optionally, terms to${APP_BASE_URL}/terms). - Verify domain ownership — either set
GOOGLE_SITE_VERIFICATIONto the token from Google Search Console (calit renders the<meta>tag on every page), or add the DNS TXT record Google offers. - Submit for verification.
Disconnect detection
Section titled “Disconnect detection”A Google connection can break without warning — access is revoked, the account password is changed, or the refresh token simply expires. calit detects this and fails closed: while an owner’s Google account is disconnected, their public booking page shows “Scheduling temporarily unavailable” instead of offering every slot as free. This prevents bookings landing on top of calendar events calit can no longer see.
Each connected account is probed on a schedule (every GOOGLE_PROBE_INTERVAL, default 1h) for a still-valid connection; the probe also keeps the refresh token warm. When a disconnect is found, the owner is emailed once per outage with a link to reconnect on the /me/google settings page.
Troubleshooting
Section titled “Troubleshooting”Read the logs first
Section titled “Read the logs first”calit logs every Google failure. With Docker Compose:
docker compose logs -f app | grep -i googleAt boot you get one line confirming what the app actually loaded (no secrets — only whether the client secret is set):
Google OAuth configured: clientId=1234-abc.apps.googleusercontent.com clientSecret=set redirectUri=https://book.example.com/api/google/callback loginRedirectUri=https://book.example.com/api/google/login/callback scope=https://www.googleapis.com/auth/calendar openid emailCompare redirectUri and loginRedirectUri against the Authorized redirect URIs in your OAuth client — they must match character for character. A wrong APP_BASE_URL (http vs https, trailing slash, wrong host) shows up here.
Connecting an account logs:
Google account connected for owner 1 (credential 3), refreshToken=storedrefreshToken=MISSING means Google returned no offline refresh token, so every later refresh will fail — disconnect the account on /me/google and connect again.
For extra detail (including transient probe failures), raise the log level:
QUARKUS_LOG_CATEGORY__SITE_ASM0DEY_CALIT_GOOGLE__LEVEL=DEBUG“Couldn’t reach Google for one or more accounts”
Section titled ““Couldn’t reach Google for one or more accounts””The account is connected but calit could not list its calendars. The log line naming the failure looks like:
WARN Google calendar list failed for owner 1, credential 3 java.io.UncheckedIOException: calendarList.list failed: HTTP 403 — Google Calendar API has not been used in project 123... before or it is disabledCommon causes, in the order they turn up:
| What the log says | Cause | Fix |
|---|---|---|
HTTP 403 … has not been used in project … or it is disabled | The Calendar API was never enabled | Enable Google Calendar API (step 1 above), then reload /me/google |
HTTP 403 … insufficient authentication scopes | The account was connected before the calendar scope was granted | Disconnect and reconnect the account |
error=invalid_client | Wrong GOOGLE_OAUTH_CLIENT_ID / GOOGLE_OAUTH_CLIENT_SECRET | Re-copy both from the Credentials page |
Google shows redirect_uri_mismatch in the browser before returning | Registered URI ≠ the redirectUri in the boot log | Register the exact URIs from the boot log |
invalid_grant | The refresh token is dead (revoked, password change, or a “Testing” app older than 7 days) | Reconnect the account; publish the OAuth app to production |
I/O error / connect timeout | The container cannot reach oauth2.googleapis.com / www.googleapis.com | Check egress firewall and DNS from inside the container |