Build your first Nook bot
Create an app, run the standalone JavaScript starter, verify HTTPS callbacks, publish a manifest, and install it into a workspace.
The five-minute mental model
Nook Apps are external HTTPS services. Nook never executes third-party code. Your service:
- receives signed JSON callbacks for commands, components, modals, lifecycle, and events;
- responds to interactive callbacks within three seconds;
- exchanges an installation callback’s single-use code for a bot token;
- calls
/api/v1/app/*with that token; - sees only the scopes and channel policy the workspace administrator granted.
One published app can be installed into multiple workspaces. Each installation gets its own bot user, policy, granted scopes, and bot token. Never share a token between installations.
Prerequisites
- Node.js 22 for the zero-dependency starter;
- a Nook Cloud or self-hosted instance with
APPS_PLATFORM_ENABLED=true; - a developer account allowed by
APPS_DEVELOPER_POLICY; - an HTTPS URL reachable by the Nook API (or local HTTP explicitly allowed only in development);
- a workspace administrator who can review and install the app.
1. Create the app
Open Settings → Developer Portal → New App. Enter a stable name and the initial interaction/event URLs. Nook returns:
| Credential | Purpose | Handling |
|---|---|---|
| App ID | Public app identifier | Safe in manifest/config and token-exchange request |
Client secret (nookcs_…) | Exchanges installation codes | Shown once; secret store only |
Signing secret (nooksig_…) | Verifies callbacks | Shown once; secret store only |
Save both secrets before leaving the show-once screen. Nook stores the client secret as a hash and cannot display it again. Rotate rather than attempting to recover a lost value.
2. Download the standalone starter
No access to Nook source code or a Git provider is required. Download the public, zero-dependency Node 22 starter and its manifest directly from the Nook website:
mkdir nook-bot
cd nook-bot
curl --fail --silent --show-error \
--output server.mjs \
https://nook.cloudylake.io/downloads/nook-bot-starter/server.mjs
curl --fail --silent --show-error \
--output manifest.example.json \
https://nook.cloudylake.io/downloads/nook-bot-starter/manifest.example.json
install -m 0700 -d ./data
install -m 0600 /dev/null ./data/bot.env
${EDITOR:-vi} ./data/bot.env
set -a
. ./data/bot.env
set +a
node --check server.mjs
node server.mjs
Download server.mjs ·
Download manifest.example.json
Enter these values in ./data/bot.env, never directly on a shell command line:
PORT=3210
NOOK_API_BASE_URL=https://api.your-nook.example
NOOK_APP_ID=<app-id>
NOOK_CLIENT_SECRET=<client-secret>
NOOK_SIGNING_SECRET=<signing-secret>
NOOK_TOKEN_STORE_PATH=./data/tokens.json
Add data/ to your local ignore rules before entering credentials. For production, inject secrets
through your secret manager instead of an environment file. The example token file must be mode
0600; a real multi-instance service should use encrypted durable storage keyed by installation ID.
The starter is intentionally one auditable file with no npm dependency. Before production, split the same boundaries into focused modules:
server raw body, bounded HTTP routing, challenge
signature HMAC-SHA256 and 300-second replay window
lifecycle install/revoke and single-use token exchange
interactions commands, components, modals, follow-ups
nook-client token exchange and scoped bot API calls
token-store encrypted installation tokens and durable dedupe
3. Make the service reachable
Production callback URLs must use trusted HTTPS. For local development, use a tunnel such as your
approved Cloudflare Tunnel/ngrok equivalent, or set APPS_ALLOW_INSECURE_URLS=true only on a local
Nook development instance.
Configure distinct endpoints:
https://bot.example.com/nook/interactions
https://bot.example.com/nook/events
Interactive traffic has a strict latency budget and should not queue behind background events. Keeping the endpoints distinct also lets you scale and protect them independently.
4. Verify both endpoints
Click Verify endpoint for each URL. Nook sends a signed callback similar to:
{
"type": "url.verification",
"challenge": "opaque-short-lived-value"
}
Verify the signature on the raw body first, parse the shared schema, then answer within ten seconds:
{ "challenge": "opaque-short-lived-value" }
Do not create a generic unauthenticated challenge route. The verification callback uses the same signature boundary as real traffic.
5. Define the manifest
Paste a draft manifest in the Developer Portal’s raw JSON editor:
{
"schemaVersion": "2026-06-10",
"name": "Acme Helper",
"description": "Answers team commands and reacts to mentions.",
"bot": { "displayName": "Acme Helper" },
"commands": [
{
"name": "echo",
"description": "Echo a line of text into the channel",
"options": [{ "name": "text", "type": "string", "required": true, "maxLength": 200 }]
}
],
"events": ["app.mentioned"],
"scopes": ["commands:receive", "interactions:respond", "messages:write", "events:receive"],
"optionalScopes": ["reactions:write"]
}
Required scopes must include every capability needed by declared commands/events. Optional scopes may be declined, so your bot must feature-detect them. Ask for the smallest useful set; a broad scope request makes installation harder and increases incident impact.
6. Publish and install
- Save the draft and resolve every manifest validation error.
- Confirm both endpoint verifications are current.
- Publish. A published version is immutable; later changes create a new version.
- Open Workspace Settings → Apps, preview the app, and review required/optional scopes.
- Choose the channel policy (
all_publicor an explicit allowlist) and install. - Nook creates the installation’s bot user and sends
app.installedto the event endpoint.
The callback carries a fresh single-use token-exchange code with a ten-minute TTL. The starter
exchanges it automatically and stores the returned nookbot_… token exactly once.
7. Invoke the command
In an allowed channel, run:
/echo text: hello from Nook
The interaction callback contains the immutable manifest command name plus resolved typed options.
Dispatch on interaction.command.name, never the workspace-local invocation alias. Return:
{ "kind": "message", "text": "Echo: hello from Nook" }
The response appears as a normal bot-authored message with an APP badge. It follows the same channel history, search, permissions, reply, and reaction rules as other messages.
Minimal message API call
After installation, send through the token-derived workspace:
const response = await fetch(`${apiBaseUrl}/api/v1/app/messages`, {
method: 'POST',
headers: {
authorization: `Bearer ${botToken}`,
'content-type': 'application/json',
'idempotency-key': crypto.randomUUID(),
},
body: JSON.stringify({
channelId,
body: 'Hello from my bot',
}),
});
if (!response.ok) {
const failure = await response.json();
throw new Error(`Nook rejected the message: ${failure.error?.code ?? response.status}`);
}
Do not place a workspace ID in the path or body. The bot token determines its installation and workspace; the requested channel must also pass the installation’s current channel policy.
Quickstart acceptance checklist
- Both endpoints reject missing, stale, and invalid signatures before JSON parsing.
- URL verification succeeds on interaction and event URLs.
- Manifest publishes with the minimum required scopes.
- Workspace admin sees the exact requested scopes and channel policy before install.
-
app.installedexchange stores one installation token without logging it. -
/echoanswers within three seconds in an allowed channel. - The same command is denied or hidden outside installation policy.
- Duplicate event delivery does not repeat side effects.
- Revoking the installation immediately kills bot API access and deletes the local token copy.
Next, implement the non-negotiable security and callback rules.