Claude-code-plugins-plus algolia-multi-env-setup
install
source · Clone the upstream repo
git clone https://github.com/jeremylongshore/claude-code-plugins-plus-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/jeremylongshore/claude-code-plugins-plus-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/saas-packs/algolia-pack/skills/algolia-multi-env-setup" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-algolia-multi-env-setup && rm -rf "$T"
manifest:
plugins/saas-packs/algolia-pack/skills/algolia-multi-env-setup/SKILL.mdsource content
Algolia Multi-Environment Setup
Overview
Algolia doesn't have built-in environment separation. You either use separate Algolia applications (strongest isolation) or index prefixing within one application (simpler). This skill covers both approaches.
Environment Strategies
| Strategy | Isolation | Cost | Complexity |
|---|---|---|---|
| Index prefixing | Shared app, prefixed names | Lowest | Low |
| Separate API keys | Shared app, scoped keys | Low | Medium |
| Separate applications | Full isolation | Highest | High |
Instructions
Step 1: Index Prefixing (Recommended for Most Teams)
// src/algolia/config.ts import { algoliasearch, type Algoliasearch } from 'algoliasearch'; type Environment = 'development' | 'staging' | 'production'; interface AlgoliaConfig { appId: string; apiKey: string; searchKey: string; environment: Environment; } function getConfig(): AlgoliaConfig { const env = (process.env.NODE_ENV || 'development') as Environment; return { appId: process.env.ALGOLIA_APP_ID!, apiKey: process.env.ALGOLIA_ADMIN_KEY!, searchKey: process.env.ALGOLIA_SEARCH_KEY!, environment: env, }; } // Prefix index names with environment export function indexName(base: string): string { const { environment } = getConfig(); if (environment === 'production') return base; // No prefix in prod return `${environment}_${base}`; // development_products, staging_products, products } let _client: Algoliasearch | null = null; export function getClient(): Algoliasearch { if (!_client) { const config = getConfig(); _client = algoliasearch(config.appId, config.apiKey); } return _client; }
Step 2: Scoped API Keys Per Environment
import { algoliasearch } from 'algoliasearch'; const adminClient = algoliasearch(process.env.ALGOLIA_APP_ID!, process.env.ALGOLIA_ADMIN_KEY!); // Create environment-scoped keys that can ONLY access their own indices async function createEnvironmentKeys() { // Staging key: can only access staging_* indices const { key: stagingKey } = await adminClient.addApiKey({ apiKey: { acl: ['search', 'addObject', 'deleteObject', 'editSettings', 'browse'], description: 'Staging environment — full access to staging indices only', indexes: ['staging_*'], maxQueriesPerIPPerHour: 10000, }, }); console.log(`Staging key: ${stagingKey}`); // Dev key: can only access development_* indices const { key: devKey } = await adminClient.addApiKey({ apiKey: { acl: ['search', 'addObject', 'deleteObject', 'editSettings', 'browse'], description: 'Development environment — full access to dev indices only', indexes: ['development_*'], maxQueriesPerIPPerHour: 5000, }, }); console.log(`Dev key: ${devKey}`); // Production search key: search only, restricted const { key: prodSearchKey } = await adminClient.addApiKey({ apiKey: { acl: ['search'], description: 'Production search — read only', indexes: ['products', 'articles', 'faq'], maxQueriesPerIPPerHour: 50000, maxHitsPerQuery: 100, }, }); console.log(`Prod search key: ${prodSearchKey}`); }
Step 3: Environment Variables Per Platform
# .env.development ALGOLIA_APP_ID=YourAppID ALGOLIA_ADMIN_KEY=dev_scoped_key_here ALGOLIA_SEARCH_KEY=dev_search_key_here NODE_ENV=development # .env.staging ALGOLIA_APP_ID=YourAppID ALGOLIA_ADMIN_KEY=staging_scoped_key_here ALGOLIA_SEARCH_KEY=staging_search_key_here NODE_ENV=staging # Production: use secret manager, not env files # GitHub Actions: # ALGOLIA_ADMIN_KEY: ${{ secrets.ALGOLIA_ADMIN_KEY_PROD }} # GCP Secret Manager: # gcloud secrets versions access latest --secret=algolia-admin-key # Vercel: # vercel env add ALGOLIA_ADMIN_KEY production
Step 4: Settings-as-Code with Environment Overrides
// config/algolia-settings.ts import type { IndexSettings } from 'algoliasearch'; const baseSettings: IndexSettings = { searchableAttributes: ['name', 'brand', 'category', 'unordered(description)'], attributesForFaceting: ['searchable(brand)', 'category', 'filterOnly(price)'], customRanking: ['desc(review_count)', 'desc(rating)'], }; const envOverrides: Partial<Record<string, Partial<IndexSettings>>> = { development: { // Faster iteration: no replicas in dev replicas: [], }, staging: { // Mirror prod replicas for testing replicas: ['virtual(staging_products_price_asc)'], }, production: { replicas: [ 'virtual(products_price_asc)', 'virtual(products_price_desc)', 'virtual(products_newest)', ], }, }; export function getSettings(env: string): IndexSettings { return { ...baseSettings, ...envOverrides[env] }; }
Step 5: Environment Isolation Guard
// Prevent accidental cross-environment operations export function guardEnvironment(operation: string, targetIndex: string) { const env = process.env.NODE_ENV || 'development'; if (env === 'production') { // In production, block access to dev/staging indices if (targetIndex.startsWith('development_') || targetIndex.startsWith('staging_')) { throw new Error(`Blocked: ${operation} on ${targetIndex} from production`); } } else { // In dev/staging, block access to production indices (no prefix = production) if (!targetIndex.startsWith(`${env}_`)) { throw new Error(`Blocked: ${operation} on ${targetIndex} from ${env}. Use prefixed index.`); } } } // Usage in service layer async function deleteIndex(name: string) { guardEnvironment('deleteIndex', name); await getClient().deleteIndex({ indexName: name }); }
Step 6: Seed Script Per Environment
// scripts/seed-environment.ts import { getClient, indexName } from '../src/algolia/config'; import { getSettings } from '../config/algolia-settings'; async function seedEnvironment() { const env = process.env.NODE_ENV || 'development'; const client = getClient(); const idx = indexName('products'); console.log(`Seeding ${env} environment → index: ${idx}`); // Apply settings await client.setSettings({ indexName: idx, indexSettings: getSettings(env) }); // Seed data (dev/staging only) if (env !== 'production') { const testData = await import('../fixtures/products.json'); const { taskID } = await client.replaceAllObjects({ indexName: idx, objects: testData.default, }); await client.waitForTask({ indexName: idx, taskID }); console.log(`Seeded ${testData.default.length} records`); } } seedEnvironment().catch(console.error);
Error Handling
| Issue | Cause | Solution |
|---|---|---|
| Wrong index in production | Missing prefix logic | Use helper everywhere |
| Staging data leaking to prod | Shared API key | Use scoped keys restricted to index patterns |
| Settings drift between envs | Manual dashboard changes | Apply settings from code in CI |
| Dev index polluting record count | Old test indices | Scheduled cleanup job for indices |
Resources
Next Steps
For observability setup, see
algolia-observability.