Skip to main content

Tasks and Calendar planning

Calendar and Tasks keep cross-product API calls on the active browser origin so host-only app cookies remain available. Their beforeFiles rewrites forward only the other product’s API families, before Tasks’ API catch-all can intercept them. The source proxy refreshes the host session and forwards its signed bearer token; Calendar routes explicitly accept Calendar and Tasks audiences while retaining workspace membership and permission checks. Do not route these APIs through Web: Web no longer owns the Calendar events or Tasks endpoints. Tasks’ session verifier also accepts Calendar for routes that explicitly accept Tasks, including labels, projects, task detail dependencies, and workspace task preferences. It preserves required scopes and does not widen unrelated app audiences. Calendar forwards /api/v1/users/me/workspaces/:wsId/configs/* to Tasks so those preferences use the verified planning actor and membership guard. Collection rewrites must have an exact path before their /:path* variant. Vercel can retain a trailing slash when the wildcard is empty, causing the owning app to return a 308 to the same browser URL and loop. Verify both collection and nested resource requests on the deployed host. Both satellite apps expose local planning views without changing workspace or requiring a cross-app login handoff: The shared sidebar retains locale and personal/internal workspace aliases, including on mobile. Calendar also serves /tasks/boards, /tasks/boards/{boardId}, and /tasks/{taskId} within the workspace. Task views hide the calendar-only mini-month sidebar to leave more room for task work. Tasks’ embedded calendar uses the shared scheduling, connection, event, and task controls and enforces manage_calendar. Each host resolves its own app-session actor. Keep the task route context and dialog manager prefix aligned: Tasks uses an empty prefix, while Calendar and Web use /tasks. Tasks already owns a dialog provider, so its embedded calendar disables the additional dialog wrapper. When validating this integration, check both locales, personal and team workspace links, board/task detail return paths, and mobile navigation with the sidebar closed. Mutations continue through the existing protected product APIs.

Overview

apps/calendar and the Calendar experience inside apps/web are paired Calendar hosts. They must stay 1:1 for product features, data behavior, mutations, permissions, and user-facing workflow updates.
  • Calendar UI and logic should live in shared packages, primarily @tuturuuu/ui/calendar-app/*, with app route files acting as thin auth and workspace-context wrappers.
  • Task-aware Calendar controls (task creation, task scheduling, habits, and task time tracking) live in @tuturuuu/tasks-ui/calendar/*. The generic Calendar shell accepts those controls through typed component seams, keeping @tuturuuu/ui independent from task-only code and preserving narrow Turbo rebuilds.
  • apps/calendar owns its standalone workspace shell, local /login, and local /verify-token completion route.
  • apps/web /{wsId}/calendar renders the same shared Calendar product surface inside the normal platform dashboard shell.
  • Dashboard navigation in apps/web should link to the local /{wsId}/calendar route, not to the standalone Calendar origin.

Auth Model

Calendar does not create local Supabase Auth sessions. It uses central apps/web login plus a Calendar app-session cookie:
  1. /login in apps/calendar normalizes a safe next path.
  2. If both the Calendar app-session cookie and Web-issued app-session cookie are already present, /login redirects to that local next path immediately.
  3. Otherwise /login redirects to apps/web /login with a return URL pointing back to Calendar /verify-token?nextUrl=....
  4. /verify-token posts the handoff token to the Calendar-local verifier, which validates through central Web and sets host-local app-session cookies.
Calendar proxy responses should clear stale sb-*-auth-token cookies. A valid Calendar session is represented by Tuturuuu app-session cookies, not by a local Supabase browser session.

API Ownership

apps/calendar owns the Calendar product APIs. Its local handlers cover events and calendars, categories and preferences, hours and default sources, scheduling and sync, Calendar connections, Google and Microsoft provider OAuth, Calendar media/generation helpers, and Calendar-owned cron work. Host-local auth/session and build-info routes are local as well.
  • Exact local handlers win before the fallback /api/:path* rewrite. The fallback remains for cross-product routes that Calendar uses but does not own, including workspace encryption and task, board, habit, and time-tracking families; typed task helpers may target the Tasks app directly.
  • Central platform login, social-provider callbacks, and cross-app token issuance remain Web-owned. Calendar-provider OAuth initiation and callbacks are Calendar-local, even though their configured redirect origins retain Web compatibility for existing provider registrations.
  • Calendar-only routes use targetApp: 'calendar'. Shared cross-product routes must name the exact accepted app audiences, such as targetApp: ['calendar', 'tasks'], rather than accepting generic satellite sessions.
  • Keep workspace membership checks and Calendar/task permissions on the owning route before admin-backed reads or writes. The Calendar proxy guard and app-session refresh do not replace route-level authorization.
  • Prefer typed packages/internal-api helpers. Browser calls remain same-origin so Calendar-local handlers are selected first; server calls to a different owning app must use its configured API base URL with forwarded authentication.

OAuth Token Safety

Calendar OAuth credentials live in calendar_auth_tokens. RLS restricts selects to user_id = auth.uid(), but admin/service-role reads bypass RLS. Calendar server pages must therefore:
  • Check manage_calendar before loading calendar integration state.
  • Scope token reads to both ws_id and the authenticated user_id.
  • Never pass access_token or refresh_token into client components. SSR props should use the shared fetchUserWorkspaceCalendarGoogleTokenForClient() helper from @tuturuuu/utils/calendar-auth-token, which projects only connection metadata (id, account email/name, provider, active state, expiry).
  • Keep token refresh and provider API calls on Calendar-local server routes; never expose provider credentials to client components.
When adding a new Calendar host route, reuse the helper instead of select('*') on calendar_auth_tokens. When adding new Calendar UI that needs protected data, update the shared Calendar component or helper first, then wire both host wrappers in the same change. Extend the route family in its owning app and expose a typed packages/internal-api seam instead of creating a host-local product API fork or adding direct client Supabase reads.

Calendar OAuth And Connections

Google Calendar OAuth initiation and callback handlers are owned by apps/calendar. The local /api/v1/calendar/auth route builds its callback URL from a browser-safe configured origin and ignores wildcard listener addresses such as 0.0.0.0 or ::. Existing provider registrations may still configure the central Web origin; if no safe configured origin is available, the fallback origin is https://tuturuuu.com. After Google returns to the callback, the flow always sends the user to the central Web Calendar page at https://tuturuuu.com/{wsId}/calendar?provider=google&connected=true. Valid local development callback URLs such as http://localhost:7803/api/v1/calendar/auth/callback may still be configured through GOOGLE_REDIRECT_URI, but wildcard listener URLs must never be emitted as Google redirect_uri values or browser redirects. Normal Google Calendar connect and reconnect URLs request access_type=offline and include_granted_scopes=true, but they must not force prompt=consent by default. Google may return a refresh token only during the first authorization, so the callback must preserve an existing stored refresh_token when a reconnect returns only a new access token. If neither the callback nor the existing token row has a refresh token, the flow must fail before saving an active Google connection because provider sync cannot refresh offline. Standalone apps/calendar also exposes the shared Calendar connections manager inside its settings dialog under Calendar -> Integrations. It should fetch the initial connection state through @tuturuuu/internal-api/calendar and render the shared Calendar connections UI inside CalendarSyncProvider rather than forking host-local connection controls.

CI And Deployment

Calendar deploys through dedicated GitHub Actions workflows, not Vercel-owned GitHub auto-builds:
  • Preview deployments use .github/workflows/vercel-preview-calendar.yaml.
  • Production deployments use .github/workflows/vercel-production-calendar.yaml.
  • The Vercel project id is provided through the environment-scoped VERCEL_CALENDAR_PROJECT_ID secret.
  • apps/calendar/vercel.json must keep git.deploymentEnabled and github.enabled set to false so GitHub Actions builds with vercel build and deploys prebuilt artifacts with vercel deploy --prebuilt.
Keep Calendar cron definitions in apps/calendar/vercel.json; disabling Vercel GitHub builds must not remove the app-owned cron schedules.

Provider Sync

Calendar owns the provider-sync schedule in apps/calendar/vercel.json (*/15 * * * *). The cron wrapper invokes Calendar’s workspace sync handler locally, preserving cron authentication, running locks, cooldown behavior and calendar_sync_dashboard audit rows. It must not send sync requests to the former Web owner through INTERNAL_WEB_API_ORIGIN or a protected preview URL. Both Tasks and Calendar show Sync needs attention beside the calendar picker when a sync stalls, data becomes stale, a provider needs reconnection, or status cannot be loaded. Open it for a persistent explanation and a Sync now, Check again, or provider-specific Reconnect action. Stored events remain visible during provider outages. Ordinary access-token expiry is not itself a reconnection failure: Google refreshes those tokens automatically. Sync health polls every 30 seconds (every 5 seconds during a run or cooldown). A running entry older than five minutes is stalled; a successful sync older than 45 minutes is stale. Partial provider failures are recorded as failed runs rather than successful syncs. Successful runs use the database’s completed status; completion writes must be checked so a constraint failure cannot leave a run stuck at running. Provider rate limits and transient outages offer retry, while authentication failures offer reconnection. Operators should inspect the Calendar cron execution and dashboard run when retries continue to fail.

Recovering individual calendar failures

The warning names each failed Google calendar after a sync attempt. Pause sync for this calendar turns off only its inbound import; it preserves its saved events and visibility, and does not disconnect the account or affect other calendars. This action is available during the manual retry cooldown. Restore provider access when applicable, then re-enable inbound sync from the calendar’s sync controls. Sync now retries the remaining enabled calendars. The sync handler stores sanitized version-1 failedCalendars diagnostics in the existing calendar_sync_dashboard.error_stack_trace text field. Each entry has only connectionId, calendarName, and a classified code. Never store OAuth credentials, provider response payloads, or headers there. The status API parses this format, filters disabled/paused connections, and returns failedCalendars; it never returns the raw diagnostic field. Older unstructured errors still show an aggregate warning and gain calendar-specific details on the next attempt. Per-calendar failure diagnostics and the pause shortcut currently cover Google inbound sync. Microsoft failures retain account-level reconnect/retry guidance; individual Microsoft calendar failure identification is not yet available.

Views and keyboard support

Calendar views share behavior across Calendar, Tasks, and embedded Web calendars. Use D, 4, W, M, Y, or A for day, four days, week, month, year, or agenda. These page shortcuts are inactive inside editors, menus, and dialogs, during input composition, or alongside Cmd/Ctrl/Alt. Month selection updates the host’s controlled state and is restored from the saved view preference. Month provides event previews and day details; year opens the selected month or day; agenda searches titles and locations across its 30-day window. Event queries must cover the entire year or all 30 agenda days, not just the first day. The event route reads ordered 1,000-row pages so busy years are not truncated by the database response limit. Shared settings list the shortcuts alongside command search, app launcher, and sidebar commands. Verify modified shortcuts in Calendar and Tasks and check typing, modifiers, modal focus, and mobile overflow when changing these views. Global command/app launchers remain available while editing. Calendar view shortcuts are intentionally suppressed throughout editor descendants, including non-editable embedded content, to prevent unexpected view changes while working in an editor.

Two-Way Sync Controls

Calendar sync has two layers of user control:
  • Workspace-user preferences in private.calendar_user_workspace_preferences enable or disable inbound imports, outbound Tuturuuu-to-provider mirroring, the default outbound provider calendar, and the conflict policy.
  • calendar_connections controls each external calendar’s import, outbound write, and provider-delete behavior.
Outbound mirroring is opt-in. When enabled, native Tuturuuu event creates and edits mirror to the selected writable Google or Microsoft connection, and the local event stores provider identity so future updates and deletes propagate externally. Manual or cron sync with direction: "outbound" or "both" may also catch up local-only or previously failed Tuturuuu events in the active sync window. Inbound provider deletes are controlled per connection through sync_delete_enabled. User-initiated deletion of a synced external event still deletes the provider event directly because it is explicit CRUD, not passive provider import cleanup. After adding or changing two-way sync columns, prepare migrations in apps/database; do not run production Supabase push commands from agent sessions. Apply locally with bun sb:up when feasible, then regenerate database types only after the local schema reflects the migration.

Installation and caching

Calendar and Tasks serve their own /serwist/sw.js workers and installable app manifests. Client instrumentation captures the install event before hydration; these hosts explicitly transpile @tuturuuu/ui for that bootstrap entry point. The Install app control uses the browser prompt when available and gives Home Screen instructions otherwise. /offline.html is a public, bilingual fallback that bypasses authentication middleware. These productivity hosts use cacheNavigations: false and staticAssetsOnly: true: private HTML, APIs, and private images are never persisted by the worker. Installation precaches only the fallback, icons, and manifest. Hashed Next assets cache on demand, with at most 160 runtime entries. This avoids downloading the entire application at installation. Loaded data remains available in an open app through memory caches. Calendar snapshots are keyed by workspace and date range and limited to 12 ranges; event lookups pre-index date intervals and memoize at most 400 day results. Private snapshots are not persisted across app restarts. Reopening a workspace, syncing, and saving changes require connectivity; this release does not add a durable offline write queue. The offline indicator communicates that limitation. Verify worker registration, install manifest, cached asset reuse, offline fallback, and the absence of API/private-page entries in Cache Storage on both Calendar and Tasks. Re-run both app builds when changing the worker routes or tracing configuration.