Ship the receipt pipeline (Livin/Grab/BRI), PocketBase spendings, PM2 ecosystem, and a proper .gitignore that excludes secrets, SQLite data, and media dumps. Co-authored-by: Cursor <cursoragent@cursor.com>
Gmail Reader
Monorepo with a React + Vite + DaisyUI client, a Fastify API, and a Gmail push watcher. The API owns Google OAuth, stores tokens in SQLite, and calls the Gmail API with gmail.readonly. The browser only receives an HTTP-only session cookie. The watcher reuses those tokens, calls users.watch, and runs each new INBOX message through a handler pipeline.
Prerequisites
- Node.js 20+
- A Google Cloud OAuth Web application client with:
- Scope:
https://www.googleapis.com/auth/gmail.readonly - Authorized JavaScript origin:
https://oauth.0dev.web.id(andhttp://localhost:5173for local) - Authorized redirect URIs:
https://oauth.0dev.web.id/callbackhttp://localhost:5173/callback(local; Vite proxies this to Fastify)
- Scope:
Local development
cp .env.example .env
Fill in GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and a long SESSION_SECRET. Keep:
GOOGLE_REDIRECT_URI=http://localhost:5173/callback
Then:
npm install
npm run dev
- UI: http://localhost:5173
- API: http://localhost:3000
- Watcher (optional):
npm run dev:watcher→ http://localhost:3001
Vite proxies /auth, /callback, and /api to Fastify so the Google redirect stays on the same origin as the SPA.
Sign in via the UI at least once so SQLite has a refresh token before starting the watcher.
Production (one process)
Build the frontend, then start Fastify. Fastify serves apps/web/dist and handles /callback itself.
npm install
npm run build
NODE_ENV=production GOOGLE_REDIRECT_URI=https://oauth.0dev.web.id/callback npm start
Point your reverse proxy at the Fastify port (PORT, default 3000). Send the whole host to that one process — SPA, /api, /auth, and /callback must share https://oauth.0dev.web.id.
Example Caddy:
oauth.0dev.web.id {
reverse_proxy 127.0.0.1:3000
}
SQLite is stored at apps/api/data/app.db. Keep that directory writable and backed up.
Gmail push watcher
Gmail does not POST to you directly. It publishes to Cloud Pub/Sub, which push-delivers to POST /pubsub/gmail.
GCP setup
- Enable Gmail API and Cloud Pub/Sub in the same Google Cloud project as the OAuth client.
- Create a topic, e.g.
projects/YOUR_PROJECT/topics/gmail-push. - Grant Pub/Sub Publisher on that topic to
serviceAccount:gmail-api-push@system.gserviceaccount.com. - Create a push subscription whose endpoint is publicly reachable HTTPS, e.g.
https://oauth.0dev.web.id/pubsub/gmail?token=YOUR_TOKEN. - Put the topic name and token in
.env(GOOGLE_PUBSUB_TOPIC,PUBSUB_VERIFICATION_TOKEN).
Start after a user has signed in through the web app:
npm run start:watcher
Example Caddy (API on 3000, watcher on 3001):
oauth.0dev.web.id {
handle /pubsub/gmail* {
reverse_proxy 127.0.0.1:3001
}
handle {
reverse_proxy 127.0.0.1:3000
}
}
The watcher renews users.watch on startup and every 12 hours. Handlers run in sequence per new message: log the event, then fetch and log the message body. A handler may pass fields to the next one or break the chain.
What it does
- Sign in with Google (server-side authorization code flow)
- List latest mail
- Search with Gmail query syntax (
from:,subject:,newer_than:, …) - Open a message and read decoded plain-text content
- Watch INBOX via Gmail push (Pub/Sub) and run a sequential handler pipeline (log event, then fetch and log the message)
- Watch INBOX via Gmail push (Pub/Sub) and run a sequential handler pipeline (log event, then fetch and log the message)