Single-owner Gmail triage console. Deterministic local rules classify mail under a configured source label, apply Gmail labels, and keep a bounded sync history. See docs/product-spec.md.
Is
- Owner-only workspace (one configured identity)
- Local rule-based triage (no LLM in the sync path)
- Bounded sync with run history and trial mode
- Gmail label mutations for triage outcomes
Is not
- Multi-user SaaS or team inbox product
- A send/compose client
- Permanent delete by default (starred mail stays protected; dangerous purge is explicit and opt-in)
- Production-hardened infrastructure — treat it as a personal experiment
This project is vibecoded / AI-assisted. It has been reviewed to a point, but use at your own risk. There is no warranty and no liability. Never point insecure local mode, Docker experiments, or live sync at a mailbox you cannot afford to mess up.
Required for any path that connects a real Gmail mailbox (local insecure Docker, Neon Docker, or Bun + Neon).
- Open Google Cloud Console and create or select a project.
- Enable the Gmail API for that project (APIs & Services → Library → Gmail API → Enable).
- Configure the OAuth consent screen:
- User type: External is fine for a personal app in testing.
- Add an app name and your email as support/developer contact.
- Under Scopes, ensure the app can request:
openidhttps://www.googleapis.com/auth/gmail.modify
- Under Test users, add the Gmail account you will connect (required while the app is in testing).
- Create credentials: APIs & Services → Credentials → Create credentials → OAuth client ID.
- Application type: Web application.
- Authorized redirect URIs — add the exact URI you will put in env (no trailing slash; must match character-for-character):
- Local / Docker on this machine:
http://localhost:3000/api/oauth/google/callback - Public deploy:
https://your.public.host/api/oauth/google/callback
- Local / Docker on this machine:
- Copy the Client ID and Client secret into your env file:
GOOGLE_CLIENT_ID=....apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=...
GOOGLE_REDIRECT_URI=http://localhost:3000/api/oauth/google/callback- Generate a stable token-encryption key (used for Gmail refresh tokens at rest):
openssl rand -base64 32TOKEN_ENCRYPTION_KEY_V1=<paste output>Keep TOKEN_ENCRYPTION_KEY_V1 stable. Rotating it without re-connecting Gmail makes stored refresh tokens unreadable.
Notes
- The app requests modify access so it can add/remove labels (and, on the explicit danger-zone path, move archive mail to Gmail Trash). It does not send mail.
- Changing the redirect URI requires updating both Google Console and
GOOGLE_REDIRECT_URI. - Prefer a dedicated test Gmail account and test labels until you trust the setup.
Official Google reference: Using OAuth 2.0 for Web Server Applications.
Required for the Neon production Docker path and the Bun + Neon path. Not required for local insecure Docker (that stack uses Compose Postgres and bypasses Neon Auth).
Docs: Neon · Managed Better Auth overview · Next.js Auth guide.
- Sign in to the Neon Console and create a project.
- On the project Dashboard, copy connection strings:
- Pooled →
DATABASE_URL(app runtime; keepsslmode=require). - Direct / unpooled →
DATABASE_URL_UNPOOLED(migrations; prefer this forbun run db:migrate/ Compose migrate).
- Pooled →
- Set:
DATABASE_URL=postgresql://...@...-pooler....neon.tech/...?sslmode=require
DATABASE_URL_UNPOOLED=postgresql://...@....neon.tech/...?sslmode=require
DATABASE_DRIVER=neon-http- In the Neon Console, open your project → branch → Auth / Managed Better Auth.
- Click Enable (or provision Auth for the branch).
- Copy the Auth URL from Auth configuration (looks like
https://ep-….neonauth….neon.tech/…/auth). - Generate a cookie secret (≥ 32 characters):
openssl rand -base64 32- Set:
NEON_AUTH_BASE_URL=https://ep-….neonauth….neon.tech/…/auth
NEON_AUTH_COOKIE_SECRET=<paste output>- For local development, ensure Auth trusted domains allow localhost (Neon Console Auth settings / trusted domains, or CLI
neon neon-auth domain …). For a public deploy, add your production origin the same way.
- Start the app (Bun or Neon Docker — see below) so
/auth/sign-inis reachable. - Sign up / sign in once with the account that should own the workspace (email/password or whatever methods you enabled in Neon Auth).
- Obtain that user’s Neon Auth user id (from Neon Auth user admin / CLI / your first successful session debug). Set:
OWNER_NEON_AUTH_USER_ID=<that user id>- Restart the app. Only that id can open the dashboard, connect Gmail, or run sync. Any other signed-in user sees “Wrong account”.
bun run db:migrateOr let docker compose -f docker-compose.neon.yml up --build run the migrate service against Neon.
Complete Google Gmail OAuth first. Complete Neon only for path 2.
| 1. Local insecure | 2. Neon production | |
|---|---|---|
| Compose file | docker-compose.yml |
docker-compose.neon.yml |
| Database | Local Postgres in Compose | Your Neon Postgres |
| Auth | Bypassed (“Continue as local owner”) | Neon Auth (real sign-in) |
| App bind | 127.0.0.1:3000 |
127.0.0.1:3000 (override with APP_HOST_BIND) |
| Use when | Trying the app without Neon | Running the containerized app against Neon |
No Neon project required. Compose starts Postgres, migrates, and runs the app with auth bypass.
cp .env.example .env- Fill Google vars from Configure Google Gmail OAuth (
GOOGLE_*,TOKEN_ENCRYPTION_KEY_V1). Redirect URI should behttp://localhost:3000/api/oauth/google/callback. - Start:
docker compose up --build- Open http://127.0.0.1:3000 → Continue as local owner → connect Gmail → configure → sync.
Compose forces APP_PROFILE=local-compose, INSECURE_LOCAL_DEV=true, ALLOW_INSECURE_LOCAL_DEV=I_UNDERSTAND, and DATABASE_DRIVER=pg. A sticky warning banner marks the mode. Do not expose port 3000 beyond localhost. Never use these flags on a real deployment.
Hybrid (Compose DB + Bun on host): publish Postgres as 127.0.0.1:5432:5432 on the db service, run docker compose up db migrate, then:
bun install --frozen-lockfile
# .env.local: APP_PROFILE=local-compose, INSECURE_LOCAL_DEV=true, ALLOW_INSECURE_LOCAL_DEV=I_UNDERSTAND,
# DATABASE_DRIVER=pg,
# DATABASE_URL=postgresql://emailtriager:emailtriager@localhost:5432/email_triager
# + Google OAuth + TOKEN_ENCRYPTION_KEY_V1
bun run devRuns the same app image against Neon Postgres + Neon Auth. No local Postgres. Insecure local mode is forced off.
- Finish Configure Neon and Configure Google Gmail OAuth.
- Copy env:
cp .env.example .env.neon- Fill
.env.neon(no insecure flags):
DATABASE_URL=postgresql://...neon.tech/...?sslmode=require
DATABASE_URL_UNPOOLED=postgresql://...neon.tech/...?sslmode=require
DATABASE_DRIVER=neon-http
NEON_AUTH_BASE_URL=https://ep-….neonauth….neon.tech/…/auth
NEON_AUTH_COOKIE_SECRET=<at least 32 random characters>
OWNER_NEON_AUTH_USER_ID=<Neon Auth user id>
GOOGLE_CLIENT_ID=....apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=...
GOOGLE_REDIRECT_URI=https://your.public.host/api/oauth/google/callback
TOKEN_ENCRYPTION_KEY_V1=<openssl rand -base64 32>For a first smoke against Neon while still on this machine, you may use GOOGLE_REDIRECT_URI=http://localhost:3000/api/oauth/google/callback (and the matching Google Console URI).
- Build and run (requires
.env.neonin the project root):
docker compose -f docker-compose.neon.yml up --build- Open http://127.0.0.1:3000 and sign in as the Neon Auth owner (not “local owner”).
migrate applies Drizzle migrations to Neon (prefer unpooled URL). app runs with NODE_ENV=production and DATABASE_DRIVER=neon-http. To publish on all interfaces (only if you understand the risk): APP_HOST_BIND=0.0.0.0 docker compose -f docker-compose.neon.yml up --build.
Production checklist
- No
INSECURE_LOCAL_DEV/ALLOW_INSECURE_LOCAL_DEVin the env file - HTTPS public URL, matching
GOOGLE_REDIRECT_URI, and Neon Auth trusted domain - Stable
TOKEN_ENCRYPTION_KEY_V1andNEON_AUTH_COOKIE_SECRET(losing them breaks sessions/tokens) -
OWNER_NEON_AUTH_USER_IDmatches the only Neon Auth user you intend to allow
- Complete Configure Neon and Configure Google Gmail OAuth.
- Install and run:
bun install --frozen-lockfile
cp .env.example .env.local
# fill DATABASE_*, Neon Auth, OWNER_NEON_AUTH_USER_ID, Google OAuth, TOKEN_ENCRYPTION_KEY_V1
bun run db:migrate
bun run dev- Open http://localhost:3000, sign in as the configured Neon Auth owner, connect Gmail, configure triage, sync.
Use DATABASE_DRIVER=neon-http (default). Other identities are rejected.
Schema lives in db/schema.ts. Prefer the unpooled URL as DATABASE_URL_UNPOOLED for migrations when using Neon.
bun run db:check
bun run db:migrateAfter schema edits: bun run db:generate, then bun run db:migrate.
bun install --frozen-lockfile
bun run typecheck
bun run lint
bun test
bun run db:check
bun run buildOptional Neon / live-Gmail tests skip unless explicitly opted in. Never point tests at a production mailbox or database.
MIT — see LICENSE.