This document explains the architectural decisions and structure of the TimelyOne application.
This application is designed as a self-hosted, single-user calendar platform, not a multi-tenant SaaS. This fundamental decision influences every aspect of the architecture:
- No complex authentication: Optional password auth, assumes trusted environment
- Simplified queries: No need to filter by user in every query
- Direct database access:
getCurrentUser()simply returns the first (and only) user - Reduced overhead: No multi-tenancy abstractions or row-level security
- Better performance: Fewer joins, simpler queries, smaller codebase
- Self-hosted: All data stays on the user's server
- Minimal external calls: Only to Google/Microsoft APIs for calendar sync
- No telemetry: Optional Next.js telemetry can be disabled
- Local storage: All events cached in local PostgreSQL database
Next.js 14+ (App Router)
- Server Components by default for better performance
- Client Components only where needed (forms, interactive UI)
- Built-in API routes for backend logic
- Automatic code splitting and optimization
React 19
- Latest features and performance improvements
- Server Components support
- Concurrent rendering
shadcn/ui + Tailwind CSS 4
- Accessible components built on Radix UI
- Customizable and themeable
- Modern, responsive design
- No runtime CSS-in-JS overhead
Next.js API Routes
- Serverless-ready architecture
- Simple REST endpoints
- Integrated with Next.js middleware
Prisma ORM
- Type-safe database queries
- Automatic migrations
- Connection pooling via PostgreSQL adapter
- Schema versioning
PostgreSQL 14+
- Reliable, proven RDBMS
- JSONB support for flexible event metadata
- Full-text search capabilities (future)
- Excellent performance for single-user scale
User visits app
↓
Middleware checks: User exists?
↓ No
Redirect to /setup
↓
User fills setup form
↓
POST /api/setup
↓
Create user in database
↓
Redirect to main app
User visits app
↓
Middleware checks: User exists?
↓ Yes
Continue to requested page
↓
Page loads (Server Component)
↓
getCurrentUser() fetches user
↓
Fetch user's events/data
↓
Render page
User connects Google Calendar
↓
OAuth flow → Store tokens
↓
Background job: Fetch events
↓
Parse and store in database
↓
Display in unified view
Purpose: Enforce setup completion before allowing app access
Logic:
if (path is public) return next()
if (user exists) return next()
else redirect to /setupWhy needed: Ensures single-user is created before any app functionality
Purpose: Get the current (and only) user from database
Implementation:
export async function getCurrentUser() {
return await prisma.user.findFirst()
}Why simple: Single-user mode means first user IS the current user
Components:
page.tsx: Setup page layoutsetup-form.tsx: Form with validation/api/setup/route.ts: API endpoint to create user
Flow:
- Display form
- Validate input
- Create user in database
- Redirect to main app
Current State: Week view with day/week/month switcher
Future Enhancement:
- Fetch events from database
- Display events in timeline
- Handle click to create/edit events
- Show conflicts visually
Even though this is single-user, we maintain user relationships in the schema:
Why?
- Flexibility: Easy to expand to multi-user if needed
- Clarity: Explicit relationships make code more maintainable
- Standard practice: Familiar pattern for developers
Tradeoff: Slight performance overhead vs. massive maintainability gain
users
- Single row in production
- Stores name, email
- Created by setup wizard
calendar_connections
- One row per connected calendar
- Stores OAuth tokens (encrypted in production)
- Links to user (always same user)
events
- Cached calendar events
- Stores all event metadata
- Links to calendar_connection and user
Assumptions:
- Application runs in trusted environment
- Only owner has network access (VPS, home server, etc.)
- Optional: Reverse proxy handles authentication (nginx, Caddy)
Security Measures:
- Database: Not exposed to internet
- API Routes: Server-side only, no client-side access to tokens
- OAuth Tokens: Stored server-side, never sent to client
- Environment Variables: Sensitive config in
.env(not committed)
User can set password during setup:
- Used for future multi-user expansion
- Currently not enforced
- Recommended if exposing to internet without reverse proxy
Production Recommendation: Use reverse proxy (nginx, Caddy) with HTTP Basic Auth or OAuth proxy
[Internet] → [Reverse Proxy] → [Next.js App] → [PostgreSQL]
(optional) (port 3000) (port 5432)
Benefits:
- Automatic container networking
- Volume persistence for database
- Easy backup/restore
- One-command deployment
[Internet] → [nginx/Caddy] → [PM2 → Next.js] → [PostgreSQL]
(SSL, port 443) (port 3000) (localhost:5432)
Benefits:
- More control over configuration
- Traditional VPS setup
- Can use system services
- No user filtering overhead: Queries don't need complex WHERE clauses
- Simplified caching: Cache entire dataset, not per-user
- Direct database access: No connection pooling complexity
- Smaller codebase: Less code = faster execution
- Indexes: On user_id, start_time, end_time for fast event queries
- Connection pooling: Via Prisma PostgreSQL adapter
- Query optimization: Server Components allow efficient data fetching
For typical single-user load (< 10,000 events):
- Page load: < 500ms
- Event sync: < 5s
- Database queries: < 50ms
Google Calendar:
src/app/api/auth/google/callback
src/lib/calendar/google.ts
Microsoft Teams:
src/app/api/auth/microsoft/callback
src/lib/calendar/microsoft.ts
Background Jobs:
- Cron job or Next.js API route with timeout
- Incremental sync (only recent changes)
- Webhook support (Google push notifications)
Conflict Detection:
- Query for overlapping time ranges
- Check across all calendar connections
- Display warnings in UI
Implementation:
src/app/booking/[username]/page.tsx → Public booking page
src/api/booking/available → Check availability
src/api/booking/create → Create appointment
Availability Calculation:
- Fetch all events for date range
- Calculate free slots based on working hours
- Apply buffer times
- Return available slots
- Server Components first: Use Client Components only when needed
- Type safety: Leverage Prisma types throughout
- Single-user aware: Don't add multi-user complexity
- Test self-hosted: Use Docker Compose for testing
src/
├── app/ # Pages and API routes
│ ├── (pages)/ # Page routes
│ └── api/ # API endpoints
├── components/ # React components
│ ├── ui/ # shadcn components
│ └── ... # Custom components
└── lib/ # Utilities and business logic
├── prisma.ts # Database client
├── user.ts # User utilities
└── calendar/ # Calendar integrations (future)
- Server-first: Fetch data in Server Components
- Type everything: Use TypeScript strictly
- Error handling: Always handle database errors gracefully
- Logging: Use console.error for server-side errors
- Comments: Document complex logic, not obvious code
Setup wizard loops:
- Check database connection
- Verify migrations ran successfully
- Check API route errors in console
Middleware not working:
- Verify matcher config in middleware.ts
- Check environment variables loaded
- Test with direct /setup navigation
Database connection fails:
- Verify PostgreSQL is running
- Check DATABASE_URL format
- Ensure migrations are applied
TimelyOne implements a comprehensive dark/light theme system using next-themes:
Components:
ThemeProvider(src/components/theme-provider.tsx): Wraps application in root layoutThemeToggle(src/components/theme-toggle.tsx): UI control for theme switching- CSS Variables in globals.css: Define colors for both themes
Configuration:
<ThemeProvider
attribute="class" // Uses class-based theme switching
defaultTheme="system" // Respects OS preference by default
enableSystem // Allow system theme detection
disableTransitionOnChange // Prevent flash during theme change
>Light Mode: Uses bg-{color}-100 backgrounds with darker text
Dark Mode: Uses dark:bg-{color}-900/40 with lighter text
Calendar event colors adapt automatically:
// From calendar-utils.ts
export const CALENDAR_COLORS = {
blue: {
bg: "bg-blue-100 dark:bg-blue-900/40",
border: "border-blue-500 dark:border-blue-400",
text: "text-blue-700 dark:text-blue-300",
hover: "hover:bg-blue-200 dark:hover:bg-blue-800/50",
},
// ... other colors
}Benefits:
- System-aware: Automatically follows OS dark mode preference
- Persistent: Theme choice saved in localStorage
- Smooth transitions: No flash of unstyled content
- Full coverage: All components styled for both themes
- Complete theme system with light/dark/system modes
- Adaptive calendar event colors
- Theme toggle in navigation bar
- Persistent theme preference
- Event colors adapt to theme (light/dark mode)
- Improved contrast and readability
- Visual conflict warnings
- Drag-and-drop event rescheduling
- Personal scheduling pages (
/book/[slug]) - Availability calculation with conflict checking
- Timezone-aware bookings
- Google Meet auto-creation
- Guest information collection
- Optional password-based login
- Bcrypt password hashing
- Session management with httpOnly cookies
- Public routes for booking pages
- Timezone auto-detection
- Loading states and feedback
- Error handling and validation
- Responsive design improvements
Frontend:
- Next.js 16.0.3 (was 14+) - Latest App Router features
- React 19.2.0 (was 19) - Concurrent rendering improvements
- Tailwind CSS 4 - Latest version with new features
- next-themes 0.4.6 - Theme management system
Backend:
- Prisma 7.0.0 (was 5.x) - Latest ORM version
- Composio SDK @composio/core - Managed OAuth for calendars
- bcryptjs - Password hashing utility
UI Components:
- shadcn/ui - Based on Radix UI primitives
- @dnd-kit - Drag and drop functionality
- lucide-react - Icon library
- sonner - Toast notifications
- react-hook-form + zod - Form validation
Composio Platform:
- Eliminates need for Google Cloud Console setup
- Automatic OAuth token management
- Multi-provider calendar support (Google, Microsoft)
- Simplified authentication flow
Calendar Sync:
- Incremental sync using sync tokens
- Only fetches changed events (efficient)
- Bi-directional synchronization
- Automatic calendar discovery
Theme Components:
components/theme-provider.tsx- Next-themes integrationcomponents/theme-toggle.tsx- UI toggle button
Calendar Components:
components/calendar/DayView.tsx- Hourly timeline viewcomponents/calendar/WeekView.tsx- 7-day grid viewcomponents/calendar/MonthView.tsx- Traditional month gridcomponents/calendar/EventCard.tsx- Event display componentcomponents/calendar/DraggableEventCard.tsx- Drag-and-drop eventscomponents/calendar/AllDayEventsRow.tsx- All-day event display
Connection Management:
components/connections/account-accordion.tsx- Calendar accounts UIcomponents/connections/calendar-list.tsx- Calendar selectioncomponents/connections/connection-header.tsx- Header with sync controls
Calendar Sync Service (lib/services/calendar-sync-service.ts):
- Handles incremental event syncing
- Uses Google sync tokens for efficiency
- Manages event creation/update/deletion
- Processes calendar-level sync operations
- Server-first: Fetch data in Server Components
- Type everything: Use TypeScript strictly with latest version
- Error handling: Always handle database errors gracefully
- Logging: Use console.error for server-side errors
- Comments: Document complex logic, not obvious code
- Theme-aware: Use Tailwind dark: variants for all UI
- Performance: Use React 19 concurrent features wisely
- Security: Never expose OAuth tokens to client
When contributing, remember:
- This is a single-user application
- Keep it simple and maintainable
- Prioritize privacy and self-hosting
- Test with Docker Compose
- Document deployment-critical changes