Abie Maxey

Design theme

Customize ~ more themes
The Blog
Building October 2026 6 min read

Your inbox and your day, on your own dashboard.

I run my days from a dashboard I built a few months back. Today I connected Gmail and Google Calendar to it, hit two walls on the way, and wrote down the exact fixes so you don't have to find them yourself.

The Today page of my dashboard: the day's phases in the middle, tasks and today's Google Calendar in the right rail

My Today page. Client names swapped out for the screenshot.

01 ~ The point

What you get

One screen for the day. My tasks, then today's calendar right under them, with the event that's running lit up and everything finished folded away. Leads in the pipeline carry their actual email history, so I never open Gmail just to remember where a conversation stopped.

Close-up of the calendar in the right rail: time, a colour dot and the title for each event, with the running one highlighted

The calendar lives in the right rail, under the tasks. Each event is a time, a dot in the calendar's own colour, and the title. Earlier events fold into one line, so the rail only shows what's still ahead. It reads the same Google account the rest of the dashboard uses, with read-only access.

02 ~ The setup

The four pieces

Every Google connection is the same four things. Miss one and it fails in a way that looks like a different problem.

OAuth client

A client ID and secret from Google Cloud. It identifies your dashboard to Google.

Scopes

gmail.readonly and calendar.readonly. Read only ~ the dashboard never sends or edits.

Refresh token

What you get once you click Allow. The server trades it for a fresh access token whenever it needs one.

APIs enabled

Gmail API and Google Calendar API switched on in the same Cloud project. Easy to forget, and the reason my calendar failed.

03 ~ Consent once

Get a refresh token

I keep a tiny script in the repo that opens Google's consent screen, catches the redirect on 127.0.0.1, and prints the refresh token. The two parameters that matter are access_type=offline, which is what returns a refresh token at all, and prompt=consent, which makes Google ask again when you add a scope.

scripts/gmail-auth.mjs (the important part)
const SCOPE = [
  "https://www.googleapis.com/auth/gmail.readonly",
  "https://www.googleapis.com/auth/calendar.readonly",
].join(" ");

const authUrl = "https://accounts.google.com/o/oauth2/v2/auth?" +
  new URLSearchParams({
    client_id: CLIENT_ID,
    redirect_uri: "http://127.0.0.1:53682",
    response_type: "code",
    scope: SCOPE,
    access_type: "offline", // returns a refresh token
    prompt: "consent",      // re-asks when scopes change
  });

The first time I ran it, it stopped straight away:

zsh ~ lead-tracker
$node scripts/gmail-auth.mjs
Set GMAIL_CLIENT_ID and GMAIL_CLIENT_SECRET first.

The values were already in .env.local. Next.js loads that file for the app, but a plain Node script doesn't. Node can load it for you, no extra package needed:

zsh ~ lead-tracker
$node --env-file=.env.local scripts/gmail-auth.mjs
Open this URL in your browser and approve access:
https://accounts.google.com/o/oauth2/v2/auth?client_id=…
GMAIL_REFRESH_TOKEN=1//0e•••••••••••••••••••••

Paste the token into .env.local. Done once, it lasts until you revoke it.

04 ~ The wall

Turn the API on

Gmail worked right away. The calendar still said access wasn't consented, which made no sense, because I had just clicked Allow on the calendar scope. So I asked Google directly what the token was allowed to do, and what the Calendar API thought about it:

checking the token
granted scopes: calendar.readonly gmail.readonly
calendar status: 403
Google Calendar API has not been used in project •••••• before
or it is disabled. Enable it by visiting the API overview…

The token was fine. The Calendar API itself was switched off in the Cloud project. Consenting to a scope and enabling the API are two different switches, and you need both. Open APIs & Services in the Cloud console, find Google Calendar API, press Enable, give it a minute. My calendar showed up on the next refresh.

If an API says “not consented” or 403 while the scope is clearly granted, check that the API is enabled before you redo any auth.

05 ~ The code

Read the day in code

The server swaps the refresh token for a short-lived access token, caches it until just before it expires, and asks for one day of events. Gmail and Calendar share the same token.

data/gcal.ts (trimmed)
const token = await googleAccessToken(); // refresh -> access, cached

const q = new URLSearchParams({
  timeMin: `${day}T00:00:00${offset}`,  // e.g. +08:00 for Manila
  timeMax: `${day}T23:59:59${offset}`,
  singleEvents: "true",  // expands recurring events
  orderBy: "startTime",
});

const res = await fetch(
  `https://www.googleapis.com/calendar/v3/calendars/primary/events?${q}`,
  { headers: { Authorization: `Bearer ${token}` } },
);
if (res.status === 401 || res.status === 403) return { ok: false, reason: "scope" };

Two small things saved me time. singleEvents=true turns a repeating block like “gym, Mon to Thu” into real events for that day. And the time window needs your time zone's offset, or the day starts at midnight somewhere else.

06 ~ Going live

Ship it without leaking it

Before I put a Gmail token on a live site, I checked who else could open it. Anyone with the link could: every page and every data route answered without a login. So the dashboard got a password page first, with a signed cookie checked on every request. Do that before you connect anything personal.

Then the values go to Vercel, marked sensitive so nobody on the team can read them back:

zsh ~ lead-tracker
$npx vercel@latest env rm GMAIL_REFRESH_TOKEN production --yes
$npx vercel@latest env add GMAIL_REFRESH_TOKEN production --sensitive
$npx vercel@latest redeploy <latest-production-url> --target production
✓ Ready

The last line matters. Vercel only reads environment variables when it builds, so a new token does nothing until you redeploy. And if a global install of the Vercel CLI asks for admin rights, skip it ~ putting npx in front runs the latest version without installing.

07 ~ Copy this

The checklist

  • Read-only scopes, consented with prompt=consent and access_type=offline
  • Gmail API and Calendar API enabled in the same project as the OAuth client
  • Client ID, secret and refresh token in .env.local, never in the repo
  • The same values on Vercel, marked sensitive, then a redeploy
  • A login in front of every page and API route before any of this goes live
  • Borrowing a token from another app ~ it only works with that app's client, and breaks when you reconnect there

That last one I learned the honest way. My site's admin dashboard already had a calendar connection, and my first idea was to reuse its token. It wouldn't have worked: a refresh token belongs to the OAuth client that asked for it. One minute of consent beat an hour of being clever.

Abie Maxey

Love,

Abie

Weekly Dispatch

The builder's diaries.

What I'm building, breaking, and figuring out.

  • AI tools I actually use to build faster
  • Behind-the-scenes of growing a business in Madrid
  • Community plays, client wins, and real revenue breakdowns
  • Grounded dispatches from someone building roots
Growing community ~ join Claude Maxxing on Skool

Drop your email.

Unsubscribe any time. No spam, ever.

Apply for Ship Yours