A self-hosted Calendly alternative for single-user scheduling. Built with SvelteKit, Supabase, and Google Calendar API.
- Single-user scheduling - Designed for individuals who want to share their availability
- Google Calendar integration - Syncs with multiple Google accounts to check availability
- Magic link authentication - Secure, passwordless admin access
- Customizable availability - Weekly schedules with date overrides
- Multiple event types - Create different meeting types (30 min call, 1 hour consultation, etc.)
- Booking management - View, filter, and cancel bookings
- Automatic calendar events - Creates Google Calendar events on booking
- Cancellation flow - Guests can cancel their own bookings
- Frontend: SvelteKit 2 + Svelte 5 + Tailwind CSS 4
- Database: Supabase (PostgreSQL + Auth)
- Calendar: Google Calendar API
- Hosting: Netlify
- Email: Resend (optional, for confirmations)
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Public Pages │ │ Admin Pages │ │ Google Calendar│
│ (Booking Flow) │ │ (Management) │ │ API │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
└───────────┬───────────┴───────────────────────┘
│
┌──────┴──────┐
│ SvelteKit │
│ Server │
└──────┬──────┘
│
┌──────┴──────┐
│ Supabase │
│ (DB+Auth) │
└─────────────┘
git clone <your-repo>
cd sascal
npm install- Create a new project at supabase.com
- Run the migration in
supabase/migrations/001_initial_schema.sqlvia the SQL Editor - Copy your project URL and keys from Settings > API
- Go to Google Cloud Console
- Create a new project (or select existing)
- Enable the Google Calendar API
- Configure OAuth consent screen
- Create OAuth 2.0 credentials (Web application)
- Add authorized redirect URI:
http://localhost:5173/api/google/callback
cp .env.example .envEdit .env with your values:
# Supabase
PUBLIC_SUPABASE_URL=https://your-project.supabase.co
PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
# Owner (single user allowed to access admin)
OWNER_EMAIL=your@email.com
# Google OAuth
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
# Encryption (generate with: openssl rand -hex 32)
TOKEN_ENCRYPTION_KEY=your-32-byte-hex-key
# Resend Email (optional)
RESEND_API_KEY=your-resend-api-keynpm run devVisit http://localhost:5173
src/
├── lib/
│ ├── availability.ts # Slot calculation algorithm
│ ├── crypto.ts # Token encryption (AES-256-GCM)
│ ├── google/
│ │ ├── oauth.ts # OAuth flow
│ │ └── calendar.ts # Calendar API helpers
│ └── supabase/
│ ├── client.ts # Browser client
│ ├── server.ts # Server client
│ └── types.ts # Database types
├── routes/
│ ├── +page.svelte # Homepage (list event types)
│ ├── [eventTypeSlug]/ # Public booking page
│ ├── confirm/[bookingId]/ # Booking confirmation
│ ├── cancel/[bookingId]/ # Cancellation page
│ ├── auth/
│ │ ├── login/ # Magic link login
│ │ ├── callback/ # Auth callback
│ │ └── unauthorized/ # Access denied
│ ├── admin/
│ │ ├── +page.svelte # Dashboard
│ │ ├── event-types/ # Manage event types
│ │ ├── availability/ # Set weekly schedule
│ │ ├── calendars/ # Connect Google accounts
│ │ ├── bookings/ # View/manage bookings
│ │ └── settings/ # General settings
│ └── api/
│ ├── google/ # OAuth endpoints
│ └── availability/ # Slot calculation API
└── hooks.server.ts # Auth middleware
| Table | Description |
|---|---|
owner_settings |
Timezone, buffer times, booking windows |
google_accounts |
Connected Google accounts (encrypted tokens) |
availability_windows |
Weekly recurring hours |
event_types |
Meeting types |
bookings |
Scheduled meetings |
date_overrides |
Block specific dates or set custom hours |
When calculating available slots for a date:
- Get weekly availability windows for that day of week
- Check for date overrides (blocked or custom hours)
- Fetch busy times from ALL connected Google Calendars
- Fetch existing bookings from database
- Generate 15-minute slots within availability windows
- Filter out slots that:
- Conflict with busy times (including buffers)
- Don't meet minimum notice requirement
- Extend past max advance days
- Return available slots
| Route | Purpose |
|---|---|
/admin |
Dashboard with stats and upcoming bookings |
/admin/event-types |
Create/edit meeting types |
/admin/availability |
Set weekly schedule and date overrides |
/admin/calendars |
Connect/disconnect Google accounts |
/admin/bookings |
View and cancel bookings |
/admin/settings |
Timezone, notice periods, buffers |
- Token encryption: Google OAuth tokens are encrypted with AES-256-GCM before storage
- Single-user auth: Only the configured OWNER_EMAIL can access admin
- Race condition protection: Slot availability is re-checked before booking
- Row Level Security: Supabase RLS policies on all tables
- Push to GitHub
- Connect repo to Netlify
- Set build command:
npm run build - Set publish directory:
build - Add all environment variables
- Update Google OAuth redirect URI to production URL
npm run dev # Start dev server
npm run build # Build for production
npm run preview # Preview production build
npm run check # TypeScript checkMIT