A full-stack calendar and task manager: natural-language input, four calendar views, a kanban board, and two-way Google Calendar sync.
Live · Features · Architecture · Testing · Getting Started
The landing page, live at taskflow-calendar.vercel.app.
Taskflow Calendar is a task manager and a calendar that are the same object: type a task in plain words and it lands on the calendar. Type "Email vendor about invoice friday 4pm urgent" and the smart input pulls out the date, time, and priority as you go, no date picker required. The app ships as one Vercel project: an Astro landing page, a React SPA, exactly two serverless functions, and Postgres. It syncs both ways with Google Calendar, so an edit made in either place shows up in both.
- Recurring events with RRULE, per-occurrence exceptions, and per-event colors
- Conflict detection across calendars when scheduling
- Attachments on tasks: upload, with image and PDF preview in place
- Settings: profile, preferences, data export (JSON download), account deletion
- Refresh-token rotation, Google sign-in, and password reset by email
- Light and dark themes, responsive down to phone widths
- Frontend: Vite 5, React 19, TypeScript 5.8, Tailwind CSS v4, Radix/shadcn primitives. Zustand for client state, TanStack Query for server state with optimistic updates and rollback.
- API: Two Vercel functions.
api/[...route].tsdispatches 38 routes to one handler per endpoint inapi/_handlers/, each behind the same middleware chain (CORS, request ID, rate limiting, JWT auth, Zod validation).api/google/[...route].tshandles Calendar sync: 8 routes, including the webhook receiver and a dailycron/renewcron. - Database: Plain SQL over
pg, no ORM. 10 migrations inlib/config/migrations/, applied transactionally bynpm run db:migrate, recorded inschema_migrations. - Files and email: Vercel Blob for attachments, Resend for password-reset mail.
- CI: GitHub Actions, three jobs.
checksruns lint, typecheck, the no-DB suites, and both builds.backend-dbboots Postgres 16, runs migrations, then the real-database suites (backend workspace, L2, L3, Google sync).e2eboots the full stack and runs Playwright in parallel. A separate scheduled workflow reconciles production sync every 15 minutes.
How Google sync works
A watch channel pushes a webhook notification on every remote change, which triggers an incremental pull. If Google returns HTTP 410, sync falls back to a full resync. Local edits queue in an outbox and drain to Google, and both sides converge through a per-field three-way merge, so a local edit and a remote edit to different fields on the same event both survive. The watch channel renews daily, and a GitHub Actions workflow reconciles every connected account every 15 minutes as a backstop.
The entry chunk is 72 KB gzip. FullCalendar, the NLP parser, the emoji picker, and the PDF viewer are code-split and lazy-loaded, so each loads only when a user reaches that feature. Fonts are self-hosted, 133 KB across four woff2 files.
| Layer | What it exercises | Command | Tests |
|---|---|---|---|
| L1 frontend | components, hooks, stores, the smart-input parsers (chrono, compromise, priority) | npm run test:frontend:run |
826 |
| L1 backend | services and middleware with the DB mocked, router dispatch tables | npm run test:backend:run |
636 |
| Shared | Zod schemas and utilities used by both sides | npm run test:run --workspace=packages/shared |
22 |
| L2 services | TaskService, EventService, TaskListService, and cross-user IDOR checks against a real Postgres | npm run test:l2 |
80 |
| L3 handler contracts | the actual api/[...route].ts and api/google/[...route].ts dispatchers mounted on an Express adapter: real routing, middleware, and SQL |
npm run test:l3:run |
107 |
| Google sync | the sync engine (channels, outbound writes, merge) against a real Postgres with a fake Google client | gated on GOOGLE_SYNC_TEST_DB_URL |
33 |
| Backend workspace | auth SQL round-trips, refresh-token rotation, schema and access-control checks | npm run test:run --workspace=packages/backend |
89 |
| L5 browser E2E | signup, login, reset-password round trip, task and event CRUD, recurrence, kanban drag, settings, all driven through a real Chromium | npm run test:e2e |
21 |
Total: 1,814 cases. The frontend layer includes the L4 optimistic-update suites, which assert the TanStack Query cache rolls back when a mutation fails, and L5 drives the built app through a real Chromium browser against a real backend and database. The real-database layers gate on env vars (L2_TEST_DATABASE_URL, L3_DATABASE_URL, GOOGLE_SYNC_TEST_DB_URL) and skip cleanly when unset, so npm run test:backend:run passes without a database. CI provides the databases: backend-db runs L2, L3, sync, and the backend workspace suite, and a separate e2e job runs Playwright in parallel so it never slows the fast checks.
Prerequisites: Node 22 and Docker.
git clone https://github.com/ShreeChaturvedi/taskflow-calendar.git
cd taskflow-calendar
npm install
docker compose up -d postgres
npm run db:migrate
npm run devnpm run dev starts the Vite dev server and a local Express API together. The app is at http://localhost:5180/app and the API at http://localhost:3001 (Vite proxies /api to it). No env file is required for this flow: the dev API and the migration runner both default to the Docker Postgres URL.
The landing page is its own workspace: npm run dev:landing.
Copy .env.example to .env.local and fill in what you need:
GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,VITE_GOOGLE_CLIENT_IDfor Google sign-in and Calendar syncRESEND_API_KEY,FROM_EMAILfor password-reset emailBLOB_READ_WRITE_TOKENfor file attachments
| Command | Description |
|---|---|
npm run dev |
Vite (5180) plus the local API (3001) |
npm run build |
shared package, landing, SPA, and backend, in order |
npm run db:migrate |
apply pending SQL migrations, transactionally |
npm run test:frontend:run / test:backend:run |
the no-DB suites |
npm run lint |
ESLint across the repo |
The rest of the scripts, including the real-database suites, individual builds, and Docker helpers, are in package.json.
One Vercel project. vercel.json chains the build (shared, then landing into dist/, then the SPA into dist/app), declares the two functions, the daily channel-renewal cron, and the /app rewrites. Set DATABASE_URL (Neon), JWT_SECRET, and whichever optional keys above you use.
Project structure
taskflow-calendar/
├── src/ # React SPA, served at /app
│ ├── components/ # calendar, tasks, kanban, smart-input, dialogs, ui
│ ├── hooks/ # TanStack Query hooks, shortcuts, Google sync
│ └── stores/ # Zustand stores
├── landing/ # Astro landing page, served at /
├── api/
│ ├── [...route].ts # catch-all Vercel function: 38 routes
│ ├── google/[...route].ts # second function: Calendar sync, webhook, cron
│ └── _handlers/ # one handler file per endpoint
├── lib/ # services, middleware, Google sync engine
│ └── config/migrations/ # 10 plain-SQL migrations
├── packages/
│ ├── shared/ # types and Zod schemas shared by both sides
│ └── backend/ # auth services (JWT, refresh rotation, OAuth)
├── test/l3/ # L3 handler-contract suite (Express adapter)
├── scripts/ # dev API server, migration runner
└── .github/workflows/ # CI and the sync reconcile cron
MIT, see LICENSE. Read CONTRIBUTING.md before opening a PR.
Built by Shree Chaturvedi.











