Help us improve
Share bugs, ideas, or general feedback.
From algolia
API-key strategy for Algolia — admin vs. search vs. monitoring vs. analytics keys, scoped keys with explicit ACLs, the secured API key pattern (HMAC-derived per-request keys with embedded filters and expiry) for multi-tenant filtering, IP allowlists, rate-limit partitioning, key rotation, and the audit trail. Use this skill any time the user is touching keys — first integration, multi-tenant rollout, security audit, key rotation after a staff change, or "we got a 403, what changed." Keys are the surface where production search experiences fail; treat them with discipline.
npx claudepluginhub bpainter/composable-dxp-claude-marketplace --plugin algoliaHow this skill is triggered — by the user, by Claude, or both
Slash command
/algolia:algolia-api-keys-securityThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill puts you in the role of a platform engineer thinking about who gets to read what, and how to make that auditable. Default posture: **least-privilege everywhere, separate keys per workload, secured keys for any per-user filtering, rotate on staff changes.**
Guides technical evaluation of code review feedback: read fully, restate for understanding, verify against codebase, respond with reasoning or pushback before implementing.
Share bugs, ideas, or general feedback.
This skill puts you in the role of a platform engineer thinking about who gets to read what, and how to make that auditable. Default posture: least-privilege everywhere, separate keys per workload, secured keys for any per-user filtering, rotate on staff changes.
Pair with algolia-indexing-pipeline (the keys the indexer uses), algolia-search-client (the keys the runtime uses), and algolia-instantsearch-react / algolia-autocomplete (the keys that ship to the browser).
NEXT_PUBLIC_*. Not in CI logs. Not in screenshots.Algolia keys come in two layers:
| Key | What it does | Who sees it |
|---|---|---|
| Admin API Key | Full read/write/configure on everything | Server runtime, account owner only |
| Search-Only API Key | Read-only search across all indices | Sometimes the browser; usually a parent for secured keys |
| Monitoring API Key | Read monitoring endpoints | Observability tools |
| Usage API Key | Read billing and usage | Finance / ops dashboards |
| Analytics API Key | Read analytics endpoints | BI / dashboards |
The Admin key is the nuclear option. It can clearIndex, deleteIndex, change settings, rotate keys (yes, including itself), invite users. Never put it where it's not strictly needed.
Created via the Search API (POST /1/keys) or Dashboard. Each scoped key has:
search, browse, addObject, deleteObject, deleteIndex, settings, editSettings, analytics, usage, recommendation, logs, seeUnretrievableAttributes.maxQueriesPerIPPerHour, maxHitsPerQuery.validUntil (Unix seconds) for time-bound keys.Use scoped keys for every workload that isn't the runtime browser search. Examples:
| Workload | Recommended ACLs | Index restriction |
|---|---|---|
| Vercel webhook indexer | addObject, deleteObject, editSettings | The indices it manages |
| CI settings/rules deploy | editSettings, settings, analytics | The indices it deploys |
| MCP server (read-only ops) | search, browse, listIndexes, analytics | All |
| MCP server (write session) | search, browse, listIndexes, addObject, editSettings | Specific indices, time-bound |
| Algolia CLI in CI | Same as the CI use case | Specific |
| BI dashboard puller | analytics, usage | All |
| Dataset extractor | browse, search | Specific |
A secured API key is derived from a parent search key, plus extra parameters embedded and HMAC-signed. The user can't strip the filters.
import { algoliasearch } from 'algoliasearch';
const SEARCH_KEY = process.env.ALGOLIA_SEARCH_KEY!;
const securedKey = algoliasearch(APP_ID, SEARCH_KEY).generateSecuredApiKey({
parentApiKey: SEARCH_KEY,
restrictions: {
filters: 'tenantId:42 AND visibility:public',
validUntil: Math.floor(Date.now() / 1000) + 3600, // 1 hour
userToken: `user-${userId}`, // also pins userToken for Personalization
restrictIndices: ['articles', 'glossary_terms'],
restrictSources: '203.0.113.0/24', // optional IP cidr
},
});
// Pass `securedKey` to the front end. Use it as the API key in the lite client.
The browser then makes search calls with this key. Algolia validates the signature and merges the embedded filters with any front-end query — the user's filters can't loosen the embedded ones.
userToken into the key.validUntil for a temporary session.# .env (server only — never committed, never NEXT_PUBLIC)
ALGOLIA_APP_ID=ABC123
ALGOLIA_ADMIN_KEY= # Admin — only the indexer / CI / scripts
ALGOLIA_INDEXER_KEY= # Scoped: addObject, editSettings on specific indices
ALGOLIA_SETTINGS_DEPLOY_KEY= # Scoped: editSettings, settings, analytics
ALGOLIA_SEARCH_KEY= # Search-only — parent for secured keys
# .env (client — NEXT_PUBLIC required for browser bundling)
NEXT_PUBLIC_ALGOLIA_APP_ID=ABC123
NEXT_PUBLIC_ALGOLIA_SEARCH_KEY= # Search-only, low-privilege; OR omitted in favor of secured keys
Never NEXT_PUBLIC_ALGOLIA_ADMIN_KEY. The IDE's autocomplete shouldn't even see that name.
// app/search/page.tsx
import { generateSecuredApiKey } from 'algoliasearch';
import { getServerSession } from '@/lib/auth';
import { SearchClient } from './search-client';
export default async function SearchPage() {
const session = await getServerSession();
const tenantId = session?.user?.tenantId ?? null;
// Build the secured key on the server, fresh each render (or cached for 5 min)
const securedKey = tenantId
? generateSecuredApiKey(process.env.ALGOLIA_SEARCH_KEY!, {
filters: `tenantId:${tenantId} AND visibility:tenant`,
validUntil: Math.floor(Date.now() / 1000) + 3600,
userToken: `user-${session!.user!.id}`,
})
: process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_KEY!; // fallback for anonymous → public-only
return <SearchClient apiKey={securedKey} />;
}
The Client Component instantiates the lite client with the supplied key. The user can't see the filters; they can only see the opaque token.
For Server Actions or Route Handlers issuing the search themselves, just use the search key directly with the filters as normal request params — secured keys are about trusting the client, not the server.
Cadence:
- Quarterly for non-critical scoped keys.
- Immediately on staff offboarding for any key the leaver had access to.
- Immediately on suspected leak.
- Immediately if the key was logged anywhere it shouldn't be (CI logs, error tracking, screenshots).
Process:
1. Create the new key (Dashboard or API).
2. Deploy the new key to the runtime that uses it.
3. Verify the new key works.
4. Delete (or `validUntil`-set-to-now) the old key.
5. Watch for 403s in logs over the following hour.
Don't rotate by overwriting in place. Always create-deploy-verify-retire to avoid downtime.
Algolia's plans include burst tolerance, but a key in the wild that gets abused can spike usage and bill. Mitigations:
maxQueriesPerIPPerHour on browser-facing keys. Stops scrapers from running through your traffic at high QPS.maxHitsPerQuery caps the largest possible response. Stops hitsPerPage: 1000 abuse.referers: ['*.example.com/*']) on browser keys. Blocks usage from other origins. Not a security boundary (referer is spoofable) but a reasonable abuse deterrent.restrictSources) on server-only keys. Strong boundary for known runtimes (Vercel egress, CI runners).The Dashboard's Logs view shows recent operations per key. For deeper audit:
GET /1/logs returns the recent N operations per app. Pipe to your log warehouse.For high-compliance engagements, set up a daily cron that pulls Logs and pushes to the engagement's SIEM.
Per-environment applications (not just per-environment indices) is the recommended pattern. Why:
clearIndex mistake in staging stays in staging.Per-engagement, expect:
slalom-{client}-prodslalom-{client}-stagingslalom-{client}-devEach with its own admin key, its own search key, its own scoped keys for indexing and CI.
# Algolia Key Plan: [Engagement]
## Applications
- slalom-{client}-prod appId=...
- slalom-{client}-staging appId=...
## Keys (per application)
| Key | ACLs | Index restriction | Stored where | Rotation |
|---|---|---|---|---|
| Admin | * | * | 1Password (ops vault) | On staff change |
| Indexer | addObject, deleteObject, editSettings | articles*, glossary_terms* | Vercel env (server) | Quarterly |
| Settings deploy | editSettings, settings, analytics | * | GitHub Actions secret | Quarterly |
| Search-only | search | * | Vercel env (NEXT_PUBLIC) + parent for secured keys | Annually |
| MCP read | search, browse, listIndexes, analytics | * | Local (per-engineer) | On engagement end |
## Secured-key pattern
- When: on every search request from authenticated users
- Filter: `tenantId:{user.tenantId} AND ...`
- Expiry: 1 hour
- userToken: `user-{user.id}`
## Rotation runbook
- Trigger conditions
- Process (create new → deploy → verify → retire old)
- Verification (synthetic search call from new key)
## Audit
- Logs API → SIEM (daily)
- Quarterly key inventory review
NEXT_PUBLIC_*. Catastrophic. Rotate immediately if discovered.validUntil set far in the future on secured keys. A leaked secured key is valid until expiry; short windows minimize blast radius.validUntil longer than the user's session.algolia-search-client, algolia-instantsearch-react, algolia-autocomplete.algolia-indexing-pipeline.algolia-mcp-cli.userToken) → algolia-personalization-ai, algolia-analytics-events.../../references/algolia-foundations.md