House Desk Calendar
A private, auto-updating Programs calendar that residents add once to Apple Calendar, Google Calendar, or Outlook.
I-House runs dozens of programs a week, announced across a newsletter and chat groups. House Desk gives residents one calendar feed that stays correct, and gives staff a single place to review and fix what goes into it. It runs on Cloudflare Pages with Pages Functions in TypeScript, D1, and R2.
In plain words. Subscribe once, and the house’s events show up in the calendar app you already use, then stay correct on their own. Behind that is a small editorial desk where staff check every event before it goes out.
At a glance
- Status: in production.
- Works with: Apple Calendar, Google Calendar, and Outlook.
- Stays fresh: the published feed is regenerated every 15 minutes.
- Private by design: each resident gets their own feed link, which lasts about four months and can be renewed or revoked.
- Reviewed: nothing reaches the calendar until staff publish it.
See it
You can try the live page at desk.ihnyc.affectivetech.com.
How it works
For a resident:
- Enter a name and email on the subscription page.
- Open the one-time link in the email. It shows what will happen, such as a first connection or replacing an existing one, and changes nothing yet.
- Confirm, and get a private feed address with buttons for Apple, Google, and Outlook. The calendar app checks the feed from then on, and residents are prompted to renew before it expires.
For staff, the events travel like this:
flowchart LR
I[Newsletter and chat intakes] --> C[Review-only candidates]
C --> O[Staff review and edit]
O --> P[(Published events)]
P --> S[Scheduled job, every 15 minutes]
S --> F[Calendar feed]
F --> A[Calendar apps]
Published events also feed the Pub’s TVs and its shift scheduling, for events marked “Share with the Pub”.
Problems I had to solve
Email links get opened by things that are not people
The preview step is a read-only request, so merely opening a link (as a mail client or security scanner might) cannot consume it or replace a working feed. Only the explicit confirmation changes anything. Feed tokens are looked up by SHA-256 hash, and newer ones are also kept in an encrypted envelope so an authorized operator can re-show a resident their own feed link. A working feed is never revoked just because a link was opened. A passwordless management page lets residents see status, renew, or revoke.
Events arrive in messy forms
- Intake. Events arrive from the weekly newsletter and from resident posts. The intake record carries no sender address or message body, and it never publishes anything itself; flyers arrive through a separate upload route.
- Review. Staff edit, merge duplicates (a matcher scores titles, places, times, and signup links), and publish. Imported identifiers survive only as provenance, and flyers are stored as immutable objects.
- Calendar notes. Every description follows one fixed layout: the event’s own text, a standard registration note (plus a recurring-program note for standing programs), links, and a source line. A single server-side composer produces it for the editor preview and the feed, so what an editor sees matches what a calendar app gets. Changing a global note advances the revision fields only for the events that use it, so clients refresh the right ones.
A broken job could blank everyone’s calendar
The scheduled job runs with no email, Notion, or House access, regenerates the feed from published events, and skips the write when nothing changed. If it ever finds zero published events without an explicit override, it stops before touching storage, so the last good feed stays in place.
Different staff need different powers
The React admin sits behind Cloudflare Access, and the app makes its own authorization decision on every call. Roles are reusable templates, but each person’s grant stores an exact permission snapshot, so a missing or stale role store can never widen access. Operator actions are audited, and alerts route to the right person per category. Analytics are first-party and use peppered email hashes, and new events do not record raw addresses. An operator view shows who signed up but never connected and where each person stopped (bounced, expired, link opened but not confirmed), which replaced a guess with a diagnosis. Read-only routes for a native iPhone app sit behind a feature flag and a separate, narrower Access application whose tokens are refused on every admin route.
A bug worth telling
Residents can enroll a mailbox to forward newsletters automatically. For several days, some automatic forwards were received, parsed, archived, and credited to nobody.
The cause was Gmail: it preserves a private forwarding address’s case in its confirmation mail, but lowercases the address when it relays an automatically forwarded message. The address contains a mixed-case token, so the lowercased version still passed the format check and produced a well-formed binding that was enrolled nowhere. Every attribution write was an exact-string lookup that quietly updated zero rows on a miss.
What kept it invisible was an earlier, correct fix. Before it, automatic forwards were rejected loudly. Afterwards they were accepted but unattributed, and an attributed and an unattributed forward produced byte-identical operator emails. I proved the cause from the mail provider’s own routing logs, which record the literal recipient upstream of any code in either repository.
The fix has two layers. It canonicalizes the binding at one choke point so everything downstream still sees an exact enrolled value, and it adds a required attribution field to the intake result, surfaced in the operator email, so any future change that breaks attribution is loud. It also records what was deliberately not done, such as touching the inbound-mail Worker, where a regression loses mail rather than a counter.
Scale and tools
More than 60 D1 migrations, applied through reviewed, isolated per-migration steps, and nearly 400 test files. Cloudflare Pages Functions, D1, R2, Resend for email, Turnstile for bot protection, TypeScript, React, Vite, Playwright.
Under the hood
The empty-feed guard (about 10 lines)
Why this matters: if a bug or an outage ever made the job think there are zero events, publishing that would wipe every resident’s calendar. This check throws instead, and the last good feed stays.
// functions/_lib/programs-ics.ts (excerpt)
const pages = await fetchPublishedD1Events(env.SUBS_DB, options);
if (
pages.length === 0
&& env.PROGRAMS_ICS_ALLOW_EMPTY_D1_FEED !== "true"
) {
throw new Error(
"D1 programs feed has zero published rows; set PROGRAMS_ICS_ALLOW_EMPTY_D1_FEED=true to allow"
);
}
Words used on this page
- Feed (ICS): a web address a calendar app checks regularly to stay up to date.
- Magic link: a one-time link sent by email instead of a password.
- Token: a long random secret that identifies one resident’s feed.
- Hash: a one-way fingerprint, so a secret can be checked without being kept.
- Fail closed: when unsure, do nothing harmful, rather than guess.
- D1 and R2: Cloudflare’s database and file storage.