The product is a fashion storefront: shoppers open collection pages and Algolia InstantSearch drives the filter chips and product grid in the browser. Nightly jobs on the server push Shopify catalog rows into Algolia and apply merchandising rules. Both Algolia keys lived in the same env file, and InstantSearch wanted a searchClient. If the browser received the admin key, every visitor with DevTools received the catalog write button.
Nothing breaks at first, because an admin key can search. Then someone copies the key from the network tab and clears an index, or dumps every record. Shoppers notice empty hits. Support starts saying search is gone, and you are explaining ACL to people who only wanted to filter by size.
InstantSearch will happily take the write key
InstantSearch wants searchClient. algoliasearch(appId, adminKey) returns a client. It answers queries. It also answers deleteIndex. One client object, two very different jobs, and the PLP only needed the first one.
I almost shipped it because the merchandising helper already imported the admin client, and wiring InstantSearch with "the Algolia client" looked like less code for a tired Friday. The Next.js client bundle does not care that I named the variable ALGOLIA_ADMIN_API_KEY in a comment. If it is NEXT_PUBLIC_, it ships. Prefix is the security boundary. A comment does not hide a public env var.
The NEXT_PUBLIC prefix is the ship boundary
The public key is a search-only key from the Algolia dashboard. ACL: search. Not addObject, not deleteIndex, not settings. InstantSearch on the storefront uses that. Server routes that push Shopify or Sanity records use the admin key and never import that module into a client component.
// client: InstantSearch
const searchClient = algoliasearch(process.env.NEXT_PUBLIC_ALGOLIA_APP_ID, process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_KEY);
// server: merchandising / index writes
const adminClient = algoliasearch(process.env.ALGOLIA_APP_ID, process.env.ALGOLIA_ADMIN_API_KEY);I had a PLP that already SSR'd hits for the first paint. The merch job ran on the server. InstantSearch still needed a browser client for filter taps. Importing the same helper on both sides put the admin key one 'use client' import away from the grid.
Hits can travel, the admin client cannot
When a server helper needs to run during SSR, I pass hits as props. A serialized client in a client component is still a key in the browser. Hits are data, keys are credentials.
The storefront module imports NEXT_PUBLIC_ALGOLIA_APP_ID and NEXT_PUBLIC_ALGOLIA_SEARCH_KEY and nothing else. The merch job imports ALGOLIA_ADMIN_API_KEY and never appears in a file with 'use client'. When I need both in one request, the write stays behind the server helper and the PLP gets a list of hits.
Algolia will let a search UI use an admin key. That is publishing the index password next to the product grid.
A search-only key still lets anyone who can load the PLP run searches. That is the product. Write ACL in the same bundle as the grid is the part I will not ship. I split the keys even when local still works with admin, because production will ship whatever I left in NEXT_PUBLIC_. The split is boring until someone pastes the wrong key into a client env and you are rebuilding an index on a Sunday.
Empty hits after a leaked admin key are a storefront incident. A search-only key in InstantSearch is the version of the same PLP I can live with.
Happy coding!
Sander