AI-assisted scheduling built with Next.js 16, shadcn/ui, Drizzle ORM, and Google Calendar. This guide explains how to run the project locally and how to connect the Google Calendar API through Google Cloud Platform.
- Node.js 20+
- pnpm 9+ (project uses pnpm by default)
- PostgreSQL 14+ (local Docker/Postgres service or managed instance)
- Google Cloud project with Calendar API enabled
git clone <repo-url>
cd meetme
pnpm installCreate a .env.local file at the project root:
cp .env.example .env.local # create one manually if the template does not existPopulate it with the values your environment needs. Example configuration:
# App
NEXTAUTH_URL=http://localhost:3000
AUTH_SECRET=your-generated-secret
# Database
DATABASE_URL=postgres://username:password@localhost:5432/meetme
DATABASE_SSL=false
# Google OAuth / Calendar
GOOGLE_CLIENT_ID=your-google-oauth-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-google-oauth-client-secret
# Email (SMTP)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=example@gmail.com
SMTP_PASSWORD=app-password-or-tokenℹ️ Generate
AUTH_SECRETwithopenssl rand -base64 32.
Run migrations after the database is reachable:
pnpm db:migrateThis applies the SQL files in drizzle/ to your database.
pnpm devVisit http://localhost:3000 to load the landing page. The dashboard, booking links, and API routes require a signed-in Google user with the configured scopes.
Follow these steps to wire up OAuth and Calendar CRUD access:
- Create a Google Cloud project (or reuse an existing one) in the Google Cloud Console.
- Enable APIs: search for and enable Google Calendar API and OAuth consent screen.
- Configure OAuth consent:
- Choose External if you need non-domain users.
- Add the required scopes (Calendar API already selected when using the client below).
- Provide app logo, support email, and authorized domains.
- Create OAuth credentials → Web application:
- Authorized JavaScript origins:
http://localhost:3000- Your production domain (e.g.
https://meetme.blackie.tech)
- Authorized redirect URIs:
http://localhost:3000/api/auth/callback/googlehttps://your-production-domain/api/auth/callback/google
- Download or copy the Client ID and Client Secret into
.env.localasGOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRET.
- Authorized JavaScript origins:
- Service account (optional but recommended):
- Create a service account if you plan to run background sync or administrative tasks.
- Grant the Calendar API scope
https://www.googleapis.com/auth/calendar. - Store its credentials securely; use them in server-side jobs or scripts (e.g. via Cloud Scheduler) that call MeetMe’s cron endpoints.
- Update scopes in production:
- The app requests
openid email profile https://www.googleapis.com/auth/calendar. Users will be prompted for consent the first time they sign in. - If you change scopes later, existing users must re-authorize; MeetMe detects insufficient scopes and surfaces reauth prompts automatically.
- The app requests
After the OAuth client is saved, restart the dev server to pick up environment changes.
MeetMe sends booking confirmations via SMTP using Nodemailer. Provide the SMTP credentials in .env.local. For Gmail, create an App Password and set SMTP_SECURE=false. For services like SendGrid or Postmark, replace host/port and enable TLS if needed.
| Command | Description |
|---|---|
pnpm dev |
Start Next.js in development mode. |
pnpm build |
Compile a production build. |
pnpm start |
Serve the production build locally. |
pnpm db:migrate |
Apply Drizzle migrations. |
pnpm db:generate |
Regenerate migration SQL from the schema. |
pnpm lint / pnpm format |
Run Biome checks or format the codebase. |
- Deploy to Vercel with
vercel deploy --prodafter runningpnpm buildlocally. - Configure the same environment variables in Vercel’s dashboard (Project Settings → Environment Variables).
- If using a custom domain, add it in Vercel and update
NEXTAUTH_URLaccordingly.
- Calendar requests fail → confirm the user granted the full Calendar scope; ask them to sign out/in again. Check GCP OAuth consent status if the app is in Testing mode (users must be added as test users).
- Token refresh issues → ensure refresh tokens are still valid. Revoking consent in Google revokes refresh tokens; reauth is required.
- Database connection errors → verify
DATABASE_URLand that SSL settings match your provider (DATABASE_SSL=truefor managed services like Supabase/Neon). - Emails not sending → confirm SMTP credentials, host/port, and whether the provider requires TLS (
SMTP_SECURE=true).
Reach out to the team or file an issue if you run into something not covered here.
MeetMe uses a single Google provider configured for offline access and full calendar CRUD. The core setup lives in lib/auth.ts:
import GoogleProvider from "next-auth/providers/google";
export const authOptions: NextAuthOptions = {
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
authorization: {
params: {
scope:
"openid email profile https://www.googleapis.com/auth/calendar",
access_type: "offline",
prompt: "consent",
},
},
}),
],
callbacks: {
async jwt({ token, account, profile }) {
if (account?.provider === "google") {
const user = await upsertGoogleAccount({
account: {
provider: account.provider,
providerAccountId: account.providerAccountId,
access_token: account.access_token,
refresh_token: account.refresh_token,
scope: account.scope,
id_token: account.id_token,
expires_at: account.expires_at,
},
profile: profile as GoogleProfile | undefined,
});
token.uid = user.id;
token.email = user.email;
token.name = user.name ?? token.name;
token.picture = user.image;
}
return token;
},
},
};The helper upsertGoogleAccount persists tokens, scopes, and expiry metadata in Postgres using Drizzle so future API calls can refresh access tokens automatically.
Incoming bookings merge custom availability, blocked times, and Google busy slots. The heavy lifting happens in lib/google-calendar.ts:
export async function checkAvailability(
bookingLinkId: string,
{ duration, date, bufferMinutes = 15 }: {
duration: number;
date: Date;
bufferMinutes?: number;
},
): Promise<Date[]> {
const [settings] = await db
.select()
.from(availabilitySettings)
.where(eq(availabilitySettings.bookingLinkId, bookingLinkId))
.limit(1);
const dayStart = new Date(date);
dayStart.setHours(startHour, 0, 0, 0);
const existingEvents = await getCalendarEvents(
bookingLink.userId,
dayStart,
dayEnd,
);
// ...combine Google events + blocked times + buffers
}When a guest confirms a slot, MeetMe writes the meeting back to Google Calendar and emails both parties:
export async function createCalendarEvent(userId: string, payload: {
summary: string;
startTime: Date;
endTime: Date;
guestEmail: string;
guestName: string;
hostEmail?: string;
}): Promise<{ eventId: string; meetingLink?: string }> {
const calendar = await getCalendarClient(userId);
const response = await calendar.events.insert({
calendarId: "primary",
requestBody: {
summary: payload.summary,
start: { dateTime: payload.startTime.toISOString(), timeZone },
end: { dateTime: payload.endTime.toISOString(), timeZone },
attendees: [
{ email: payload.guestEmail, displayName: payload.guestName },
payload.hostEmail ? { email: payload.hostEmail, displayName: "Host" } : undefined,
].filter(Boolean),
reminders: {
useDefault: false,
overrides: [
{ method: "email", minutes: 24 * 60 },
{ method: "popup", minutes: 15 },
],
},
conferenceData: {
createRequest: { requestId: `meetme-${nanoid()}` },
},
},
conferenceDataVersion: 1,
sendUpdates: "all",
});
return {
eventId: response.data.id!,
meetingLink: response.data.conferenceData?.entryPoints?.[0]?.uri,
};
}If Google denies conference creation because the user revoked permissions, the helper retries without Meet links and surfaces a re-auth prompt.
Key architectural choices referenced in the “Building a Cloud-Native Booking System with Google OAuth & Calendar” notes:
- Next.js App Router serves landing, dashboard, and booking flows with React Server Components and shadcn/ui for design consistency.
- Drizzle ORM + Postgres persist users, OAuth tokens, booking links, availability windows, and blocked times (
db/schema.ts). - Edge caching (via
@vercel/edge-config) and background sync ensure availability data stays fresh without hammering the Calendar API. - Service accounts & Cloud Scheduler (documented in the blog post) run nightly reconciliation jobs so Google data, Postgres, and MeetMe stay in sync even when users are offline.
- Email pipelines in
lib/email.tsleverage Nodemailer to send branded confirmations to both host and guest immediately after a booking.
These snippets, combined with the GCP setup steps above, give you the full blueprint for running MeetMe locally, extending it in production, or adapting the patterns in your own cloud-native scheduling stack.