Self-hosted scheduling assistant via Gmail and Calendar.
cc-assistant is a Next.js application that integrates with Google Gmail and Calendar APIs to provide scheduling automation. It uses lightweight, fetch-based API wrappers — no heavyweight SDK dependencies.
- Runtime: Bun
- Framework: Next.js 16 (App Router)
- Language: TypeScript
- Styling: Tailwind CSS v4 + shadcn/ui
- Auth: Better Auth with Google OAuth2
- Bun v1.3+
- Google Cloud project with OAuth2 credentials
- Gmail and Calendar API scopes enabled
# Install dependencies
bun install
# Copy environment variables
cp .env.example .env
# Fill in your Google OAuth credentials in .env| Variable | Description |
|---|---|
GOOGLE_CLIENT_ID |
Google OAuth client ID |
GOOGLE_CLIENT_SECRET |
Google OAuth client secret |
CC_AUTHORIZED_DOMAINS |
Comma-separated allowed email domains |
CC_AUTHORIZED_EMAILS |
Comma-separated allowed email addresses |
CC_CALENDAR_ID |
Google Calendar ID (defaults to primary) |
bun devbun testapp/ # Next.js App Router pages and layouts
components/ # React components (UI, chat, dashboard)
lib/
google/ # Fetch-based Google API wrappers
gmail.ts # Gmail: threads, messages, drafts, search
calendar.ts # Calendar: events, free/busy
oauth.ts # OAuth2 token exchange and refresh
authorized.ts # Email authorization checks
fetch.ts # Shared authenticated fetch wrapper
types.ts # TypeScript types for all API responses
constants.ts # API base URLs and OAuth scopes
gmail.ts # Re-exports from google/gmail
calendar.ts # Re-exports from google/calendar
tests/ # Unit tests
All Google API wrappers use plain fetch and return a discriminated union — they never throw:
type GoogleApiResult<T> =
| { ok: true; data: T }
| { ok: false; status: number; error: string };listMessages— search messages by querylistThreads— search/list threadsgetThread— fetch a full thread with all messagesgetMessage— fetch a single messagecreateDraft— create a draft reply in a threadlistDrafts— list drafts with optional querysendDraft— send a previously created draft
listEvents— list events in a time rangegetEvent— fetch a single eventcreateEvent— create a new event with attendeesqueryFreeBusy— check availability across calendars