Integrations
Discord
Discord connects one text channel to one AppNorth app. It is available on Pro and Studio; self-hosted instances with billing disabled also have access. Free workspaces can see the connection and disconnect it, but cannot install, send test messages or sync.
New chats and reports create a channel message and a thread. The message includes the app name, the reporter's email when provided (otherwise an anonymous-user label), the first message, available device/OS/app-version details, and a dashboard link. Missing device information is labelled unavailable. The chat transport currently supplies the platform and bundle ID, so chat messages may have fewer device details than reports.
Subsequent user messages and ordinary team replies are mirrored into the thread. Internal notes and paywall cards are not sent. Status changes appear as thread messages, including resolution. Reports created before connecting are not backfilled.
Connect a channel
Open Settings → Apps → your app → Integrations.
Select Connect Discord as an AppNorth owner or admin.
Authorize the bot in a server you own or can manage.
Choose a text channel and save it. Only channels where the bot has all required permissions appear.
Send a test message.
Use a channel restricted to your support team: reports include private customer messages and may include an email address. The selected server and channel are stored per app. To move to another channel or server, disconnect first. Existing Discord messages are retained, but pending deliveries and thread mappings are removed on disconnect; reconnecting starts with new conversations.
The onboarding Integrations step shows the current connection and opens the same settings page in a new tab. It refreshes when you return to the tab.
Reply from Discord
Post a text message in the report's thread. While the conversation is open, AppNorth imports it as a team reply, and the SDK receives it through its existing chat polling endpoint.
The Discord account that authorized installation is mapped to that AppNorth organization member. Re-authorizing an existing installation maps the current installer's Discord account as well. A mapping only applies while the user is still a member of that organization. Other Discord users must have View Channel and Manage Messages on the parent channel; their imported replies are attributed to Team. Bot messages, webhook messages and system events are ignored, preventing reply loops. Attachments, edits and deletions are not synchronized.
Resolved conversations are no longer polled. Reopen the conversation in AppNorth before sending further replies from Discord. Disconnecting stops all synchronization.
Configure the platform bot
Create an application and bot in the Discord Developer Portal. Set these server-side variables:
DISCORD_APPLICATION_ID=your_application_id
DISCORD_PUBLIC_KEY=your_application_public_key
DISCORD_BOT_TOKEN=your_bot_token
DISCORD_CLIENT_SECRET=your_oauth_client_secretThe platform bot token and OAuth client secret stay in the server environment. AppNorth does not store per-organization bot tokens or OAuth access tokens. The OAuth access token is used transiently to verify the installer and their server permissions; only installation IDs, member mappings and one-use hashed OAuth state are persisted. No credentials are sent to the browser.
Set the OAuth redirect URL to:
https://YOUR_APPNORTH_HOST/api/integrations/discord/callbackIt must match NEXT_PUBLIC_APP_URL. Enable the Message Content privileged intent in the bot settings, obtaining Discord approval when required. The worker verifies the application’s intent flags before reading messages, so missing access does not silently advance the reply cursor. The intent governs message content over REST as well as Gateway connections; see Discord's intent documentation.
The installation requests these bot permissions: View Channel, Send Messages, Embed Links, Read Message History, Create Public Threads and Send Messages in Threads. Channel permission overrides must also allow them. Forum, media and announcement channels are not supported.
The optional Interactions endpoint is:
https://YOUR_APPNORTH_HOST/api/integrations/discord/interactionsIt verifies Ed25519 signatures against the raw request body and rejects timestamps older than five minutes. Only Discord's PING is supported; normal messages do not arrive through Interactions. PING has no side effects, so replay does not create replies.
Run synchronization
Apply migration 0037_discord through the normal migration process. The tables, indexes, RLS policies and capture triggers are defined in that migration; the Discord repository uses parameterized SQL rather than generated Prisma models.
For the self-hosted Compose deployment, enable the dedicated HTTP poller:
docker compose -f compose.selfhost.yaml --profile discord up -dThe poller calls POST /api/integrations/discord/sync with Authorization: Bearer <CRON_SECRET>, waits for completion, and starts another pass after 30 seconds. An external scheduler can call the same endpoint. The web process needs the bot variables; the poller only needs CRON_SECRET.
The implementation uses HTTP polling rather than Gateway: it avoids a persistent WebSocket, reconnect/resume logic and another bot runtime. Thread cursors and incoming reply IDs survive process restarts. Each pass has a time budget and processes at most ten outgoing events and ten incoming pages per app; busy installations take longer than one interval. Organizations and threads are visited fairly across passes. At larger scale, a Gateway consumer could reduce polling requests without changing the outbox or reply deduplication.
Delivery and recovery
Database triggers capture new reports, messages and status changes in the same tenant transaction as the original write. Each pending delivery remains in discord_outbox until it completes. Transactions use withTenant; project advisory locks prevent concurrent workers and disconnects from racing.
Incoming message IDs become reply idempotency keys. Reply insertion and cursor advancement commit together. Outgoing sends use a stable nonce plus a persisted Discord message marker to recover after a process dies between a successful Discord request and a database commit. Thread creation reuses its starter message ID.
Failures retry with exponential backoff and jitter, up to one hour between ordinary retries. HTTP 429 responses honor Discord's retry_after and persist a bot-wide pause. Permission errors remain visible in settings and are retried after permissions are fixed. The worker rechecks the paid plan before delivery, including inbound replies.
History reconciliation scans up to 1,000 messages. If it cannot establish whether an event was already delivered within that bound, it records RECONCILE_LIMIT and retains the event rather than sending a possible duplicate. Completed outbox rows are removed after 30 days; pending events have no automatic discard deadline. Discord-side deletion of a sent message also removes its reconciliation marker, so delivery cannot be guaranteed exactly once after manual deletion.
Test coverage
The fake provider tests cover message → thread → reply → chat, long messages, status mirroring, unauthorized senders, bot-loop prevention and pagination. HTTP tests cover signatures, provider rate limits, OAuth/session boundaries, roles and plan gating. The opt-in worker.live.test.ts additionally exercises real Postgres capture triggers, rollback, RLS, concurrent replay, SDK chat reads, durable backoff and one-use OAuth state against a dedicated test database.
A real bot installation still needs a smoke test with your Discord application and channel.