This guide covers common issues when running AgentGate, organized by Symptom → Cause → Solution.
- Webhook Failures
- 429 Rate Limit Errors
- Decision Token Errors
- Slack Bot Not Posting
- Discord Bot Not Responding
- Policies Not Matching
- Database Migration Failures
- Docker Networking Issues
Cause: The webhook URL is unreachable from the server, or the target returns a non-2xx status code.
Solution:
- Check webhook delivery status via
GET /api/webhooks— each webhook tracks delivery attempts. - Verify the URL is reachable from the server container (see Docker Networking).
- Check that
WEBHOOK_TIMEOUT_MS(default:5000) is sufficient for your endpoint. Increase if your endpoint is slow:WEBHOOK_TIMEOUT_MS=10000
- Failed deliveries are retried via a DB-based retry scanner (runs every 30s) with exponential backoff:
2^attempt × 1000ms— so attempt 1 retries after ~2s, attempt 2 after ~4s, attempt 3 after ~8s. Maximum 3 total attempts (1 initial + 2 retries). Check server logs for retry attempts.
Cause: The secret used when creating the webhook doesn't match what the receiver is using to verify X-AgentGate-Signature.
Solution:
- The signature is
HMAC-SHA256(secret, raw_body)— ensure you're hashing the raw request body, not a parsed/re-serialized version. - Re-create the webhook with a known secret if unsure.
- If using
WEBHOOK_ENCRYPTION_KEY, note that this encrypts secrets at rest in the database — it does not affect the HMAC signature sent to your endpoint.
Cause: AgentGate validates webhook URLs against SSRF attacks. Private/internal IPs and non-HTTP(S) schemes are rejected.
Solution:
- Use a publicly routable URL or a DNS name that resolves to a public IP.
- In development, if you need to target
localhost, you may need to use the Docker service name (e.g.,http://host.docker.internal:4000).
Cause: The API key has exceeded its per-minute request limit.
Solution:
- Check the rate limit headers in the response:
X-RateLimit-Limit— max requests per minuteX-RateLimit-Remaining— remaining in current windowX-RateLimit-Reset— seconds until window resets
- Wait for the reset window, then retry.
- Increase the per-key rate limit via
POST /api/keys(setrateLimitto a higher value, ornullfor unlimited). - The global default is controlled by
RATE_LIMIT_RPM(default:60). Adjust if needed:RATE_LIMIT_RPM=120
- To disable rate limiting entirely (not recommended for production):
RATE_LIMIT_ENABLED=false
Cause: Rate limiting may be disabled, or using the memory backend which resets on server restart and doesn't share state across instances.
Solution:
- Verify
RATE_LIMIT_ENABLED=true(default). - For multi-instance deployments, switch to Redis backend:
RATE_LIMIT_BACKEND=redis REDIS_URL=redis://redis:6379
Cause: Decision tokens have a configurable TTL. Default is 24 hours (DECISION_TOKEN_EXPIRY_HOURS=24).
Solution:
- Request a new approval — the agent or user must create a new request.
- Increase the token expiry if your approval workflows are long-running:
DECISION_TOKEN_EXPIRY_HOURS=72
Cause: The token was already used, doesn't exist, or was cleaned up by the retention policy.
Solution:
- Each decision token is single-use. Once a request is approved or denied, all sibling tokens are cross-invalidated.
- Check if the request was already decided:
GET /api/requests/:id. - Expired tokens are cleaned up after
CLEANUP_RETENTION_DAYS(default:30).
Cause: Another approver (via dashboard, Slack, Discord, or another token) already decided this request.
Solution:
- This is expected behavior — AgentGate uses atomic conditional updates to prevent double-decisions. Check the request status via
GET /api/requests/:idto see who decided and when.
Cause: Missing or incorrect channel configuration, or the bot lacks permissions.
Solution:
- Ensure required environment variables are set:
SLACK_BOT_TOKEN=xoxb-... # Bot User OAuth Token SLACK_SIGNING_SECRET=... # From Slack app settings AGENTGATE_API_KEY=agk_... # API key for server communication
- Set a default channel:
SLACK_DEFAULT_CHANNEL=C01234ABCDE(use channel ID, not name). - Verify the bot is invited to the channel (
/invite @YourBotin Slack). - Check the bot has these OAuth scopes:
chat:write,channels:read,groups:read. - Review bot logs — the bot prints configuration status on startup (✓/✗ for each setting).
Cause: The SLACK_BOT_TOKEN environment variable is not set.
Solution:
- Set the token from your Slack app's OAuth & Permissions page.
- If using Docker secrets, use
SLACK_BOT_TOKEN_FILEpointing to the secret file.
Cause: Slack can't reach the bot's HTTP endpoint for interactivity.
Solution:
- The Slack bot runs on
SLACK_BOT_PORT(default:3001). Ensure this port is accessible from the internet (or use a tunnel like ngrok in development). - Set the Request URL in your Slack app's Interactivity settings to point to the bot.
Cause: Missing bot token, wrong channel ID, or insufficient bot permissions.
Solution:
- Ensure required environment variables are set:
DISCORD_BOT_TOKEN=... # Bot token from Discord Developer Portal AGENTGATE_URL=http://localhost:3000 # AgentGate server URL
- Set a default channel:
DISCORD_CHANNEL_ID=123456789012345678. - Verify the bot has these permissions in the target channel:
- Send Messages
- Embed Links
- Use External Emojis
- Read Message History
- Ensure the bot is added to your server with the correct OAuth2 scopes (
bot,applications.commands).
Cause: The DISCORD_BOT_TOKEN environment variable is not set.
Solution:
- Get the token from Discord Developer Portal → Bot → Token.
- If using Docker secrets, use
DISCORD_BOT_TOKEN_FILE.
Cause: DECISION_LINK_BASE_URL is not configured, so the bot can't generate clickable approve/deny URLs.
Solution:
- Set the base URL to your AgentGate server's public address:
DECISION_LINK_BASE_URL=https://gate.example.com
- To disable links entirely:
DISCORD_INCLUDE_LINKS=false.
Cause: Policy priority ordering, disabled policies, or match criteria not aligning with the request.
Solution:
- List policies via
GET /api/policiesand verify:- The policy is
enabled: true. - The
priorityis correct — lower numbers = higher priority. The first matching policy wins. - The
matchcriteria actually match the request'saction,params, orcontext.
- The policy is
- Check match syntax:
- Exact match:
{ "action": "send_email" } - Regex match:
{ "action": { "$regex": "^send_" } }— note that regex patterns are validated for ReDoS safety; overly complex patterns are rejected.
- Exact match:
- Review the audit trail (
GET /api/requests/:id/audit) to see which policy was evaluated.
Cause: AgentGate uses safe-regex2 to reject patterns vulnerable to ReDoS (Regular Expression Denial of Service).
Solution:
- Simplify the regex. Avoid nested quantifiers like
(a+)+or(a|b)*c*. - Use exact string matches when possible — they're faster and safer.
Cause: Migrations haven't been run, or there's a schema mismatch.
Solution:
- SQLite (default): Run migrations manually:
pnpm --filter @agentgate/server db:migrate
- PostgreSQL: Ensure the database exists and is reachable:
DB_DIALECT=postgres DATABASE_URL=postgresql://agentgate:agentgate@localhost:5432/agentgate
- In Docker, migrations run automatically on server startup. Check server logs for migration errors:
docker-compose logs server | grep -i migrat
Cause: The SQLite database file path is not writable.
Solution:
- Default path is
./data/agentgate.db. Ensure thedata/directory exists and is writable. - In Docker, the data directory is mounted as a volume. Verify volume permissions.
Cause: PostgreSQL is not running or the connection string is wrong.
Solution:
- Verify PostgreSQL is healthy:
docker-compose ps postgres docker-compose logs postgres
- Check
DATABASE_URLformat:postgresql://USER:PASSWORD@HOST:PORT/DBNAME. - In Docker Compose, use the service name as host:
postgresql://agentgate:agentgate@postgres:5432/agentgate.
Cause: Services are on different Docker networks, or using localhost instead of Docker service names.
Solution:
- Use Docker Compose service names for inter-container communication:
DATABASE_URL=postgresql://agentgate:agentgate@postgres:5432/agentgate REDIS_URL=redis://redis:6379
- All services must be on the same network. The default
docker-compose.ymlusesagentgate-internalfor backend services andagentgate-publicfor exposed services. - Never use
localhostinside containers to reach other containers —localhostrefers to the container itself.
Cause: The dashboard (nginx) proxies API requests to the server container. If the server isn't running or isn't on the same network, requests fail.
Solution:
- Verify both services are running:
docker-compose ps. - The dashboard typically expects the API at
http://server:3000via Docker networking. Check the nginx config in the dashboard image. - If accessing the dashboard from outside Docker, ensure
CORS_ALLOWED_ORIGINSincludes the dashboard's public URL.
Cause: Bot services (Slack, Discord) need to communicate with the server over the Docker network.
Solution:
- Set
AGENTGATE_URL=http://server:3000in the bot container environment. - Ensure bot services are on the
agentgate-internalnetwork (they should be if using the--profile botsflag). - Verify with:
docker-compose exec slack wget -qO- http://server:3000/health
curl http://localhost:3000/healthSet LOG_LEVEL=debug and LOG_FORMAT=json for detailed, parseable logs:
LOG_LEVEL=debug LOG_FORMAT=json pnpm --filter @agentgate/server devThe server validates all environment variables at startup using Zod schemas. If a required variable is missing or invalid, the error message will indicate which field failed validation.
For Docker deployments, use _FILE suffixed environment variables to read secrets from mounted files (e.g., ADMIN_API_KEY_FILE=/run/secrets/admin_api_key). The explicit env var takes precedence if both are set.
Run NODE_ENV=production to enable production validations. The server will warn if:
ADMIN_API_KEYis not setJWT_SECRETis not setCORS_ALLOWED_ORIGINSis not configuredWEBHOOK_ENCRYPTION_KEYis not set