OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-booking commits / 1d1e729

docs/research: Google Calendar on DreamHost feasibility
Research only, no code. Blocked on one fact: Shared or VPS? DreamHost Shared allows PHP ONLY — Node unsupported, Passenger removed (was the only path to Python/Ruby), no persistent processes (procwatch), cron >=5min. That removes the entire Hypercore stack: Hyperswarm needs a persistent process + UDP, Hyperbee/autobase need a live process. Cannot be walked back on Shared at any price; P2P requires VPS. DreamHost VPS runs Ubuntu 22.04/24.04 -> glibc 2.35/2.39, above bare's GLIBC_2.34 floor (measured via objdump on the linux-x64 prebuild), so bare runs fine there. Google OAuth constraints verified: device flow does NOT support any Calendar scope; OOB copy-paste fully removed 2023-02-01; loopback 127.0.0.1 supported and recommended for desktop clients. Two viable flows only. Measured, not assumed: bare-fetch does TLS to Google (200/159ms); lib/gcal.js runs unmodified under plain node with global fetch via its httpClient seam; test baseline 15/17 under bare (2 pre-existing integration failures). See hyper-booking#1 for the device-flow post-mortem. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1d1e729fa40c52a828060adda85d9ec5665e708b
by plankatron <fleeky@gmail.com> · 2026-07-30T01:21:43.000Z

Changed files (1) +240 -0

docs/research/dreamhost-gcal-feasibility.md +240 -0
……@@ -0,0 +1,240 @@
1# Google Calendar sync/edit on DreamHost — feasibility research
2
3**Date:** 2026-07-29
4**Status:** Research complete. Blocked on ONE fact: is the target DreamHost **Shared** or **VPS**?
5**Question asked:** "can we make a bare library that will sync / edit google calendar?"
6**Short answer:** Yes on VPS. On Shared, `bare` is impossible and the only runtime left is **PHP**.
7
8---
9
10## 1. Prior art (already in this repo)
11
12Google Calendar work already exists here — this was NOT greenfield:
13
14| File | Lines | What |
15|---|---|---|
16| `lib/gcal.js` | 275 | `GoogleCalendarClient` — OAuth2 code exchange, token refresh (5-min buffer), FreeBusy, calendarList, Hyperbee token store + 5-min cache, disconnect |
17| `lib/routes/gcal.js` | 211 | connect / callback / calendars / disconnect, CSRF nonce state |
18| `test/gcal.test.js` | 617 | 17 tests, **all against an injected mock `httpClient`** |
19| `docs/design/v1-google-calendar.md` | 671 | Full spec incl. push channels, edge cases, build plan |
20
21**Test baseline (measured, `bare test/gcal.test.js`): 15/17 pass.**
22The 2 failures are pre-existing HTTP-integration tests
23(`integration — slots API excludes Google Calendar busy times`,
24`integration — slots API works without gcal configured`).
25All 14 unit tests pass. Not investigated — unrelated to this research.
26
27### What prior art does NOT cover
28- **Read-only.** Scopes are `calendar.freebusy` + `calendar.calendarlist.readonly`.
29- **No event CRUD** — no `events.insert/patch/delete`. This is the "edit" half.
30- **No incremental sync** — no `events.list` + `syncToken`. This is the "sync" half.
31- **Not a library** — takes a Hyperbee `db`, hardcodes `gcal:provider:${id}` keys,
32 `console.error`s directly, assumes hyper-booking's provider model.
33
34---
35
36## 2. Verified by measurement (not docs)
37
38Everything in this section was actually run on 2026-07-29.
39
40- **`bare-fetch` does TLS to Google.**
41 `GET https://accounts.google.com/.well-known/openid-configuration`
42 → **200 in 159 ms** under `bare v1.26.0`. Every existing test is mocked, so this
43 had never been proven before.
44
45- **`lib/gcal.js` runs UNMODIFIED under plain node.**
46 Ran under `node v24.13.0` with global `fetch` injected as `httpClient`:
47 `mergeBusyBlocks` correct; live token-endpoint call rejected with the real Google
48 error `"The OAuth client was not found."`
49 Works because `gcal.js` imports nothing runtime-specific at top level — it only
50 lazy-imports `bare-fetch` as a *fallback* when no `httpClient` is injected.
51 **That injection seam is the portability escape hatch.**
52
53- **`bare` linux-x64 requires `GLIBC_2.34`.**
54 `objdump -T .../bare-runtime-linux-x64/bin/bare` → max symbol `GLIBC_2.34`.
55 Dynamically linked against `libstdc++`, `libgcc_s`, `libm`, `libc`. Not static.
56 → needs Ubuntu 22.04+ / Debian 12+ / RHEL 9+.
57
58---
59
60## 3. DreamHost capability matrix
61
62### Shared
63| | |
64|---|---|
65| Node.js | **Not supported.** VPS/Dedicated only. Third-party docs citing DreamHost's wiki warn that attempting to compile Node **can get the account locked**. |
66| Passenger | **Removed from Shared** ("after March 31st"; surrounding blog context = 2024). This was the only route to Python/Ruby. |
67| Persistent processes | **Forbidden by policy.** `procwatch` kills on RAM, CPU, or *"running for over a week."* |
68| Cron | ≥ 5 min interval, ≤ 5 simultaneous jobs (AUP) |
69| PHP | 8.1–8.5, including **command-line PHP** |
70| MySQL | 8 (shared server) |
71| HTTPS | free Let's Encrypt |
72
73**⇒ On Shared the only server-side runtime is PHP.** Not a preference — the only
74thing left standing. `bare` cannot run (no Node, no compile, glibc question never
75even arises).
76
77### VPS / Dedicated
78- Ubuntu **22.04 (Jammy)** or **24.04 (Noble)**; 24.04 is the 2026 default.
79 → glibc **2.35 / 2.39**, both above bare's **2.34** floor. **`bare` runs.**
80- Root access. `procwatch` does **not** run on Managed VPS / Dedicated / DreamPress.
81- ⚠️ **Do not use DreamHost's Passenger for Node** — it "can't host Node.js apps
82 newer than version 13." Run `bare`/node directly under systemd or pm2 instead.
83- ⚠️ Managed vs Self-Managed VPS may differ on root / binary-exec rights — unconfirmed.
84
85*Caveat: DreamHost's KB returns 403 to automated fetches, so the above is from
86search excerpts and third-party docs, not pages read directly. Confirm in panel.*
87
88---
89
90## 4. Google OAuth constraints (all verified against Google docs)
91
92These eliminate most "headless auth" options:
93
94- **Device flow does NOT support Calendar.** Allowed scopes are exactly:
95 `email`, `openid`, `profile`, `drive.appdata`, `drive.file`, `youtube`,
96 `youtube.readonly`. *"The OAuth 2.0 flow for devices is supported only for the
97 following scopes."* No Calendar scope of any kind.
98 ⚠️ A `device/code` probe with a bogus `client_id` returns `401 invalid_client`
99 **before** scope validation — so that probe does NOT prove scope support.
100 This is how the wrong recommendation was initially made in-session.
101
102- **OOB / manual copy-paste is fully REMOVED**, not deprecated.
103 `urn:ietf:wg:oauth:2.0:oob` blocked Oct 3 2022, enforced Feb 1 2023.
104
105- **Loopback IP is supported and recommended for desktop apps** —
106 `http://127.0.0.1:PORT` / `http://[::1]:PORT`. Deprecated for **mobile** only.
107 Requires a *Desktop app* OAuth client type.
108
109- **PKCE** supported, "recommended" not required.
110
111⇒ For Calendar there are exactly **two** viable flows:
112**(A)** loopback redirect (desktop client), **(B)** public HTTPS callback (web client).
113
114---
115
116## 5. Architecture by tier
117
118### If Shared → PHP + MySQL + cron
119
120The callback is an ordinary route — **the app IS the public host, so there is no NAT
121and no relay**. PKCE becomes optional (its job was protecting a code across an
122untrusted hop; there is no hop).
123
124```
125~/yourdomain.com/ (webroot)
126 oauth/start.php → build auth URL, redirect to Google
127 oauth/callback.php → code → tokens → MySQL
128 webhook.php → optional: Google push notifications
129~/lib/ (OUTSIDE webroot)
130 gcal.php → refresh, list, insert, patch, delete
131 db.php
132 config.php → client_secret, chmod 600
133~/bin/
134 sync.php → cron: php8.3 ~/bin/sync.php
135```
136
137```sql
138CREATE TABLE gcal_account (
139 id INT AUTO_INCREMENT PRIMARY KEY,
140 user_id VARCHAR(64) NOT NULL,
141 refresh_token VARBINARY(512) NOT NULL, -- encrypt at rest
142 access_token VARBINARY(512),
143 token_expiry BIGINT,
144 sync_token VARCHAR(255), -- incremental cursor
145 channel_id VARCHAR(64), -- if using push
146 channel_expiry BIGINT,
147 UNIQUE KEY (user_id)
148);
149```
150
151Why PHP genuinely fits: the Calendar API is plain REST/JSON over HTTPS, and OAuth +
152REST are **request-scoped** — exactly shared hosting's execution model. PHP core has
153`curl`, `json`, `hash`, `openssl`, `random_bytes`. No Composer, no Google SDK.
154
155**Two correctness requirements:**
1561. **Handle `410 GONE`.** Sync tokens get invalidated server-side (expiry, ACL
157 changes); response carries `"reason": "fullSyncRequired"`. Must discard the token
158 and re-run a **full paginated sync** to earn a fresh one. Will happen in production.
1592. **Push notifications still need cron.** Google: *"there's no automatic way to renew
160 a notification channel — when a channel is close to its expiration, you must
161 replace it"* via `watch`. Let's Encrypt satisfies the valid-cert requirement, so
162 push works, but it **adds** a renewal job rather than removing the cron.
163 → **Start cron-only (`syncToken`); add push later purely as latency optimization.**
164
165### If VPS → standalone runtime-neutral `bare-gcal`
166
167The originally chosen design is back on the table:
168
169```
170bare-gcal/
171 lib/auth.js loopback flow + web-redirect flow + refresh (NOT device flow)
172 lib/events.js list / insert / patch / delete
173 lib/sync.js syncToken incremental; syncOnce() + optional loop
174 lib/freebusy.js lifted from hyper-booking/lib/gcal.js as-is
175 lib/store.js TokenStore interface + HyperbeeTokenStore
176```
177Core carries **zero `bare-*` imports**; `fetch` injected or global (proven in §2).
178`hyper-booking/lib/gcal.js` becomes a thin adapter.
179
180---
181
182## 6. ⚠️ The tradeoff if Shared is chosen
183
184Dropping bare/node on Shared **removes the entire Hypercore stack**. Hyperswarm needs
185a persistent process and UDP; Hyperbee and autobase need a live process holding
186storage open. None survive `procwatch`.
187
188The result is a conventional LAMP app with **no P2P properties whatsoever**. That may
189be the right call for "sync a client's calendar on hosting they already pay for" —
190but it is a different kind of app from everything else in this KB, and **it cannot be
191walked back later on Shared at any price.** P2P requires VPS.
192
193---
194
195## 7. What transfers regardless of tier
196
197`lib/gcal.js` is mostly runtime-independent algorithm; the PHP port is mechanical:
198- `mergeBusyBlocks()` (lines 245–275) — interval sort + merge, pure logic
199- token refresh with the **5-minute expiry buffer** (line 117) — worth keeping
200- cache-TTL pattern; graceful-degradation-on-API-failure (never blocks slot calc)
201- endpoint URLs and OAuth parameter shapes, already correct
202
203~100 lines of thought-through, tested logic.
204
205---
206
207## 8. Open questions / next actions
208
2091. **☐ BLOCKING: Shared or VPS?** Decides language, architecture, and whether P2P is
210 possible at all. If SSH access exists, this settles the environment:
211 ```bash
212 ldd --version | head -1; uname -m; hostname; node -v 2>/dev/null || echo "no node"
213 ```
2142. ☐ Confirm the Passenger-removal year in the DreamHost panel (KB 403s automated fetch).
2153. ☐ Confirm Managed vs Self-Managed VPS root / binary-exec rights (if VPS).
2164. ☐ Register OAuth client — **type depends on flow**: *Desktop app* for loopback,
217 *Web application* for an HTTPS callback. Different credentials.
2185. ☐ Scope upgrade to `calendar.events` — current scopes cannot write.
2196. ☐ First build step either way: a **live OAuth round-trip against real Google**
220 (everything to date is mocked; that is the piece most likely to surprise).
2217. ☐ Unrelated pre-existing: 2 failing integration tests in `test/gcal.test.js`.
222
223---
224
225## Sources
226
227- [Node.js overview – DreamHost KB](https://help.dreamhost.com/hc/en-us/articles/217185397-Node-js-overview)
228- [Persistent processes – DreamHost KB](https://help.dreamhost.com/hc/en-us/articles/214869648-Persistent-processes)
229- [Why did procwatch kill processes – DreamHost KB](https://help.dreamhost.com/hc/en-us/articles/216540488-Why-did-procwatch-kill-processes-on-my-Shared-server-)
230- [Passenger overview – DreamHost KB](https://help.dreamhost.com/hc/en-us/articles/215769578-Passenger-overview)
231- [Migrating a Django site off DreamHost Shared Passenger](https://orangegnome.com/posts/2880/migrating-a-django-website-from-dreamhost-shared-server-with-passenger-to-vps-with-gunicorn)
232- [PHP versions at DreamHost](https://help.dreamhost.com/hc/en-us/articles/215082337-What-versions-of-PHP-are-available-at-DreamHost)
233- [MySQL overview – DreamHost KB](https://help.dreamhost.com/hc/en-us/articles/215099117-MySQL-overview)
234- [Create a cron job – DreamHost KB](https://help.dreamhost.com/hc/en-us/articles/215088668-Create-a-cron-job)
235- [Ubuntu 22.04 vs 24.04 for a VPS – DreamHost](https://www.dreamhost.com/blog/ubuntu-22-04-lts-24-04-lts-vps/)
236- [OAuth 2.0 for TV and Limited-Input Devices](https://developers.google.com/identity/protocols/oauth2/limited-input-device)
237- [OAuth 2.0 for Mobile & Desktop Apps](https://developers.google.com/identity/protocols/oauth2/native-app)
238- [OOB Flow Migration Guide](https://developers.google.com/identity/protocols/oauth2/resources/oob-migration)
239- [Calendar API: Push Notifications](https://developers.google.com/workspace/calendar/api/guides/push)
240- [Calendar API: Synchronize resources efficiently](https://developers.google.com/workspace/calendar/api/guides/sync)