Skip to content
All builds
Websites

SundialAppointment Scheduler

What it does for you

Clients book themselves, and your staff calendar keeps up.

Public booking in the client's time zone, a drag-to-reschedule staff calendar, .ics feeds and imports, and a transaction-guarded no-double-booking rule.

Sundial screenshot

Who it's for

Small service businesses that lose hours to phone tag over appointments. Clients book in their own time zone, and staff move appointments around on a calendar.

Screenshots

Sundial screenshot 1

In more detail

Sundial is a booking system for a small service business. Clients pick a time in their own time zone, staff manage a drag-and-drop calendar, and every appointment drops into any calendar app. Small service businesses lose hours to phone tag, and cheap booking tools get time zones and double bookings wrong. I wanted to build the hard middle of a scheduler, not just the form on top.

Honest limits

Nothing talks to Google Calendar, Supabase, or any mail service. Those seams are only documented in the README. Drag-to-reschedule and the booking clicks were checked by hand and by screenshot, not by a browser test runner. The API paths behind them are covered by live tests. It's never been used by a real business with real clients.

How I built it

Sundial started on Codex's list. On the last day, when Codex stopped picking up rows, Claude Code took it over, and it took about 40 minutes of wall-clock time.

It runs on Node 24 with only built-ins (node:http, node:sqlite, node:crypto), vanilla ES modules in the browser, no build step, no npm. Claude Code wrote the pure scheduling core first (time zones, intervals, availability, slots, repeat rules, an iCalendar writer and parser, reminders, the waitlist) with 290 assertions before any server code existed. Then the server, then the UI. I set the scope and the rules: store UTC, render with Intl, hash every token at rest, demo login only with --demo. I reviewed the screenshots.

Staff get day and week calendars, drag-to-reschedule, a waitlist, a subscribable calendar feed, and an import for busy times from any .ics export. Emails go to an outbox folder, never sent. Everything was written in files under about 4 KB because the API connection was still dropping long responses.

What I learned

  • Wall-clock time needs two rules written down: an ambiguous fall-back time takes the earlier instant, and a spring-forward gap shifts forward. With those, "9:00 every Thursday" stays 9:00 across daylight saving, and the 23-hour and 25-hour days fall out of the tests cleanly.
  • A no-double-booking rule only holds if the conflict check runs inside the same BEGIN IMMEDIATE lock as the insert. We proved it with worker threads on separate SQLite connections released by a shared barrier, and with five simultaneous HTTP requests. Exactly one booking wins every time.
  • Calendar files fold long lines at 75 octets, not 75 characters. Folding by UTF-8 byte count keeps accented names and emoji intact, and reading my own output back with my own parser was the cheapest round-trip test.
  • Hashing tokens at rest has a product cost. The server can't resend a manage link, so reminder emails point back to the confirmation instead.

What carried forward

Sundial follows the same zero-dependency Node and SQLite shape as the other auth-heavy builds finished that day (61, 62, 63, 75, 80): scrypt staff sessions, an httpOnly cookie, CSRF, and hashed links. The "pure core with hundreds of assertions before any server code" order is the same one project 75 used. When the strict CSP blocked the inline theme script and the first screenshot had no colors, the fix was a separate theme-boot.js, the same file Latchkey (61) ships.

If I took it further

I'd store availability as a small rules table so Postgres could enforce no-overlap with an exclusion constraint, and drive the UI with a real browser test runner.

#068 Built with Claude Code September 24, 2026 521 automated checks

Want one like this?