import { algoliasearch } from 'algoliasearch';
const client = algoliasearch(process.env.ALGOLIA_APP_ID!, process.env.ALGOLIA_ADMIN_KEY!);
const ROLES = {
searchService: {
acl: ['search'] as const,
description: 'Search service — production read-only',
indexes: ['products', 'articles'],
maxQueriesPerIPPerHour: 100000,
},
indexingPipeline: {
acl: ['addObject', 'editSettings', 'listIndexes'] as const,
description: 'Indexing pipeline — write-only, no delete',
indexes: ['products', 'articles'],
maxQueriesPerIPPerHour: 10000,
},
analyticsDashboard: {
acl: ['analytics', 'usage', 'listIndexes'] as const,
description: 'Analytics reader — no record access',
indexes: ['products', 'articles'],
maxQueriesPerIPPerHour: 5000,
},
dataAdmin: {
acl: ['search', 'browse', 'addObject', 'deleteObject', 'editSettings', 'listIndexes', 'deleteIndex'] as const,
description: 'Data admin — full access, staging only',
indexes: ['staging_*'],
maxQueriesPerIPPerHour: 50000,
},
};
async function createRoleKey(roleName: keyof typeof ROLES) {
const role = ROLES[roleName];
const { key } = await client.addApiKey({
apiKey: {
acl: [...role.acl],
description: role.description,
indexes: role.indexes,
maxQueriesPerIPPerHour: role.maxQueriesPerIPPerHour,
},
});
console.log(`Created ${roleName} key: ...${key.slice(-8)}`);
return key;
}
interface UserContext {
userId: string;
tenantId: string;
role: 'admin' | 'editor' | 'viewer';
}
function generateUserSearchKey(user: UserContext): string {
let filters = `tenant_id:${user.tenantId}`;
switch (user.role) {
case 'admin':
break;
case 'editor':
filters += ` AND (status:published OR author_id:${user.userId})`;
break;
case 'viewer':
filters += ' AND status:published';
break;
}
return client.generateSecuredApiKey({
parentApiKey: process.env.ALGOLIA_SEARCH_KEY!,
restrictions: {
filters,
validUntil: Math.floor(Date.now() / 1000) + 3600,
restrictIndices: ['products', 'articles'],
},
});
}
async function validateKeyPermissions(
apiKey: string,
requiredAcl: string[]
): Promise<boolean> {
try {
const keyInfo = await client.getApiKey({ key: apiKey });
const hasAll = requiredAcl.every(perm => keyInfo.acl.includes(perm));
if (!hasAll) {
const missing = requiredAcl.filter(p => !keyInfo.acl.includes(p));
console.warn(`Key missing permissions: ${missing.join(', ')}`);
}
return hasAll;
} catch (e) {
console.error('Failed to validate API key:', e);
return false;
}
}
function requireAlgoliaPermission(requiredAcl: string[]) {
return async (req: any, res: any, next: any) => {
const key = req.headers['x-algolia-api-key'];
if (!key || !(await validateKeyPermissions(key, requiredAcl))) {
return res.status(403).json({ error: 'Insufficient Algolia permissions' });
}
next();
};
}
async function auditApiKeys() {
const { keys } = await client.listApiKeys();
console.log(`Total API keys: ${keys.length}\n`);
for (const key of keys) {
const ageMs = Date.now() - new Date(key.createdAt * 1000).getTime();
const ageDays = Math.floor(ageMs / (1000 * 60 * 60 * 24));
console.log(`Key: ...${key.value.slice(-8)}`);
console.log(` Description: ${key.description || '(none)'}`);
console.log(` ACL: ${key.acl.join(', ')}`);
console.log(` Indices: ${key.indexes?.join(', ') || 'ALL'}`);
console.log(` Rate limit: ${key.maxQueriesPerIPPerHour || 'unlimited'}/hr`);
console.log(` Age: ${ageDays} days`);
if (ageDays > 90) {
console.log(` WARNING: Key is ${ageDays} days old — consider rotation`);
}
if (key.acl.includes('deleteIndex') && !key.description?.includes('admin')) {
console.log(` WARNING: Has deleteIndex permission — verify this is intentional`);
}
console.log('');
}
}
Algolia Dashboard Team Roles (configured in dashboard.algolia.com > Team):
| Dashboard Role | Can Do | Can't Do |
|----------------|-------------------------------------------|-----------------------|
| Owner | Everything + billing + team management | N/A |
| Admin | All index operations + API key management | Billing |
| Editor | Search, index data, edit settings | API key management |
| Viewer | Search, view analytics | Modify anything |
Configure at: dashboard.algolia.com > Settings > Team
Enterprise plans support SSO (SAML 2.0) for team authentication.
The application has documented role-to-ACL mappings, restricted keys for each actor, and an auditable rotation path. Failed authorization attempts can be traced to a missing or excessive grant instead of being worked around with an Admin key.