Set up OpenInstaDMstep by step.
This is the only guide you need. Follow it top to bottom — even if you've never touched Postgres, Redis, or a Meta app before. Real time: ~30 minutes, mostly waiting on Meta's dashboard.
Web app
Dashboard, auth, OAuth callback, webhooks. Runs on Vercel.
Worker
Queue + DMs + polling reconciler. Always-on host (Railway/VM). npm run worker.
Postgres + Redis
Same DATABASE_URL & REDIS_URL & ENCRYPTION_KEY in both processes.
Failed to decrypt.Start here
How OpenInstaDM works
Someone comments a keyword like LINK on your reel → Meta sends a webhook → your app matches the keyword → the worker sends the DM via Meta's Private Reply API. No scraping, no password, no browser.
Comment
LINK on your post
Webhook
Meta → /api/webhook
Match
Keyword check
Queue
BullMQ job
DM
Private reply sent
Official API only
Uses Instagram Private Replies. Personal accounts won't work — switch to Business/Creator first.
One reply per match
Deduped by comment ID. Rate-limited to Meta's 750/hr cap.
Two processes
Web app receives, worker sends. Both must run.
Before you start
Prerequisites
Create these free accounts first. The Meta app step is the longest — everything else is 2 minutes.
Instagram Business/Creator
RequiredPersonal accounts can’t use the API. Switch in Instagram: Settings → Account type → Switch to Professional.
Facebook account
RequiredRequired to create a Meta developer app at developers.facebook.com.
Resend account
RequiredSends magic-link login emails. Free tier covers dev. Get an API key at resend.com/api-keys.
PostgreSQL & Redis
RequiredPick one: local install, Docker, or free cloud (Neon/Supabase + Upstash). Docker is optional.
Node.js 20.19+ / 22.12+ / 24+
RequiredRequired by Prisma 7 & Next 16. Check with node -v. Use nvm if you’re on an older version.
Public HTTPS URL (for webhooks)
RequiredA tunnel like Cloudflare Tunnel, ngrok, or zrok — so Meta can reach your localhost.
Step 1 · 2 minutes
Generate your environment secrets
Four random strings protect sessions, encrypt tokens, and verify webhooks. Run these in your terminal — copy the outputs for the next step.
# 1. NextAuth session secret
openssl rand -base64 32
# 2. Cron protection secret (token refresh)
openssl rand -base64 32
# 3. Encryption key — MUST be exactly 64 hex chars (32 bytes)
openssl rand -hex 32
# 4. Webhook verify token — Meta uses this to confirm it's you
openssl rand -hex 16.env next. Keep the same ENCRYPTION_KEY in both the web app and the worker — otherwise decrypt fails.openssl rand -hex 32 gives 64 characters. Don't use base64 here — it must be hex.Step 2 · 5 minutes
Local development setup
AClone & install
git clone https://github.com/xeven777/openinstadm.git
cd openinstadm
npm installBCreate your .env
cp .env.example .envOpen .env and paste the four secrets you generated in Step 1. You'll fill the Meta keys in Step 4.
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=<output of step 1 — #1>
CRON_SECRET=<output of step 1 — #2>
ENCRYPTION_KEY=<output of step 1 — #3 — 64 hex chars>
WEBHOOK_VERIFY_TOKEN=<output of step 1 — #4>
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/openinstadm
REDIS_URL=redis://localhost:6379
RESEND_API_KEY=re_xxx
EMAIL_FROM="OpenInstaDM <login@yourdomain.com>"
# you’ll add these in Step 4
INSTAGRAM_APP_ID=
INSTAGRAM_APP_SECRET=
FACEBOOK_APP_SECRET=
META_GRAPH_API_VERSION=v26.0CStart PostgreSQL & Redis
Pick one option per datastore. Docker is supported but not required.
brew install postgresql@16 redis
brew services start postgresql@16
redis-server --daemonize yes
createdb openinstadm
psql -c "ALTER USER postgres PASSWORD 'postgres';"sudo apt install postgresql redis-server
sudo systemctl start postgresql
redis-server --daemonize yes
sudo -u postgres createdb openinstadm
sudo -u postgres psql -c "ALTER USER postgres PASSWORD 'postgres';"Keep the shipped .env values: postgresql://postgres:postgres@localhost:5432/openinstadm and redis://localhost:6379.
DInitialize the database
npm run db:generate # builds Prisma client to @/app/generated/prisma
npm run db:migrate # applies prisma/migrations to your DBNo Docker? Reset with dropdb openinstadm && createdb openinstadm then re-run the two commands.
Step 3 · 3 minutes
Expose your localhost with a public HTTPS URL
Instagram webhooks must reach your machine. You need one tunnel — all three options below do the same thing. Cloudflare Tunnel is the most reliable free choice (stable URL, no session limits).
# macOS
brew install cloudflared
# Linux
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o cloudflared
chmod +x cloudflared && sudo mv cloudflared /usr/local/bin/
# Windows: download from github.com/cloudflare/cloudflared/releases/latestcloudflared tunnel --url http://localhost:3000
# copy the https://xxx.trycloudflare.com URLNEXTAUTH_URL=https://xxx.trycloudflare.comcloudflared login
cloudflared tunnel create opendm-dev
cloudflared tunnel route dns opendm-dev dev.yourdomain.comtunnel: opendm-dev
credentials-file: /home/<you>/.cloudflared/<tunnel-id>.json
ingress:
- hostname: dev.yourdomain.com
service: http://localhost:3000
- service: http_status:404cloudflared tunnel run opendm-devNEXTAUTH_URL=https://dev.yourdomain.comNEXTAUTH_URL and the two Meta URLs in Step 4.Step 4 · 15 minutes — the critical part
Configure the Meta developer app
Follow in order. The Instagram Graph API only works from a real Meta app — this is where most beginners get stuck.
- Go to developers.facebook.com/apps → Create App.
- App type: Business → Next.
- Name + contact email → Use Cases screen → filter “All” → select Manage messaging and content on Instagram.
.envApp Settings → Basic
FACEBOOK_APP_SECRET
Click Show next to App Secret.
Instagram → API Setup
INSTAGRAM_APP_ID
Long number like 2036...
Instagram → API Setup
INSTAGRAM_APP_SECRET
Click Show under Instagram App Secret.
INSTAGRAM_APP_ID=2036xxxxxxxxxxx
INSTAGRAM_APP_SECRET=your-instagram-app-secret
FACEBOOK_APP_SECRET=your-facebook-app-secret
META_GRAPH_API_VERSION=v26.0In Development mode Meta only allows testers you authorize.
- In the Meta dashboard go to App Roles → Roles → Instagram Testers → Add Testers → type your Instagram username → Send invite.
- On your phone (crucial): Instagram → Profile → Settings and activity → Apps and websites → Tester Invites → Accept.
Business login → Redirect URI
In Instagram → API Setup → Business login settings add:
<YOUR-TUNNEL-URL>/api/instagram/callback
# e.g. https://xxx.trycloudflare.com/api/instagram/callbackConfigure Webhooks
In Instagram → Configure Webhooks:
- Callback URL:
<YOUR-TUNNEL-URL>/api/webhook - Verify Token: paste
WEBHOOK_VERIFY_TOKENfrom Step 1 - Click Verify and Save → Subscribe to
- App Settings → Basic: set Privacy, Terms, and Data Deletion URLs to
<YOUR-TUNNEL-URL>/privacyetc. - Flip App Mode from Development to Live at the top of the dashboard.
Step 5 · 1 minute
Start OpenInstaDM
You need two terminals open at the same time. One receives, the other sends.
npm run devServes the dashboard at http://localhost:3000 (your tunnel forwards to it) and receives Meta webhooks.
npm run workerLong-running BullMQ worker. Sends DMs, button messages, public replies, and runs the comment polling reconciler.
Verify everything is healthy
Open this in your browser after both processes start.
open https://<your-tunnel-url>/api/health
# expect: { worker: { healthy: true }, db: ok, redis: ok }Step 6 · Production
Deploy to production
When you're ready to go live for real, split your hosting: Vercel for the web app, an always-on host for the worker.
- New Project → Add PostgreSQL + Redis.
- Import repo → set
NIXPACKS_BUILD_CMD=npm run db:generateandNIXPACKS_START_CMD=npm run worker. - Add all env vars — use Railway's internal hostnames.
# Railway internal (worker)
DATABASE_URL=postgresql://postgres:xxx@postgres.railway.internal:5432/railway
REDIS_URL=redis://default:xxx@redis.railway.internal:6379- Import GitHub repo into Vercel.
- Add all env vars — use Railway's public proxy URLs (
*.proxy.rlwy.net). - Deploy.
# Vercel (public proxy)
DATABASE_URL=postgresql://postgres:xxx@postgres.proxy.rlwy.net:1234/railway
REDIS_URL=redis://default:xxx@redis.proxy.rlwy.net:1234Run migrations on production
DATABASE_URL="postgresql://postgres:password@your-railway-proxy.rlwy.net:5432/railway" npm run db:migratedocs/deploy-dokploy.md — two applications (web + worker), same repo, different start commands, internal hostnames, and a nixpacks.toml pin for Node 22.13.1.Help
Troubleshooting
Cause: Account not added as Tester, or invite not accepted on the phone.
Fix: Re-send tester invite in App Roles → Roles → Instagram Testers. On phone: Instagram → Settings → Apps and websites → Tester Invites → Accept. Then reconnect.
Cause: WEBHOOK_VERIFY_TOKEN mismatch, tunnel not running, or Meta still pointing at an old trycloudflare URL.
Fix: Ensure the token in Meta’s webhook config exactly matches .env. Restart quick tunnels → update NEXTAUTH_URL and the two Meta URLs (callback + webhook). Prefer a named tunnel or reserved zrok share.
Cause: Worker not running or it crashed. Web app queues, worker sends.
Fix: Check https://<your-domain>/api/health → worker.healthy must be true. In dev: npm run worker must stay open. In prod: check Railway/worker logs and restart. Also check /logs in the dashboard for FAILED with reason.
Cause: ENCRYPTION_KEY in the web app doesn’t match the worker. Every encrypted token fails to decrypt.
Fix: Set the identical 64-hex ENCRYPTION_KEY in both environments. Regenerate with openssl rand -hex 32 and update both sides, then re-connect the Instagram account to re-encrypt its token.
Cause: DATABASE_URL wrong, DB not started, or migrations tried during Docker build (Dokploy — build has no network).
Fix: Verify PG is running (pg_isready / docker ps). Use pooler URL with sslmode=require on Neon. On Dokploy: prisma generate at build, prisma migrate deploy at start — not during build.
Cause: REDIS_URL is HTTP-only (e.g. Upstash REST) — BullMQ needs native Redis TCP. Or Redis not reachable from Vercel.
Fix: Use Redis Cloud, Upstash Redis with rediss:// TCP, or Aiven. From Vercel use the public proxy URL; from Railway/Dokploy use the internal hostname.
Reference
Environment variables
Copy from .env.example. Never commit real values — set them in your host's env settings in production.
| Variable | Required | Example / Notes |
|---|---|---|
| NEXTAUTH_URL | Required | https://xxx.trycloudflare.com — must be public HTTPS |
| NEXTAUTH_SECRET | Required | openssl rand -base64 32 — session signing |
| CRON_SECRET | Required | openssl rand -base64 32 — /api/cron/* bearer |
| ENCRYPTION_KEY | Required | 64 hex chars — openssl rand -hex 32 — same in web+worker |
| DATABASE_URL | Required | postgresql://... Neon pooler recommended |
| REDIS_URL | Required | redis:// or rediss:// — must be TCP for BullMQ |
| RESEND_API_KEY | Required | re_... — magic-link emails |
| EMAIL_FROM | Required | OpenInstaDM <login@yourdomain.com> |
| INSTAGRAM_APP_ID | Required | Long numeric — Instagram → API Setup |
| INSTAGRAM_APP_SECRET | Required | Instagram App Secret (Show) |
| FACEBOOK_APP_SECRET | Required | App Settings → Basic → App Secret |
| WEBHOOK_VERIFY_TOKEN | Required | openssl rand -hex 16 — paste in Meta webhook config |
| META_GRAPH_API_VERSION | Optional | v26.0 — default in .env.example |
DATABASE_URL, REDIS_URL, or ENCRYPTION_KEY, redeploy both the web app and the worker.Under the hood
Architecture in 30 seconds
[Comment]
↓ webhook POST /api/webhook (HMAC verified)
↓ enqueue BullMQ → dm-processing
↓ worker/dm-worker.ts → lib/queue/dm-worker.ts
↙ ↓ ↘
processComment processPostback processMessage
↓ Meta Graph API (private/public reply, buttons, follow gate)
Stack
Next.js 16 / React 19 · Prisma 7 + Postgres · BullMQ 5 + Redis · Auth.js (Resend) · Tailwind 4
Free production
Vercel (web) · Neon (Postgres) · Redis Cloud · Oracle VM / Railway (worker) · Resend · Meta
Key files
- app/api/webhook/route.ts — verify & enqueue
- worker/dm-worker.ts — lifecycle + polling
- lib/queue/dm-worker.ts — match → send
- lib/meta/client.ts — Graph API
- prisma/schema.prisma — DB schema
- docs/stack.md — full stack
Dokploy self-hosting?
Same repo, two Dokploy Applications (web + worker), internal hostnames, and start-command migrations. See docs/deploy-dokploy.md for the three gotchas (Node pin, build/start split, tunnel).
You're ready
Create your first campaign
Connect your Instagram, pick a reel, set a keyword like LINK, and comment it to watch the DM arrive. Every send is logged in /logs.
This guide mirrors SETUP.md and docs/stack.md. If Meta's dashboard changes, a PR documenting the new flow helps everyone.