The open-source, self-hosted alternative to ManyChat for Instagram.
Automate comment replies, story-reply funnels, and keyword DMs β from a dashboard you own, with no SaaS fees.
Nudgra is an open alternative to ManyChat for Instagram. It focuses on the automations most operators actually use: connect Instagram accounts, receive messages and comments through Meta webhooks, send replies and follow-up sequences, and manage everything from a clean dashboard.
This repository is the self-hosted version β you run the app, Postgres, webhooks, OAuth, background jobs, and backups yourself. It is built with TanStack Start, Better Auth, Hono, Drizzle, pg-boss, and Postgres.
Don't want to manage infrastructure? Use Nudgra Cloud instead β a separate hosted Next.js + Convex build of the same product.
π Prefer a friendlier, step-by-step walkthrough? The hosted docs at docs.nudgra.com mirror this guide in a more readable format.
π‘ You can also hand this README to a coding agent β Claude Code, Codex, Cursor, or similar β and ask it to walk you through setup. That works especially well for VPS commands,
.envediting, Caddy configuration, and reading logs.
Questions or stuck? DM @Maikoke5.
| π¬ Comment automations | Auto-reply to comments on any post, specific posts/reels, or your next post when a comment matches your keywords β then send a DM with tracked link buttons. |
| π Story-reply funnels | Turn replies (or reactions) to any story, or one specific story, into the entry point of a follow-up sequence. |
| β‘ Keyword DM rules | Respond automatically when someone DMs a keyword. Set the rule once and Nudgra handles the rest. |
| π Follow-up sequences | Multi-step, delayed DM sequences delivered reliably with pg-boss background jobs. |
| π₯ Unified inbox | Read and reply to Instagram conversations from one dashboard. |
| π₯ Contacts | A lightweight contact record per person who engages, with CSV export. |
| π Tracked links & CTR | Per-button click tracking so you can see which automations convert. |
| πͺͺ Multi-account | Connect and switch between multiple Instagram professional accounts. |
| π Logs & analytics | Run counts, CTR, and delivery logs for every automation. |
| π‘οΈ Safety guardrails | Automations auto-pause when something looks off, so you stay within Meta's rules. |
- Connect an Instagram professional account through Meta's Instagram Business Login β Nudgra runs the OAuth flow and stores encrypted tokens itself.
- Build comment, story-reply, or keyword-DM automations in the dashboard.
- React β Meta webhooks hit your server, Nudgra matches triggers, and sends replies, DMs, and link buttons.
- Follow up β delayed sequences, token refreshes, and outbound deliveries run on pg-boss jobs.
- Measure β watch runs, CTR, conversations, and logs from your dashboard.
- Nudgra automates Instagram professional accounts (business or creator) only.
- New-follower welcome automations are not possible. Instagram exposes no public new-follower webhook trigger, so welcome-DM-on-follow cannot be set live. Use a comment, story-reply, or keyword-DM automation instead.
- Your Meta app must be published for webhooks to work, and each Instagram account must accept its tester invite (see Meta / Instagram setup).
| Layer | Choice |
|---|---|
| Framework | TanStack Start (React 19) |
| API | Hono |
| Auth | Better Auth (Google sign-in) |
| Database | Postgres + Drizzle ORM |
| Background jobs | pg-boss |
| Realtime | Postgres LISTEN/NOTIFY + SSE |
| Styling | Tailwind CSS v4 + Radix UI |
- What you need
- Important URLs
- Environment variables
- π» Local development
- π Deploy to a VPS (Docker Compose + Caddy)
- Google OAuth setup
- π· Meta / Instagram setup
- Updating a deployment
- Backup & restore
- Troubleshooting
- Useful commands
- Notes for operators
- Contributing, security & license
The setup has a few moving parts because Google OAuth, Instagram OAuth, Meta webhooks, HTTPS, and Postgres all need to agree with each other. Expect about 15 minutes if you already have a VPS and domain ready.
- A Google account for dashboard sign-in.
- A Meta Developer account.
- An Instagram professional account (business or creator) that you want to automate.
- For production: a VPS, a domain or subdomain, Docker, and HTTPS.
- For local development: Node.js, Git, and either Docker or another Postgres database.
Replace https://your-domain.com with your real domain or subdomain, for example https://nudgra.example.com.
| Purpose | URL |
|---|---|
| App | https://your-domain.com |
| Health check | https://your-domain.com/api/v1/health |
| Google OAuth redirect URI | https://your-domain.com/api/auth/callback/google |
| Meta / Instagram OAuth redirect URI | https://your-domain.com/api/meta/callback |
| Meta webhook callback URL | https://your-domain.com/api/meta/webhooks |
| OpenAPI document | https://your-domain.com/api/v1/openapi.json |
Copy .env.example to .env and fill in the values for your environment.
DATABASE_URL=postgres://nudgra:nudgra@localhost:5432/nudgra
DATABASE_POOL_MAX=10
DATABASE_SSL=false
DATABASE_SSL_REJECT_UNAUTHORIZED=true
SITE_URL=http://localhost:3000
BETTER_AUTH_URL=http://localhost:3000
BETTER_AUTH_SECRET=replace-with-a-random-secret
TOKEN_ENCRYPTION_KEY=replace-with-another-random-secret
TRUSTED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000
TRUST_PROXY_HEADERS=false
OPERATOR_EMAIL_ALLOWLIST=you@example.com
DEFAULT_WORKSPACE_TIMEZONE=UTC
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
META_APP_ID=
META_APP_SECRET=
META_VERIFY_TOKEN=
META_GRAPH_API_VERSION=v23.0
META_WEBHOOK_MAX_BODY_BYTES=1048576Production values should look like this:
SITE_URL=https://your-domain.com
BETTER_AUTH_URL=https://your-domain.com
TRUSTED_ORIGINS=https://your-domain.com
TRUST_PROXY_HEADERS=true
OPERATOR_EMAIL_ALLOWLIST=you@example.comGenerate secrets with OpenSSL:
openssl rand -base64 32
openssl rand -hex 32Or generate them with Node:
node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
β οΈ Do not rotateBETTER_AUTH_SECRETorTOKEN_ENCRYPTION_KEYafter real users or Instagram accounts are connected. Stored OAuth and Instagram tokens depend on those secrets.
Production fails closed when BETTER_AUTH_SECRET, TOKEN_ENCRYPTION_KEY, or OPERATOR_EMAIL_ALLOWLIST are missing or still set to placeholder values. An empty operator allowlist is allowed only for local development.
This path is best for development and UI testing. For a complete Instagram webhook flow, use a VPS or an HTTPS tunnel, because Meta must be able to reach your webhook URL from the internet.
Install:
- Git
- Node.js 24 or newer
- Docker, if you want local Postgres through a container
In a Unix shell (macOS, Linux, or Git Bash):
git clone https://github.com/MaikoCode/nudgra_oss.git nudgra-oss
cd nudgra-oss
npm install
cp .env.example .envIn Windows PowerShell:
git clone https://github.com/MaikoCode/nudgra_oss.git nudgra-oss
cd nudgra-oss
npm install
Copy-Item .env.example .envIf you use Docker:
docker run --name nudgra-postgres \
-e POSTGRES_DB=nudgra \
-e POSTGRES_USER=nudgra \
-e POSTGRES_PASSWORD=nudgra \
-p 5432:5432 \
-d postgres:17-alpineIn Windows PowerShell:
docker run --name nudgra-postgres `
-e POSTGRES_DB=nudgra `
-e POSTGRES_USER=nudgra `
-e POSTGRES_PASSWORD=nudgra `
-p 5432:5432 `
-d postgres:17-alpineYour local .env can keep:
DATABASE_URL=postgres://nudgra:nudgra@localhost:5432/nudgra
DATABASE_SSL=falseAt minimum, set:
SITE_URL=http://localhost:3000
BETTER_AUTH_URL=http://localhost:3000
TRUSTED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000
TRUST_PROXY_HEADERS=false
BETTER_AUTH_SECRET=your-generated-secret
TOKEN_ENCRYPTION_KEY=your-generated-token-secret
OPERATOR_EMAIL_ALLOWLIST=your-google-email@gmail.comGoogle OAuth is required for normal sign-in. Create Google OAuth credentials using the Google section below, then set:
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secretFor local Google OAuth, use:
Authorized JavaScript origin:
http://localhost:3000
Authorized redirect URI:
http://localhost:3000/api/auth/callback/google
npm run db:migrate
npm run devOpen http://localhost:3000.
Useful local commands:
npm run typecheck
npm run test
npm run test:visual
npm run db:studioThis is the recommended production setup.
Create an A record for your domain or subdomain.
Type: A
Name: nudgra
Value: YOUR_VPS_PUBLIC_IP
Proxy status: DNS only while issuing the first certificate
If you use Cloudflare, use DNS only for the first certificate setup. After HTTPS works, the Cloudflare proxy can be enabled if SSL/TLS mode is set to Full (strict).
On Ubuntu or Debian:
sudo apt update && sudo apt upgrade -y
sudo apt install -y git curl ufw
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER
newgrp dockerInstall Caddy using your preferred package method. If Caddy is already running other apps on the server, keep it running and add a new site block for Nudgra.
Open the firewall:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enablecd /opt
sudo git clone https://github.com/MaikoCode/nudgra_oss.git nudgra
sudo chown -R $USER:$USER /opt/nudgra
cd /opt/nudgra
cp .env.example .envnano .envSet:
SITE_URL=https://your-domain.com
BETTER_AUTH_URL=https://your-domain.com
TRUSTED_ORIGINS=https://your-domain.com
TRUST_PROXY_HEADERS=true
BETTER_AUTH_SECRET=your-generated-secret
TOKEN_ENCRYPTION_KEY=your-generated-token-secret
OPERATOR_EMAIL_ALLOWLIST=your-google-email@gmail.com
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
META_APP_ID=your-instagram-app-id
META_APP_SECRET=your-instagram-app-secret
META_VERIFY_TOKEN=your-generated-webhook-verify-tokenFor production, also change the Postgres password in docker-compose.yml and update DATABASE_URL inside the app.environment section to match. Keep a private copy of .env β it is required to decrypt stored OAuth and Instagram tokens.
TRUST_PROXY_HEADERS=true is recommended only when the app port is private and traffic reaches the container through a trusted reverse proxy such as Caddy. If you expose the app container directly to the internet, leave it false.
Recommended: bind the app port to localhost only:
ports:
- "127.0.0.1:3000:3000"docker compose up -d --build
docker compose logs -f appCheck the local health endpoint:
curl http://127.0.0.1:3000/api/v1/healthThe app container runs database migrations automatically before startup.
sudo nano /etc/caddy/CaddyfileAdd a site block:
your-domain.com {
reverse_proxy 127.0.0.1:3000
}If your Caddy server is already bound to a specific public IP for other apps, add bind to avoid port conflicts:
your-domain.com {
bind YOUR_VPS_PUBLIC_IP
reverse_proxy 127.0.0.1:3000
}Then validate and reload:
sudo caddy fmt --overwrite /etc/caddy/Caddyfile
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddyIf reload hangs or reports address already in use, inspect listeners:
sudo ss -tulpn | grep -E ':80|:443'
sudo systemctl status caddy --no-pager -lFinally, check HTTPS β you want a 200 response:
curl -I https://your-domain.com/api/v1/healthGo to the Google Cloud Console and create OAuth credentials for a web application.
For production, set:
Authorized JavaScript origin:
https://your-domain.com
Authorized redirect URI:
https://your-domain.com/api/auth/callback/google
For local development, set:
Authorized JavaScript origin:
http://localhost:3000
Authorized redirect URI:
http://localhost:3000/api/auth/callback/google
If Google asks for an authorized domain, use the root domain, for example example.com.
Copy the client ID and secret into .env:
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...Restart after changing .env:
docker compose up -d --build # production
npm run dev # local developmentMeta's dashboard changes often, so labels may move slightly. The important pieces stay the same: create a Meta app, add Instagram testers, publish the app, configure webhooks, configure Instagram business login, copy the credentials into .env, and connect the account from Nudgra.
Go to https://developers.facebook.com/apps/, sign up if needed, then click Create App.
During setup:
- Give the app a name.
- For the use case, select Manage messaging & content on Instagram.
- Finish the app creation flow.
Inside your Meta app dashboard:
- Go to App roles.
- Click Roles (not Test users).
- Click Add People.
- In the modal, choose Instagram Tester.
- Add the Instagram accounts you want to automate with Nudgra.
Then each Instagram account must accept the invite:
- Open Instagram in a desktop browser.
- Go to Settings β Website permissions β Apps and Websites β Test invites.
- Accept the invite.
The Website permissions area may not appear on mobile, so use Instagram on a computer.
In the Meta app dashboard:
- Go to Use cases.
- Open the Instagram use case.
- Go to API setup with Instagram login.
- In Add required messaging permissions, add the permissions Nudgra needs:
instagram_business_basic
instagram_business_manage_messages
instagram_business_manage_comments
You do not need to manually generate access tokens in Meta. Nudgra performs the Instagram login flow and stores encrypted tokens itself.
Webhooks require the app to be published. For testing your own connected Instagram accounts, you can publish with basic app details.
- Go to Publish.
- Add a privacy policy URL.
- Fill any required basic fields.
- Publish the app.
For testing, a simple public privacy policy page is enough β for example, a published Notion page.
Return to your app at https://developers.facebook.com/apps/, then:
- Go to Use cases.
- Click Customize or open the Instagram use case.
- Open API setup with Instagram login.
- Find and copy the Instagram app ID.
- Reveal and copy the Instagram app secret.
Put them in .env:
META_APP_ID=your-instagram-app-id
META_APP_SECRET=your-instagram-app-secret
META_VERIFY_TOKEN=your-random-webhook-verify-tokenGenerate a verify token yourself β any strong random string works:
openssl rand -hex 32
# or
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"Restart the app after editing .env:
docker compose up -d --buildIn API setup with Instagram login, open Configure webhooks and set:
Callback URL: https://your-domain.com/api/meta/webhooks
Verify token: same value as META_VERIFY_TOKEN in .env
Click Verify and save, then subscribe to these webhook fields:
messages
messaging_postbacks
comments
The app expects Instagram webhook events and requires valid Meta signatures in production. Unsigned webhook POSTs are accepted only outside production, for local development.
In API setup with Instagram login, open Set up Instagram business login and set:
Redirect URL: https://your-domain.com/api/meta/callback
Save the redirect URL.
Open your deployment at https://your-domain.com and sign in with the Google account in OPERATOR_EMAIL_ALLOWLIST. Then:
- Go to Manage Accounts.
- Click Add account.
- Complete the Instagram permission screen.
- Allow profile/media access, comment access, and message access.
- Confirm the account appears as connected and active.
cd /opt/nudgra
git pull
docker compose up -d --build
docker compose logs -f appCreate a compressed Postgres backup:
docker compose exec -T postgres pg_dump -U nudgra -d nudgra --format=custom > nudgra.backupRestore into an empty database:
docker compose exec -T postgres pg_restore -U nudgra -d nudgra --clean --if-exists < nudgra.backupBack up
.envseparately. It contains the auth and token-encryption secrets needed to decrypt stored OAuth and Instagram tokens.
curl https://your-domain.com/api/v1/health does not return 200
docker compose ps
docker compose logs app
curl http://127.0.0.1:3000/api/v1/health
sudo systemctl status caddy --no-pager -l
sudo journalctl -u caddy -n 100 --no-pagerCaddy says address already in use
Check what owns the ports:
sudo ss -tulpn | grep -E ':80|:443'If another Caddy site is already bound to a public IP, use bind YOUR_VPS_PUBLIC_IP in the Nudgra site block. If Nginx is running and you do not use it, stop it:
sudo systemctl stop nginx
sudo systemctl disable nginxGoogle login redirects to the wrong URL
Make sure all of these match the real domain:
SITE_URL=https://your-domain.com
BETTER_AUTH_URL=https://your-domain.com
TRUSTED_ORIGINS=https://your-domain.comAlso verify the Google OAuth redirect URI:
https://your-domain.com/api/auth/callback/google
Meta webhook verification fails
Check that:
META_VERIFY_TOKENin.envexactly matches the token entered in Meta.https://your-domain.com/api/meta/webhooksis reachable publicly.- The app was restarted after
.envchanges. - The Meta app is published.
- The subscribed webhook fields are
messages,messaging_postbacks, andcomments.
curl -i https://your-domain.com/api/meta/webhooks
docker compose logs -f appA plain browser or curl request to the webhook URL may return 403 because it is missing Meta's verification query parameters. That still proves the route is reachable β Meta's Verify and save button is the real webhook verification.
Instagram account does not appear during connection
Check that:
- The Instagram account accepted the tester invite.
- The invite was accepted from Instagram on desktop.
- The app has the required Instagram permissions.
- The Instagram account is professional (business or creator).
- You are logged into the correct Instagram account when authorizing.
Mutations fail with Forbidden origin
Add the exact origin to TRUSTED_ORIGINS, then restart:
TRUSTED_ORIGINS=https://your-domain.comnpm run build
npm run start
npm run typecheck
npm run test
npm run test:visual
npm run db:check
npm run db:generate
npm run db:migrate
npm run db:studio- Better Auth OAuth tokens are encrypted before storage. Instagram Graph API tokens are encrypted separately with
TOKEN_ENCRYPTION_KEYwhen set, falling back toBETTER_AUTH_SECRETfor local development. - Cookie-authenticated API mutations reject requests whose
Originis not inTRUSTED_ORIGINS,SITE_URL, orBETTER_AUTH_URL. - In production, dashboard access requires
OPERATOR_EMAIL_ALLOWLIST; if it is empty, authenticated users receive access denied instead of getting a workspace. - Instagram long-lived token refreshes, contact profile refreshes, delayed follow-ups, and outbound deliveries are handled with pg-boss. Delayed jobs use the
PG_BOSS_SCHEMAPostgres schema, defaulting topgboss. - Dashboard realtime invalidation uses
/api/v1/eventswith PostgresLISTEN/NOTIFY;REALTIME_HEARTBEAT_SECONDScontrols SSE keepalives. SetREALTIME_POSTGRES_DISABLED=trueonly for tests or single-process debugging.
- Contributing β see CONTRIBUTING.md. Bug fixes, tests, docs, and UI polish are all welcome.
- Security β please report vulnerabilities privately. See SECURITY.md.
- License β MIT. The MIT license does not grant rights to the
Nudgraname or branding; see TRADEMARKS.md.
If Nudgra is useful to you, consider starring the repo β β it genuinely helps.
Built with TanStack Start Β· Hono Β· Drizzle Β· pg-boss Β· Postgres