Deploy and operate Nook bots
Production token exchange, durable dedupe, zero-downtime rotation, rate limits, observability, upgrades, and incident response.
Production architecture
Separate interactive and background capacity even when both routes share one service:
Nook interaction delivery ──> /nook/interactions ──> reserved low-latency workers
Nook lifecycle/events ───────> /nook/events ───────> durable queue/background workers
│
├── encrypted installation-token store
├── durable idempotency store
└── Nook API client with bounded retries
Interactive work must not wait behind a large event backlog. Acknowledge within three seconds and move slow actions to your queue, authorized by the short-lived response token when a Nook follow-up is required.
Environment and secret contract
At minimum, provide:
PORT=3210
NOOK_API_BASE_URL=https://api.your-nook.example
NOOK_APP_ID=<public-app-id>
NOOK_CLIENT_SECRET=<secret-store-reference>
NOOK_SIGNING_SECRET=<secret-store-reference>
NOOK_PREVIOUS_SIGNING_SECRET=<only-during-rotation>
Do not bake values into a container image. Restrict secret access to the callback/token-exchange workload that needs it. Use separate production and development apps so a local tunnel never shares production credentials or installations.
Token exchange bootstrap
app.installed carries:
{
"type": "app.installed",
"installationId": "…",
"workspaceId": "…",
"tokenExchange": {
"code": "single-use-value",
"attemptId": "fresh-per-delivery-attempt",
"expiresAt": "…"
}
}
Exchange within ten minutes:
const response = await fetch(`${apiBaseUrl}/api/v1/apps/token-exchange`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
appId,
clientSecret,
tokenExchangeCode: callback.tokenExchange.code,
}),
});
Persist the returned token atomically under installationId before acknowledging durable success.
The token appears exactly once. Never log the callback code or response token.
Bootstrap decisions:
| Condition | Response |
|---|---|
| Usable installation token already stored | Ignore later install attempts |
Same attemptId already processed | Skip this HTTP retry |
Older code is APP_TOKEN_EXCHANGE_CODE_SUPERSEDED | Wait for the newer delivery attempt |
| Code is reused | Treat as interception; Nook revokes the token issued by that code |
| Code expired | Workspace admin resends installation callback for a fresh code |
| Two active tokens already exist | Revoke an old token, then retry while code remains valid |
Durable idempotency
Store these separately:
- processed callback
meta.idempotencyKeywith result/expiry; - processed install
tokenExchange.attemptId; - outgoing bot API idempotency key with canonical request digest and result;
- your own external-system idempotency key (for example Jira issue creation).
A database transaction or durable queue handoff should close the gap between callback acceptance and
side effects. If you return 2xx before durable acceptance, Nook will not know to retry after a crash.
Deployment checklist
- Trusted HTTPS, modern TLS, bounded body size, and correct raw-body capture.
- Interaction and event paths route only signed
POSTrequests. - Host clock alerting keeps the ±300-second replay window meaningful.
- Current/previous signing secrets and client secret come from a secret manager.
- Installation tokens are encrypted and isolated by installation ID.
- Persistent callback/install/outgoing idempotency exists.
- Interactive workers have reserved concurrency and a three-second latency alert.
- Background queue has bounded retry/backoff and dead-letter/operator visibility.
- Logs redact every credential prefix, authorization header, response token, raw callback body, message body, and webhook URL.
- Health checks do not require or expose customer content.
- Revocation callback deletes the corresponding token from every cache/store.
- Staging tests use a separate app and workspace.
Rate limits and retries
Default limits include:
| Surface | Default |
|---|---|
| Bot API per installation | 60 requests/min sustained, burst 120/min, 3600/hour |
| Bot API per app | 600 requests/min across installations |
| Interactions per installation | 60/min |
| Token exchange per installation | 5/min and 20/hour |
| Token exchange per app | 600/hour |
| Bot DMs per installation/user | 5/hour |
| Bot DMs per installation | 100/day |
Honor Retry-After. Add bounded jitter, cap total attempts and elapsed time, and preserve the
original logical idempotency key. Never automatically retry a non-idempotent external side effect
unless your own system can deduplicate it.
When token-exchange rate limiting would outlive the code TTL, Nook returns
APP_TOKEN_EXCHANGE_RETRY_WINDOW_EXCEEDED; request a fresh installation callback instead.
Observability without content leakage
Useful dimensions:
- app/version and opaque installation identifier;
- callback type and delivery class;
- safe Nook error code and request ID;
- latency, queue age, attempt number, response status;
- dedupe hit, follow-up count, rate-limit bucket class;
- token age/fingerprint hint, never token value;
- current/previous signing-secret version label, never secret or HMAC input.
Alert on signature failures, sustained three-second risk, exchange failures, permanently failed deliveries, queue age, repeated runtime-gate closure, rate-limit saturation, and callback pause. Keep raw message bodies and modal submissions out of general traces.
Publish an app upgrade
- Create a new immutable manifest version.
- Classify scope changes. New required scopes need explicit administrator consent.
- Verify endpoints and compatibility against current plus candidate payload schemas.
- Publish and preview the exact upgrade.
- Roll out to a controlled workspace and verify commands/events/components.
- Upgrade remaining installations according to policy.
- Rotate bot tokens when scopes were added; existing tokens never gain newly added scopes.
Removing a scope applies immediately to the intersection and may delete capability state such as active bot-DM consents. Code must degrade before the administrator applies the upgrade.
Rotate credentials
Signing secret
Deploy support for dual signatures first if necessary. Rotate in Developer Portal, configure new as current and old as previous, verify live callbacks, then remove old after the 24-hour overlap.
Client secret
Rotate, deploy the new value while the old remains valid during the 24-hour overlap, verify a fresh token exchange in a controlled installation, then remove old.
Installation bot token
Create a second token, deploy it to that installation, verify a bounded API call, and revoke the old token. Maximum two active tokens makes this a deliberate new → verify → revoke sequence.
Pause, disable, revoke, and delete
These states are different:
- Paused delivery: fix callback failures, then resume; do not reinstall to erase diagnostics.
- Installation disabled: runtime API and deliveries close until an administrator enables it.
- Installation revoked: tokens die and the bot user is deactivated for future authority while historical attribution remains.
- App suspended/deleted: runtime gate closes across installations according to platform policy.
On app.revoked, delete the installation token and cached authority locally. Do not delete business
records required for audit; mark them terminal and remove secret material.
Incident response
Suspected bot-token leak
- Revoke the affected installation token immediately.
- If scope is uncertain, disable/revoke the installation.
- Search safe audit/request IDs and provider logs without printing the token.
- Create a replacement token only after containment and scope review.
Suspected signing-secret leak
- Rotate the signing secret.
- Deploy new + previous only for the bounded overlap.
- Review invalid-signature and replay telemetry.
- Remove the compromised value from every secret store and build cache.
Duplicate side effect
Preserve the two signed callback metadata records and your idempotency rows. Determine whether the wrong key was used, persistence occurred after acknowledgement, or an external action lacked its own idempotency boundary. Do not “fix” by suppressing all retries.
Release acceptance tests
- valid and invalid raw-body signature vectors, stale timestamp, dual-secret overlap;
- URL verification on both endpoints;
- install code exchange, HTTP retry, superseded attempt, expired/reused code;
- command immediate response under three seconds and slow follow-up;
- component, modal, message, file, reaction, and event flows for granted scopes;
- missing optional scope graceful degradation;
- disallowed/private channel non-enumeration;
- DM consent, revocation, global preference, and uniform denial;
- 429 retry preserving idempotency and honoring
Retry-After; - installation disable/revoke and app suspend closing cached/runtime access;
- secret/token rotation overlap and old-authority removal;
- no credentials or customer content in logs, traces, errors, images, or support artifacts.
The public starter can be syntax-checked without access to Nook source code:
node --check server.mjs
Add your own signature vectors, callback fixtures, queue/database tests, and deployment smoke tests before production. The acceptance list above is the required behavioral contract, not the starter’s single-file structure.