Inngest setup runbook
This runbook describes how to set up and run the InboxOps worker with Inngest for async jobs (email fetch cron, fetch-per-inbox, and future send/auto-response jobs). See PRODUCT-AND-ARCHITECTURE.md §4.7 and implementation-plan/M06-email-pipeline.md.
Overview
- Worker app (
apps/worker): Express server that exposes an Inngestserveendpoint at/api/inngestand runs Inngest functions (cron + event-driven). - Inngest Cloud (production): You create an app, get Event Key and Signing Key, and configure the worker URL so Inngest can invoke your functions.
- Inngest Dev Server (local): Run the open-source dev server locally so you can develop and test without Inngest Cloud.
Environment variables
The worker loads env from the monorepo root (.env and .env.local when cwd is apps/worker). Required for the worker to start:
| Variable | Required | Description |
|---|---|---|
DATABASE_URL | Yes | Postgres connection string (pooled). |
DATABASE_URL_UNPOOLED | Yes | Postgres connection string (direct). |
AUTH_SECRET | Yes | Lucia session signing (min 32 chars). |
ENCRYPTION_KEY | Yes | AES-256 key (base64) for inbox credentials. |
INNGEST_EVENT_KEY | Yes* | Event key from Inngest Cloud (or dummy for local dev). |
INNGEST_SIGNING_KEY | Yes* | Signing key from Inngest Cloud (or dummy for local dev). |
APP_URL | Yes | Base URL of the web app (e.g. http://localhost:3000). |
* For local dev with the Inngest Dev Server, dummy values (e.g. dev-event-key and dev-signing-key) are accepted; the dev server does not validate keys.
Optional (used by worker functions):
| Variable | When needed | Description |
|---|---|---|
PORT | Optional | Worker HTTP port. Default: 8288. |
GOOGLE_CLIENT_ID | Gmail inboxes | OAuth client ID for Gmail API (token refresh). |
GOOGLE_CLIENT_SECRET | Gmail inboxes | OAuth client secret for Gmail API. |
MICROSOFT_CLIENT_ID | Microsoft inboxes | OAuth app ID for Microsoft Graph. |
MICROSOFT_CLIENT_SECRET | Microsoft inboxes | OAuth client secret. |
MICROSOFT_TENANT_ID | Microsoft inboxes | Tenant ID (e.g. common). Defaults to common if unset. |
GOOGLE_PUBSUB_TOPIC | Gmail push | Full topic name (e.g. projects/my-project/topics/inboxops-gmail) for Gmail watch. When unset, Gmail uses polling only. |
GRAPH_PUSH_NOTIFICATION_URL | Microsoft push | Webhook URL for Graph change notifications (e.g. https://worker.example.com/api/webhooks/graph-push). When unset, Microsoft uses polling only. |
AWS_ACCESS_KEY_ID | Attachments | S3 upload for inbound/outbound attachments. |
AWS_SECRET_ACCESS_KEY | Attachments | S3 secret. |
AWS_REGION | Attachments | e.g. eu-central-1. |
AWS_S3_BUCKET | Attachments | Bucket name. |
For local dev with the Inngest Dev Server:
| Variable | Description |
|---|---|
INNGEST_DEV | Set to 1 so the SDK sends events to the dev server. |
INNGEST_BASE_URL | If the dev server runs on a non-default port, set e.g. http://localhost:8289. |
See .env.example for a full list and §9 in PRODUCT-AND-ARCHITECTURE.md.
Local development
1. Install dependencies
From the repo root:
pnpm install
2. Configure env
Copy .env.example to .env.local and fill in at least the required variables. For local dev you can use dummy Inngest keys:
INNGEST_EVENT_KEY=dev-event-key
INNGEST_SIGNING_KEY=dev-signing-key
3. Start the worker
From the repo root:
pnpm --filter @inboxops/worker dev
The worker listens on http://localhost:8288 and serves Inngest at http://localhost:8288/api/inngest.
4. Start the Inngest Dev Server
The Inngest Dev Server defaults to port 8288, which conflicts with the worker. Run the dev server on another port and point it at the worker:
npx --ignore-scripts=false inngest-cli@latest dev -p 8289 -u http://localhost:8288/api/inngest
- Dev Server UI: http://localhost:8289 (dashboard, function list, invoke, event log).
- The dev server will poll the worker for functions and run crons / invoke functions by calling the worker.
Optional: so that the worker sends events (e.g. from the cron step) to the dev server on 8289, set in .env.local:
INNGEST_DEV=1
INNGEST_BASE_URL=http://localhost:8289
If you run the dev server on the default port 8288, you must run the worker on a different port (e.g. PORT=8290 pnpm --filter @inboxops/worker dev) and use -u http://localhost:8290/api/inngest when starting the dev server.
5. Verify
- Open http://localhost:8289 and confirm the app is registered and functions appear (e.g.
email-fetch-cron,email-fetch). - Use Invoke on
email-fetch-cronto trigger the cron once, or wait for the 5-minute schedule; then check thatinboxops/email.fetchevents are sent andemail-fetchruns for each connected inbox.
Production (Inngest Cloud)
1. Create an Inngest app
- Sign up at app.inngest.com.
- Create an app and add a Sync URL that Inngest can reach (your worker’s public URL), e.g.
https://worker.yourdomain.com/api/inngest. - Create an Event Key and a Signing Key in the Inngest dashboard.
2. Configure env
Set in your production env (e.g. Fly.io secrets or your platform’s config):
INNGEST_EVENT_KEY= Event Key from the dashboard.INNGEST_SIGNING_KEY= Signing Key from the dashboard.- Ensure the worker has the same required vars (database, auth, encryption,
APP_URL).
Do not set INNGEST_DEV or INNGEST_BASE_URL in production.
3. Deploy the worker
Deploy the worker so it is reachable at the Sync URL you registered. Inngest will poll this URL to discover functions and invoke them (crons and events).
4. Verify
In the Inngest Cloud dashboard, open your app and confirm that functions are synced and that runs appear when the cron triggers or when events are sent.
Functions (M06.3–M06.6)
| Function ID | Trigger | Description |
|---|---|---|
email-fetch-cron | Cron */5 * * * * | Every 5 minutes, lists connected inboxes and sends one inboxops/email.fetch event per inbox. |
email-fetch | Event inboxops/email.fetch | Loads inbox, enforces rate limit, fetches new messages (IMAP/Gmail/Graph), processes messages (spam/bounce/dedupe/thread/new ticket), persists attachments to S3, updates sync state. Concurrency: one per inbox. |
gmail-watch-register | Event inboxops/gmail.watch.register | Registers Gmail push watch (when GOOGLE_PUBSUB_TOPIC set). Called after Google OAuth connect and by renewal cron. |
gmail-watch-renewal-cron | Cron daily 06:00 | Finds Gmail inboxes with watch expiring within 24h and emits gmail.watch.register per inbox. |
graph-subscription-create | Event inboxops/graph.subscription.create | Creates Graph change notification subscription (when GRAPH_PUSH_NOTIFICATION_URL set). Called after Microsoft OAuth connect. |
graph-subscription-renewal-cron | Cron every 12h | Renews Microsoft Graph subscriptions expiring within 24h. |
Gmail and Graph push (M06.6)
- Gmail: Configure a Google Cloud Pub/Sub topic and grant the Gmail API permission to publish to it. Set
GOOGLE_PUBSUB_TOPICto the full topic name (e.g.projects/my-project/topics/inboxops-gmail). Point the topic’s push subscription to your worker URL:https://<worker-host>/api/webhooks/gmail-push. After a user connects Gmail, the worker registers a watch and Gmail sends push messages to Pub/Sub, which forwards to the worker; the worker enqueuesinboxops/email.fetch. - Microsoft Graph: Set
GRAPH_PUSH_NOTIFICATION_URLto the worker webhook URL (e.g.https://<worker-host>/api/webhooks/graph-push). The worker must be reachable over HTTPS for Graph validation. After a user connects Microsoft, the worker creates a subscription; Graph sends change notifications to the webhook, which enqueuesinboxops/email.fetchper notification. - If these env vars are unset, the worker still runs; Gmail and Microsoft inboxes use the 5-minute polling cron only.
Troubleshooting
- Worker fails to start: Run from repo root so
.env/.env.localare found. Check that all required env vars are set;getEnv()will throw with a clear message. - Dev server does not see functions: Ensure the worker is running and the
-uURL is correct. Openhttp://localhost:8288/api/inngestin a browser to see the Inngest debug payload (e.g.functionsFound). - Cron not firing locally: In the Dev Server UI, use Invoke on
email-fetch-cronto run it once. For automatic 5-min runs, leave the dev server and worker running. - Gmail/Graph fetch fails with token errors: Set
GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRETorMICROSOFT_CLIENT_ID/MICROSOFT_CLIENT_SECRET(andMICROSOFT_TENANT_IDif needed). The worker uses these to refresh OAuth tokens when fetching mail. - Attachments not stored: Configure
AWS_*env vars. If any are missing, the worker skips S3 upload and still creates tickets/messages.