Agent Skills: Algolia Multi-Environment Setup

|

UncategorizedID: jeremylongshore/claude-code-plugins-plus-skills/algolia-multi-env-setup

Install this agent skill to your local

pnpm dlx add-skill https://github.com/jeremylongshore/claude-code-plugins-plus-skills/tree/HEAD/plugins/saas-packs/algolia-pack/skills/algolia-multi-env-setup

Skill Files

Browse the full folder contents for algolia-multi-env-setup.

Download Skill

Loading file tree…

plugins/saas-packs/algolia-pack/skills/algolia-multi-env-setup/SKILL.md

Skill Metadata

Name
algolia-multi-env-setup
Description
|

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 indexName() 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 development_* indices |

Resources

Next Steps

For observability setup, see algolia-observability.