OpenInstaDM logoOpenInstaDM
0Get started
Docs/Setup GuideBeginner-friendly

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.

Start setup View on GitHub
~30 min 6 steps No Docker required
What you're building

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.

Encryption key must match
The web app encrypts the Instagram token, the worker decrypts it. Different keys → every DM fails with Failed to decrypt.

On this page

OverviewPrerequisites011 — Generate secrets022 — Local development033 — Public tunnel044 — Meta app055 — Start the app066 — ProductionTroubleshootingEnvironment referenceHow it works

Need help?

Stuck on Meta's dashboard? See Troubleshooting or open an issue on GitHub.

Open an issue
OverviewPrerequisites1 — Generate secrets2 — Local development3 — Public tunnel4 — Meta app5 — Start the app6 — ProductionTroubleshootingEnvironment referenceHow it works

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.

01

Comment

LINK on your post

02

Webhook

Meta → /api/webhook

03

Match

Keyword check

04

Queue

BullMQ job

05

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

Required

Personal accounts can’t use the API. Switch in Instagram: Settings → Account type → Switch to Professional.

Facebook account

Required

Required to create a Meta developer app at developers.facebook.com.

Resend account

Required

Sends magic-link login emails. Free tier covers dev. Get an API key at resend.com/api-keys.

PostgreSQL & Redis

Required

Pick one: local install, Docker, or free cloud (Neon/Supabase + Upstash). Docker is optional.

Node.js 20.19+ / 22.12+ / 24+

Required

Required by Prisma 7 & Next 16. Check with node -v. Use nvm if you’re on an older version.

Public HTTPS URL (for webhooks)

Required

A tunnel like Cloudflare Tunnel, ngrok, or zrok — so Meta can reach your localhost.

01

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.

Generate secretsbash
# 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
Save these safely
You'll paste them into .env next. Keep the same ENCRYPTION_KEY in both the web app and the worker — otherwise decrypt fails.
ENCRYPTION_KEY = 64 hex
openssl rand -hex 32 gives 64 characters. Don't use base64 here — it must be hex.
02

Step 2 · 5 minutes

Local development setup

AClone & install

bash
git clone https://github.com/xeven777/openinstadm.git
cd openinstadm
npm install

BCreate your .env

bash
cp .env.example .env

Open .env and paste the four secrets you generated in Step 1. You'll fill the Meta keys in Step 4.

.env — fill this nowenv
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.0

CStart PostgreSQL & Redis

Pick one option per datastore. Docker is supported but not required.

macOSbash
brew install postgresql@16 redis
brew services start postgresql@16
redis-server --daemonize yes

createdb openinstadm
psql -c "ALTER USER postgres PASSWORD 'postgres';"
Ubuntu / Debianbash
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

bash
npm run db:generate   # builds Prisma client to @/app/generated/prisma
npm run db:migrate    # applies prisma/migrations to your DB

No Docker? Reset with dropdb openinstadm && createdb openinstadm then re-run the two commands.

03

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).

Quick Tunnel — no account, URL changes on restart
Install cloudflaredbash
# 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/latest
Start tunnelbash
cloudflared tunnel --url http://localhost:3000
# copy the https://xxx.trycloudflare.com URL
.envenv
NEXTAUTH_URL=https://xxx.trycloudflare.com
Named Tunnel — stable URL, survives restarts
bash
cloudflared login
cloudflared tunnel create opendm-dev
cloudflared tunnel route dns opendm-dev dev.yourdomain.com
~/.cloudflared/config.ymlyaml
tunnel: opendm-dev
credentials-file: /home/<you>/.cloudflared/<tunnel-id>.json
ingress:
  - hostname: dev.yourdomain.com
    service: http://localhost:3000
  - service: http_status:404
bash
cloudflared tunnel run opendm-dev
env
NEXTAUTH_URL=https://dev.yourdomain.com
Keep the tunnel running
Leave the tunnel command open in its own terminal. If the URL changes on restart, update NEXTAUTH_URL and the two Meta URLs in Step 4.
04

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.

1Create the app
  1. Go to developers.facebook.com/apps → Create App.
  2. App type: Business → Next.
  3. Name + contact email → Use Cases screen → filter “All” → select Manage messaging and content on Instagram.
Don't pick the wrong use case
Do NOT select “Authenticate with Facebook Login” or “Marketing API”. You need the Instagram product specifically.
2Copy your app keys into .env

App 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.

.envenv
INSTAGRAM_APP_ID=2036xxxxxxxxxxx
INSTAGRAM_APP_SECRET=your-instagram-app-secret
FACEBOOK_APP_SECRET=your-facebook-app-secret
META_GRAPH_API_VERSION=v26.0
3Add yourself as an Instagram Tester

In Development mode Meta only allows testers you authorize.

  1. In the Meta dashboard go to App Roles → Roles → Instagram Testers → Add Testers → type your Instagram username → Send invite.
  2. On your phone (crucial): Instagram → Profile → Settings and activity → Apps and websites → Tester Invites → Accept.
Common mistake
If you skip the phone acceptance, connecting Instagram later fails with “Insufficient Developer Role”. The invite must be accepted inside the Instagram app.
4Redirect URI & Webhook — use your tunnel URL

Business login → Redirect URI

In Instagram → API Setup → Business login settings add:

text
<YOUR-TUNNEL-URL>/api/instagram/callback
# e.g. https://xxx.trycloudflare.com/api/instagram/callback

Configure Webhooks

In Instagram → Configure Webhooks:

  • Callback URL: <YOUR-TUNNEL-URL>/api/webhook
  • Verify Token: paste WEBHOOK_VERIFY_TOKEN from Step 1
  • Click Verify and Save → Subscribe to
commentsmessages
5Go Live
  1. App Settings → Basic: set Privacy, Terms, and Data Deletion URLs to <YOUR-TUNNEL-URL>/privacy etc.
  2. Flip App Mode from Development to Live at the top of the dashboard.
05

Step 5 · 1 minute

Start OpenInstaDM

You need two terminals open at the same time. One receives, the other sends.

Terminal 1 — Web app
bash
npm run dev

Serves the dashboard at http://localhost:3000 (your tunnel forwards to it) and receives Meta webhooks.

http://localhost:3000Turbopack
Terminal 2 — Worker
bash
npm run worker

Long-running BullMQ worker. Sends DMs, button messages, public replies, and runs the comment polling reconciler.

If this isn't running, webhooks are logged but no DM ever sends.

Verify everything is healthy

Open this in your browser after both processes start.

bash
open https://<your-tunnel-url>/api/health
# expect: { worker: { healthy: true }, db: ok, redis: ok }
Try it end-to-end
Connect Instagram in the dashboard → create a campaign for a post → comment your keyword from a tester account → watch the DM log. If it says “queued” but never “sent”, check Terminal 2.
06

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.

Railway — DB, Redis, Worker
  1. New Project → Add PostgreSQL + Redis.
  2. Import repo → set NIXPACKS_BUILD_CMD=npm run db:generate and NIXPACKS_START_CMD=npm run worker.
  3. Add all env vars — use Railway's internal hostnames.
env
# Railway internal (worker)
DATABASE_URL=postgresql://postgres:xxx@postgres.railway.internal:5432/railway
REDIS_URL=redis://default:xxx@redis.railway.internal:6379
Vercel — Web app
  1. Import GitHub repo into Vercel.
  2. Add all env vars — use Railway's public proxy URLs (*.proxy.rlwy.net).
  3. Deploy.
env
# Vercel (public proxy)
DATABASE_URL=postgresql://postgres:xxx@postgres.proxy.rlwy.net:1234/railway
REDIS_URL=redis://default:xxx@redis.proxy.rlwy.net:1234
Internal URLs don't work from Vercel — it's outside Railway's network. Use the proxy.

Run migrations on production

bash
DATABASE_URL="postgresql://postgres:password@your-railway-proxy.rlwy.net:5432/railway" npm run db:migrate
Self-host alternativeOwn server? See Dokploy guide in docs/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

Insufficient Developer Role when connecting Instagram

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.

Webhook verification fails

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.

Comments logged but no DMs sent

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.

Decryption errors in the worker

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.

Prisma: Can't reach database / P1001

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.

Redis / BullMQ: job not processed

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.

VariableRequiredExample / Notes
NEXTAUTH_URLRequiredhttps://xxx.trycloudflare.com — must be public HTTPS
NEXTAUTH_SECRETRequiredopenssl rand -base64 32 — session signing
CRON_SECRETRequiredopenssl rand -base64 32 — /api/cron/* bearer
ENCRYPTION_KEYRequired64 hex chars — openssl rand -hex 32 — same in web+worker
DATABASE_URLRequiredpostgresql://... Neon pooler recommended
REDIS_URLRequiredredis:// or rediss:// — must be TCP for BullMQ
RESEND_API_KEYRequiredre_... — magic-link emails
EMAIL_FROMRequiredOpenInstaDM <login@yourdomain.com>
INSTAGRAM_APP_IDRequiredLong numeric — Instagram → API Setup
INSTAGRAM_APP_SECRETRequiredInstagram App Secret (Show)
FACEBOOK_APP_SECRETRequiredApp Settings → Basic → App Secret
WEBHOOK_VERIFY_TOKENRequiredopenssl rand -hex 16 — paste in Meta webhook config
META_GRAPH_API_VERSIONOptionalv26.0 — default in .env.example
Tip — keep both envs in sync
In Railway, Vercel, and Dokploy you set envs in the dashboard. After changing 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.

Open dashboard Browse templates

This guide mirrors SETUP.md and docs/stack.md. If Meta's dashboard changes, a PR documenting the new flow helps everyone.

OpenInstaDM
HomeTemplatesGitHub