Skip to content

MaikoCode/nudgra-oss

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

19 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Nudgra β€” own your Instagram automation

License: MIT TypeScript React 19 Postgres Self-hosted PRs welcome

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.


Watch the demo Β  Read the docs Β  Nudgra Cloud


What is Nudgra?

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, .env editing, Caddy configuration, and reading logs.

Questions or stuck? DM @Maikoke5.

✨ Features

πŸ’¬ 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.

βš™οΈ How it works

  1. Connect an Instagram professional account through Meta's Instagram Business Login β€” Nudgra runs the OAuth flow and stores encrypted tokens itself.
  2. Build comment, story-reply, or keyword-DM automations in the dashboard.
  3. React β€” Meta webhooks hit your server, Nudgra matches triggers, and sends replies, DMs, and link buttons.
  4. Follow up β€” delayed sequences, token refreshes, and outbound deliveries run on pg-boss jobs.
  5. Measure β€” watch runs, CTR, conversations, and logs from your dashboard.

πŸ“Œ Good to know

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

🧱 Tech stack

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

Table of contents

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.

What you need

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

Important URLs

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

Environment variables

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=1048576

Production 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.com

Generate secrets with OpenSSL:

openssl rand -base64 32
openssl rand -hex 32

Or 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 rotate BETTER_AUTH_SECRET or TOKEN_ENCRYPTION_KEY after 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.

πŸ’» 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.

1. Install dependencies

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

In Windows PowerShell:

git clone https://github.com/MaikoCode/nudgra_oss.git nudgra-oss
cd nudgra-oss
npm install
Copy-Item .env.example .env

2. Start Postgres

If 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-alpine

In 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-alpine

Your local .env can keep:

DATABASE_URL=postgres://nudgra:nudgra@localhost:5432/nudgra
DATABASE_SSL=false

3. Configure local .env

At 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.com

Google 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-secret

For local Google OAuth, use:

Authorized JavaScript origin:
http://localhost:3000

Authorized redirect URI:
http://localhost:3000/api/auth/callback/google

4. Run the app

npm run db:migrate
npm run dev

Open http://localhost:3000.

Useful local commands:

npm run typecheck
npm run test
npm run test:visual
npm run db:studio

πŸš€ Deploy to a VPS (Docker Compose + Caddy)

This is the recommended production setup.

1. Point DNS to the VPS

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

2. Install server packages

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 docker

Install 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 enable

3. Clone the app

cd /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 .env

4. Configure production .env

nano .env

Set:

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-token

For 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"

5. Start the app

docker compose up -d --build
docker compose logs -f app

Check the local health endpoint:

curl http://127.0.0.1:3000/api/v1/health

The app container runs database migrations automatically before startup.

6. Configure Caddy

sudo nano /etc/caddy/Caddyfile

Add 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 caddy

If reload hangs or reports address already in use, inspect listeners:

sudo ss -tulpn | grep -E ':80|:443'
sudo systemctl status caddy --no-pager -l

Finally, check HTTPS β€” you want a 200 response:

curl -I https://your-domain.com/api/v1/health

Google OAuth setup

Go 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 development

πŸ“· Meta / Instagram setup

Meta'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.

1. Create the Meta app

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.

2. Add Instagram testers

Inside your Meta app dashboard:

  1. Go to App roles.
  2. Click Roles (not Test users).
  3. Click Add People.
  4. In the modal, choose Instagram Tester.
  5. Add the Instagram accounts you want to automate with Nudgra.

Then each Instagram account must accept the invite:

  1. Open Instagram in a desktop browser.
  2. Go to Settings β†’ Website permissions β†’ Apps and Websites β†’ Test invites.
  3. Accept the invite.

The Website permissions area may not appear on mobile, so use Instagram on a computer.

3. Add permissions

In the Meta app dashboard:

  1. Go to Use cases.
  2. Open the Instagram use case.
  3. Go to API setup with Instagram login.
  4. 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.

4. Publish the Meta app

Webhooks require the app to be published. For testing your own connected Instagram accounts, you can publish with basic app details.

  1. Go to Publish.
  2. Add a privacy policy URL.
  3. Fill any required basic fields.
  4. Publish the app.

For testing, a simple public privacy policy page is enough β€” for example, a published Notion page.

5. Copy Instagram app credentials

Return to your app at https://developers.facebook.com/apps/, then:

  1. Go to Use cases.
  2. Click Customize or open the Instagram use case.
  3. Open API setup with Instagram login.
  4. Find and copy the Instagram app ID.
  5. 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-token

Generate 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 --build

6. Configure webhooks

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

7. Configure Instagram business login

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.

8. Connect Instagram in Nudgra

Open your deployment at https://your-domain.com and sign in with the Google account in OPERATOR_EMAIL_ALLOWLIST. Then:

  1. Go to Manage Accounts.
  2. Click Add account.
  3. Complete the Instagram permission screen.
  4. Allow profile/media access, comment access, and message access.
  5. Confirm the account appears as connected and active.

Updating a deployment

cd /opt/nudgra
git pull
docker compose up -d --build
docker compose logs -f app

Backup & restore

Create a compressed Postgres backup:

docker compose exec -T postgres pg_dump -U nudgra -d nudgra --format=custom > nudgra.backup

Restore into an empty database:

docker compose exec -T postgres pg_restore -U nudgra -d nudgra --clean --if-exists < nudgra.backup

Back up .env separately. It contains the auth and token-encryption secrets needed to decrypt stored OAuth and Instagram tokens.

Troubleshooting

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-pager
Caddy 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 nginx
Google 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.com

Also verify the Google OAuth redirect URI:

https://your-domain.com/api/auth/callback/google
Meta webhook verification fails

Check that:

  • META_VERIFY_TOKEN in .env exactly matches the token entered in Meta.
  • https://your-domain.com/api/meta/webhooks is reachable publicly.
  • The app was restarted after .env changes.
  • The Meta app is published.
  • The subscribed webhook fields are messages, messaging_postbacks, and comments.
curl -i https://your-domain.com/api/meta/webhooks
docker compose logs -f app

A 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.com

Useful commands

npm 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

Notes for operators

  • Better Auth OAuth tokens are encrypted before storage. Instagram Graph API tokens are encrypted separately with TOKEN_ENCRYPTION_KEY when set, falling back to BETTER_AUTH_SECRET for local development.
  • Cookie-authenticated API mutations reject requests whose Origin is not in TRUSTED_ORIGINS, SITE_URL, or BETTER_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_SCHEMA Postgres schema, defaulting to pgboss.
  • Dashboard realtime invalidation uses /api/v1/events with Postgres LISTEN/NOTIFY; REALTIME_HEARTBEAT_SECONDS controls SSE keepalives. Set REALTIME_POSTGRES_DISABLED=true only for tests or single-process debugging.

Contributing, security & license

  • 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 Nudgra name 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

Packages

 
 
 

Contributors

Languages