@Datalayer brings a Datalayer application into a Discord server: /ask in a channel or thread, the Ask Datalayer message command, /datalayer for a person's link and thread, and /datalayer-admin for the server. Discord is a host of Datalayer's existing session and approval contracts (the shared host kit in agent-runtimes' src/portable/hosts); it is not a separate agent runtime.
The user and administrator documentation is at datalayer.ai/docs/discord. The service's settings, roles, database and image are in docs/operations.md; the decisions in docs/adr/. discord:0.1.0 runs on r1 (the ingress and worker roles) at https://discord-dev.datalayer.run for Datalayer Dev; the pilot's host https://discord.datalayer.run has its certificate and waits on the pilot application (DC-501).
There are two applications, both owned by a Discord team, never by a personal account (verification later asks for the team owner's identity):
| Application | Used for | Public bot | Endpoints |
|---|---|---|---|
Datalayer Dev |
Development, on r1 (https://discord-dev.datalayer.run) |
off | https://discord-dev.datalayer.run |
Datalayer |
The r1 pilot, the service deployed beside Slack's | on | the service's host name on r1 (DC-501) |
Do the steps below once for each application. Keep every value you copy in a password manager until it reaches .env (development) or the Secret (r1); never paste one into an issue, a chat, a commit or the shell's history.
- Turn on two-factor authentication on your Discord account (User Settings β My Account β Enable Authenticator App). Team administration and verification need it.
- Open the Developer Portal, choose Teams β New Team, name it
Datalayerand create it. - Invite the other developers (Invite Member, by Discord username), with the Developer role for those who change settings and Read-only for the others. Only the owner and admins see tokens and secrets.
- Applications β New Application.
- Name:
Datalayer Dev(development) orDatalayer(the pilot). Team:Datalayer. Accept the Developer Terms of Service and Developer Policy, then Create.
- App icon (at least 512Γ512) and Description: Ask your Datalayer applications from Discord.
- Copy Application ID β
DATALAYER_DISCORD_APPLICATION_IDand Public Key βDATALAYER_DISCORD_PUBLIC_KEY(64 hex characters). Neither is secret, but each belongs to its application. - Terms of Service URL:
https://datalayer.ai/terms. Privacy Policy URL:https://datalayer.ai/privacy. - Leave Interactions Endpoint URL empty for now: Discord saves it only once the running service answers its PING (step 8).
- Save Changes.
- Installation Contexts: Guild Install only. Untick User Install.
- Install Link: Custom URL,
<DATALAYER_DISCORD_APP_URL>/discord/install(Datalayer Dev:https://discord-dev.datalayer.run; the pilot:https://discord.datalayer.run; both run on r1 with cert-manager certificates; a tunnel origin only when you run the service on your own machine, step 7). A Discord Provided Link cannot carry Datalayer's bind code, and None would bar the App Directory later. - Save Changes.
- Client Secret β Reset Secret, confirm, and copy it β
DATALAYER_DISCORD_CLIENT_SECRET. Discord shows it once; a reset ends the previous one. - Redirects β Add Redirect:
<DATALAYER_DISCORD_APP_URL>/discord/oauth/callback, exactly (scheme, host and path). Save Changes. - Do not build a URL with the OAuth2 URL Generator: the service builds the install link itself (
bot applications.commands, guild install, permissions309237763136, computed insrc/permissions.ts).
- Username:
Datalayer DevorDatalayer, and the same icon. - Reset Token, confirm, and copy it β
DATALAYER_DISCORD_BOT_TOKEN. Discord shows it once; a reset ends the previous one. - Public Bot: off for
Datalayer Dev(only the team adds it), on forDatalayer(any server owner adds it from the Ship tab). - Requires OAuth2 Code Grant: on, so every install passes through the service's callback, which binds the server to Datalayer.
- Privileged Gateway Intents: all three off (Presence, Server Members, Message Content; ADR 0003).
- Save Changes.
Discord checks each endpoint by calling it, so the service must run first.
Development, from datalayer-osp/src:
cd tech/datalayer/discord
cloudflared tunnel --url http://localhost:3000 # or: ngrok http 3000Keep the tunnel running and take its https://β¦ origin as DATALAYER_DISCORD_APP_URL (a new origin at each restart: update steps 4, 5 and 8 when it changes). In a second terminal, create .env (ignored by Git), readable by you alone, with a fresh encryption key that is never printed:
cd tech/datalayer/discord
umask 077
printf 'DATALAYER_DISCORD_TOKEN_ENCRYPTION_KEY=%s\n' "$(openssl rand -base64 32)" > .envThen open .env in an editor and add the rest, pasting each value from the password manager (never echo one: it would stay in the shell's history):
NODE_ENV=development
PORT=3000
DATALAYER_DISCORD_ROLE=all
DATALAYER_DISCORD_APPLICATION_ID=
DATALAYER_DISCORD_PUBLIC_KEY=
DATALAYER_DISCORD_BOT_TOKEN=
# The OAuth2 page's client secret (Reset Secret shows it once).
DATALAYER_DISCORD_CLIENT_SECRET=
DATALAYER_DISCORD_TOKEN_ENCRYPTION_KEY_VERSION=1
DATALAYER_DISCORD_APP_URL=https://<your tunnel>
DATALAYER_API_URL=https://r1.datalayer.run
DATALAYER_WEB_URL=https://datalayer.aiWithout DATABASE_URL development keeps everything in memory (a restart forgets installs and links); set it to a local PostgreSQL and run npm run migrate to keep them. Then start the service:
npm run dev
curl --fail https://<your tunnel>/healthzIn the monorepo, agent-runtimes' watch:lib serves lib/portable; in a standalone clone, run npm install then npm run fetch:agent-runtimes first. The service refuses to start and names each missing or malformed setting.
The r1 pilot: put the four values in the r1 Secret datalayer-discord-env of the datalayer-api namespace, under the setting names (DATALAYER_DISCORD_APPLICATION_ID, DATALAYER_DISCORD_PUBLIC_KEY, DATALAYER_DISCORD_BOT_TOKEN, DATALAYER_DISCORD_CLIENT_SECRET), from a file that never enters Git and is removed after:
kubectl -n datalayer-api create secret generic datalayer-discord-env \
--from-env-file=/path/to/discord-r1.env
shred -u /path/to/discord-r1.envThen set DATALAYER_DISCORD_ENABLED=true in ~/.datalayer/datalayerrc-r1 and run the p reup the builder hands over with the chart (DC-501). Go on once https://<host>/readyz answers 200.
Once the service answers on its host (https://discord-dev.datalayer.run for Datalayer Dev, https://discord.datalayer.run for the pilot; both run on r1 with Let's Encrypt certificates), open the application at discord.com/developers/applications and fill these four fields, each followed by Save Changes (the green bar at the bottom):
| Left menu | Field | Datalayer Dev |
The pilot (Datalayer) |
|---|---|---|---|
| General Information | Interactions Endpoint URL | https://discord-dev.datalayer.run/discord/interactions |
https://discord.datalayer.run/discord/interactions |
| Webhooks | Endpoint URL, then switch Events on and tick Application Authorized only | https://discord-dev.datalayer.run/discord/events |
https://discord.datalayer.run/discord/events |
| OAuth2 | Redirects β Add Redirect | https://discord-dev.datalayer.run/discord/oauth/callback |
https://discord.datalayer.run/discord/oauth/callback |
| Installation | Install Link β Custom URL | https://discord-dev.datalayer.run/discord/install |
https://discord.datalayer.run/discord/install |
- Discord saves the Interactions Endpoint URL only when the service answers its test: a PING it must answer, and a request with a bad signature it must refuse. If saving fails, the service is not running or its
DATALAYER_DISCORD_PUBLIC_KEYis not this application's. - Discord also tests the Webhooks endpoint when events are switched on. Application Deauthorized stays unticked: it names no server.
- On the Bot page, check two switches: Requires OAuth2 Code Grant on (every install passes through
/discord/oauth/callbackand is bound to a Datalayer organization), and Public Bot off forDatalayer Dev(on for the pilot only when other servers may install it).
Check what Discord saved, from the bot token (it prints the settings, never the token):
curl -sS -H "Authorization: Bot ${DATALAYER_DISCORD_BOT_TOKEN}" https://discord.com/api/v10/applications/@me \
| jq '{interactions_endpoint_url, event_webhooks_url, event_webhooks_status, event_webhooks_types, redirect_uris, custom_install_url, bot_require_code_grant, bot_public, integration_types_config}'Expected: the four URLs above, event_webhooks_status: 2 (enabled) with ["APPLICATION_AUTHORIZED"], bot_require_code_grant: true, bot_public: false for Datalayer Dev, and integration_types_config with key "0" only (guild install). The webhook events can also be switched on from the bot token, if the portal toggle was missed:
curl -sS -X PATCH -H "Authorization: Bot ${DATALAYER_DISCORD_BOT_TOKEN}" -H 'content-type: application/json' \
https://discord.com/api/v10/applications/@me -d '{"event_webhooks_status":2,"event_webhooks_types":["APPLICATION_AUTHORIZED"]}'The commands are defined in commands/commands.json and registered with one bulk overwrite (never one create per command). Turn on User Settings β Advanced β Developer Mode in Discord, right-click the test server and Copy Server ID, then:
set -a; . ./.env; set +a
npm run commands:schema # offline: within Discord's limits
npm run commands:register -- --guild <server id> # development: one server, at once
npm run commands:check -- --guild <server id> # what Discord has equals the fileServer commands appear at once but not in direct messages. For the pilot, register globally (no --guild) and check with npm run commands:check -- --public-bot (when its Public Bot is on). The check also compares the application's settings of step 8 with commands/application.json ({origin} is DATALAYER_DISCORD_APP_URL) and names each one Discord holds differently, such as the Bot page's two switches. The script reads DATALAYER_DISCORD_APPLICATION_ID, DATALAYER_DISCORD_BOT_TOKEN and DATALAYER_DISCORD_APP_URL and prints neither secret.
- An install starts in Datalayer, never from the portal: open the application in the Studio, Ship β the Discord card β Add @Datalayer to a server. It sets up a host installation and opens
<DATALAYER_DISCORD_APP_URL>/discord/install?datalayer_bind=β¦, good once, for ten minutes. While the service is unpublished the card offers no link (DATALAYER_DISCORD_APP_URL = ''in the landing'ssrc/views/studio/apps/appDiscordSurface.ts): for development, set it to your tunnel origin in a local landing, and never commit that. - Discord's authorization page lists the permissions; pick the test server and Authorize. The callback binds the server at Datalayer and keeps its host key encrypted. A page saying the install did not finish means the code was spent or expired: start again from the Ship tab.
- In the server, run
/datalayer link, open the link it answers (only you see it), sign in to Datalayer and choose Link. /datalayer statusshows the connection, your link and the application. Then/ask question: Summarize the launch risksin a channel: the answer comes in a thread started from it.- A second member account, unlinked, should be told privately to link first.
User and administrator documentation is published at datalayer.ai/docs/discord.
| Symptom | Check |
|---|---|
| The portal will not save the interactions URL | The service runs, /healthz answers through the tunnel, and DATALAYER_DISCORD_PUBLIC_KEY is this application's. |
| Discord sent no code after authorizing | Bot β Requires OAuth2 Code Grant is off: switch it on. |
| Invalid OAuth2 redirect_uri on Discord's page | OAuth2 β Redirects must be <DATALAYER_DISCORD_APP_URL>/discord/oauth/callback exactly, with the same origin as DATALAYER_DISCORD_APP_URL. |
| The install page refuses | Start from the Ship tab: its code works once, for ten minutes. |
| The commands do not appear | Registered with --guild for that server? npm run commands:check -- --guild <id>; server commands never reach direct messages. |
/ask says a permission is missing |
A channel override denies one of the permissions of src/permissions.ts to the bot; the answer names it. |
| Everything is forgotten after a restart | Development without DATABASE_URL keeps memory only: set it and run npm run migrate. |
| The service will not start | Its first line names each missing or malformed setting (docs/operations.md). |
npm run checkFormatting, lint, type checking, the bundle-graph gate, the tests and the esbuild bundle; CI runs the same with PostgreSQL.