Build Rise Calendar
KINDAback to the verdict
A personal calendar and task planner whose difficulty is not the grid but the sync underneath it: an incremental sync that invalidates, push channels that expire quietly, blocks that must never move a real meeting, and a plan that has to re-fit itself every time the day slips.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the desktop application, the sync worker and the tests.
Get it nodejs.org, or nvm install 22. open ↗
Verify
node --version prints v22 or higher - accountfree
Why Recurrence with exceptions, declined invitations and all-day events are the cases that break sync and planning, and an empty test calendar contains none of them.
Get it Your own calendar, with at least one recurring series that has a cancelled instance and one declined invitation.
Verify
The cancelled instance is free and the rest of the series is busy after sync - API keyfree
Why The tool syncs as you, through a client you created, and the unverified cap of a hundred users is why it stays personal.
Get it Create a project and an OAuth client in the provider's console, add the narrowest calendar scope, and add yourself as a test user. open ↗
Verify
The consent screen grants the scope and an incremental sync completes - have readyfree with a tunnel, pennies on a host
Why Push channels deliver to a URL the provider can reach, and without one the tool is polling only, which is acceptable but should be a decision.
Get it A tunnel to your machine or a tiny relay on a host you control.
Verify
The sync message arrives when a channel is created - decidefree
Why The planner packs into working hours and treats declined and all-day events by rules that are asserted in tests, so they belong decided before phase one.
Get it Write down working hours per day in your zone, whether an all-day event blocks the day, and whether a declined invitation is free time.
Verify
All three are in the configuration and named in the README
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- calendars(id, provider, account_ref, scopes_granted, sync_token, last_full_sync_at, last_incremental_at, channel_id, channel_expires_at) - events(id, calendar_id, external_id, starts_at, ends_at, all_day, status, recurrence_json, is_ours, updated_at) - tasks(id, title, estimate_minutes, remaining_minutes, deadline_at, priority, project_id, state) - blocks(id, task_id, external_event_id, starts_at, ends_at, placed_at, pinned, at_risk) · the join between a task and an event the tool created - working_hours(day_of_week, start_local, end_local, timezone) - plan_runs(id, at, reason, placed, moved, unplaced, at_risk) - A block links a task to a calendar event identified by stored id, so the planner can move its own blocks freely and never touches a meeting it did not make.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATABASE_PATH | required | ./data/planner.db | Events, tasks, blocks and plan runs. |
CALENDAR_CLIENT_IDsecret | required | a client id from your own project | Your own OAuth client. Unverified, and capped at a hundred users. |
CALENDAR_CLIENT_SECRETsecret | required | a client secret from the same project | Never in the repository. The desktop app holds it in the platform keychain. |
PUSH_CALLBACK_URL | optional | https://relay.example/calendar-push | Where notification channels deliver. Absent means polling only, stated on screen. |
POLL_SECONDS | required | 300 | The backstop. Runs whether or not a channel is live. |
SYNC_STALE_MINUTES | required | 30 | Past this the sync age becomes a warning on screen. |
WORKING_HOURS_PATH | required | ./config/working-hours.json | Per day, in your zone. The planner never places outside them. |
TOUCH_FOREIGN_EVENTS | required | never | Present as a constant. There is no other value. |
The build, in order
Reading one calendar correctly
The local copy matches the server, including after the server invalidates the sync token.
Clear the store and start again; the token is gone and will not come back.
Files
src/sync.tstests/resync-410.test.tsThe series minus its cancellations, not the series.
Files
src/recurrence.tssrc/recurrence.test.tsRead events and write your own is enough for everything here.
Files
src/oauth.ts
done when · tick each as it passeswatch out- Treating the 410 as an error and retrying the same token, or ignoring it and carrying on, leaves a calendar that drifts from the server without anything visibly breaking, until a meeting cancelled last week still blocks a slot. Clear and resync, and keep your own blocks through it by their stored ids.
Tasks as blocks in free time
Tasks are packed into free working time and stay where they were put unless they must move.
Working hours minus every event, recurring ones expanded first.
Files
src/planner.tssrc/planner.test.tsMove what conflicts and nothing else, and say what moved.
Files
src/planner.tsA deadline the plan cannot meet is a fact the person needs today.
Files
src/risk.ts
done when · tick each as it passeswatch out- Re-planning from scratch on every change reshuffles the whole day each time a meeting lands, and the person stops trusting the plan within a week. Keep existing placements stable, move only what conflicts, and show what moved and why.
Writing blocks without touching anyone else's events
Blocks appear in the calendar as events the tool owns, and no foreign event is ever changed.
A rename must not orphan a block or endanger a meeting.
Files
src/calendar-write.tsTested by attempting it, not by intending it.
Files
tests/foreign-events.test.tsMoved by hand means pinned, not overwritten.
Files
src/reconcile.ts
done when · tick each as it passeswatch out- Identifying your own events by title means a renamed block goes invisible and gets recreated as a duplicate, and a coincidence of naming lets the planner move a real meeting. Store the created event id, and treat everything without one as untouchable.
Staying current when the day slips
The calendar is never quietly stale, and an overrun rolls the rest of the day forward.
A lapsed channel looks exactly like a quiet day.
Files
src/push.tstests/channel-expiry.test.tsThe notification says something changed. Go and look.
Files
src/push.tsRemaining minutes, stable placement, affected blocks only.
Files
src/rollover.ts
done when · tick each as it passeswatch out- Assuming a notification carries the changed data, or that a channel lives forever, produces a client that goes stale the day the channel expires while looking entirely normal. Renew before expiry, sync on every notification, poll regardless, and put the sync age on screen.
A booking link, an export and your own OAuth clientproduct builder
One-slot bookings cannot double-book you, the data leaves cleanly, and the OAuth reality is stated.
Check-then-insert double-books when a page is popular.
Files
src/booking.tstests/concurrency.test.tsA task block is busy time too.
Files
src/availability.tsSo the plan is not trapped in this application.
Files
src/export.ts
done when · tick each as it passeswatch out- Computing availability for the booking page from the calendar alone offers a slot a task block already occupies, and check-then-insert double-books when two people click at once. Use the planner's availability and reserve inside the booking transaction.
That is the whole plan for Rise Calendar. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Multi-calendar conflict handling.
- Team routing and round robin.
- A hosted multi-user service.
- Cross-device notifications.
- multi-calendar conflict handling
- race-free booking under load
- team routing and round robin
- reliable reminders and timezone edge cases
- calendar-provider edge cases and timezone correctness
- A focus mode that shows only the current block and the next one, which is what the day actually needs
- A weekly review of at-risk tasks and estimate accuracy, computed from plan runs
- A second calendar in read-only mode, once ownership and resync are provably correct for one
Need the files? The project pack on the verdict page hands your agent the whole brief · more scheduling.