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/uiindependent from task-only code and preserving narrow Turbo rebuilds. apps/calendarowns its standalone workspace shell, local/login, and local/verify-tokencompletion route.apps/web /{wsId}/calendarrenders the same shared Calendar product surface inside the normal platform dashboard shell.- Dashboard navigation in
apps/webshould link to the local/{wsId}/calendarroute, not to the standalone Calendar origin.
Auth Model
Calendar does not create local Supabase Auth sessions. It uses centralapps/web login plus a Calendar app-session cookie:
/logininapps/calendarnormalizes a safenextpath.- If both the Calendar app-session cookie and Web-issued app-session cookie are
already present,
/loginredirects to that localnextpath immediately. - Otherwise
/loginredirects toapps/web /loginwith a return URL pointing back to Calendar/verify-token?nextUrl=.... /verify-tokenposts the handoff token to the Calendar-local verifier, which validates through central Web and sets host-local app-session cookies.
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 astargetApp: ['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-apihelpers. 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 incalendar_auth_tokens. RLS restricts
selects to user_id = auth.uid(), but admin/service-role reads bypass RLS.
Calendar server pages must therefore:
- Check
manage_calendarbefore loading calendar integration state. - Scope token reads to both
ws_idand the authenticateduser_id. - Never pass
access_tokenorrefresh_tokeninto client components. SSR props should use the sharedfetchUserWorkspaceCalendarGoogleTokenForClient()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.
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 byapps/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_IDsecret. apps/calendar/vercel.jsonmust keepgit.deploymentEnabledandgithub.enabledset tofalseso GitHub Actions builds withvercel buildand deploys prebuilt artifacts withvercel deploy --prebuilt.
apps/calendar/vercel.json; disabling Vercel
GitHub builds must not remove the app-owned cron schedules.
Provider Sync
Calendar provider sync is scheduled byapps/web cron, not Trigger.dev. The
shared source of truth is apps/web/cron.config.json; calendar-provider-sync
must stay at */15 * * * * and apps/web/vercel.json must be regenerated from
that file with node scripts/sync-web-crons.js.
The cron wrapper at /api/cron/calendar/provider-sync must keep calling
/api/v1/workspaces/:wsId/calendar/sync through INTERNAL_WEB_API_ORIGIN when
available. The workspace sync route owns Google and Microsoft fan-out, running
locks, cron cooldown behavior, and calendar_sync_dashboard audit rows. Do not
add provider-specific fetch logic or Trigger.dev scheduled calendar jobs outside
that route family.
Two-Way Sync Controls
Calendar sync has two layers of user control:- Workspace-user preferences in
private.calendar_user_workspace_preferencesenable or disable inbound imports, outbound Tuturuuu-to-provider mirroring, the default outbound provider calendar, and the conflict policy. calendar_connectionscontrols each external calendar’s import, outbound write, and provider-delete behavior.
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.