{"title":"Restrict a store to specific IP addresses","slug":"restrict-store-access-by-ip-address","url":"https://support.storeconnect.com/articles/restrict-store-access-by-ip-address","url_markdown":"https://support.storeconnect.com/articles/restrict-store-access-by-ip-address.md","subtitle":null,"summary":"Send visitors to a holding page based on their IP address, using the theme controller. Use this to keep a store, or parts of it, private to your team and your client before launch, when stealth mode lets in too many visitors and disabling the store lets in none.","type":"Developer_Documentation","video_url":"","keywords":"ip restriction, ip allowlist, ip whitelist, restrict store access, private store, pre-launch store, hide store before launch, block visitors, holding page, coming soon page, theme controller, controllers/theme, current_request.ip, too many redirects","last_modified":"2026-10-29T21:35:26+0000","body_markdown":"Use the theme controller to limit a store to a list of IP addresses, so only your team and your client can see the storefront before launch. Visitors from any other address are redirected to a holding page.\n\n## Choose how to limit access\n\nStoreConnect has three ways to keep a store out of view. They differ in who can open the store.\n\n| Option | Who can open the store | Search and AI engines | Partial access |\n|---|---|---|---|\n| [Stealth mode](stealth-mode) | Anyone with the URL | Asked not to index the store. Compliant crawlers follow the request, but it is not enforced | No. Applies to the whole store |\n| [Disable the store](store-disable-system) | No one. Every visitor sees the disabled content block | Nothing to index | No. There is no exception for an IP address or a user |\n| IP restriction (this article) | Only the IP addresses you list. Everyone else sees the holding page | Not affected. Turn on stealth mode as well | Yes, with a path condition |\n\nThe check in this article uses the visitor's IP address. You can test other request attributes the same way, such as the visitor's country with `current_request.geolocation_country_code`, by changing the condition in the controller.\n\n## Before you start\n\n- You need edit access to the **Theme** your store uses, so you can create a **Theme Template** on it. See [theme templates](theme-templates).\n- You have the public IP address of every network that needs access, such as your office and your client's office.\n\n## Create the holding page\n\nCreate the page first. Visitors who are not on the list are sent to it, so it must exist before the restriction goes on.\n\n1. Go to the **Pages** list and create a new **Page**.\n2. Set **Store** to the store you are restricting.\n3. Set **Path** to `access-restricted`.\n4. Set **Title** to the message visitors see, for example `This site is not open yet`.\n5. (Optional) Add `Your IP address is {{ current_request.ip }}` to **Content Body**. A team member who lands on the holding page can then send you the address to add to the list.\n6. Save the page.\n\nThe page is now live at `/access-restricted` on your store. If the store has a **Path**, such as `au`, the page is at `/au/access-restricted`.\n\n## Restrict the store to your IP addresses\n\n`controllers/theme` runs before every storefront page, so a redirect in the `before` phase stops the page rendering for anyone not on the list.\n\n:::warning\nThe restriction applies to every store that uses this theme. To restrict one store only, give it its own **Theme** first.\n:::\n\n1. Open the **Theme** record your store uses.\n2. On the **Theme Templates** related list, create a new **Theme Template**.\n3. Set **Key** to `controllers/theme`.\n4. Set **Content** to the following, replacing the two example addresses with your own:\n\n    \n```liquid\n\n    {% liquid\n    before\n      assign allowed_ips = \"203.0.113.10,198.51.100.25\" | split: \",\"\n      assign visitor_ip = current_request.ip | strip\n      assign local_path = current_request.local_path\n      assign holding_path = current_request.store_path | append: \"/access-restricted\"\n\n      assign exempt = false\n      if local_path == \"/access-restricted\" or local_path contains \"/inbound_webhooks/\" or local_path contains \"/payment/postback\" or local_path contains \"/pos/card_capture/\" or local_path contains \"/embedded_payment\"\n        assign exempt = true\n      endif\n\n      unless allowed_ips contains visitor_ip or exempt\n        redirect to: holding_path, status: 302\n      endunless\n    endbefore\n    %}\n    ```\n\n\n    Separate the addresses with a comma and no spaces. An address with a space next to it does not match. The `exempt` check lets the holding page itself load. Without it, the holding page redirects to itself and the browser shows a \"too many redirects\" error. The same check lets through the requests listed under [What the IP restriction covers](#what-the-ip-restriction-covers). `store_path` keeps the redirect on the right store when the store has a **Path**.\n\n5. Save the template.\n\nVisit the store from an address on the list and the storefront loads as normal. From any other address, including a phone on mobile data, the browser lands on `/access-restricted`.\n\n## What the IP restriction covers\n\nThe restriction applies to storefront pages, including component reloads on those pages. The StoreConnect API and the POS app do not run the theme controller, so API clients and registers are not affected.\n\nSome storefront routes are reached from outside your network even while you test. The `exempt` condition in the controller lets these through, so test orders and fulfillment keep working:\n\n- **Payment postbacks** — a payment provider's servers confirm a payment at `/checkout/payment/postback` or `/orders/\u003creference\u003e/payment/postback`.\n- **Shipping and fraud webhooks** — Bringg, EasyPost, Easyship, Forter, Ship24, ShipEngine, and Shippit send updates to `/async/inbound_webhooks/\u003cservice\u003e`.\n- **POS subscription card capture** — a customer enters card details for a POS order's subscriptions at `/pos/card_capture/\u003creference\u003e`.\n- **Embedded payment form** — external apps load the payment form for API-created carts at `/embedded_payment`.\n\nRemove a line from the condition if your store does not use that route. Payment returns through the visitor's own browser, such as after a 3D Secure check, need no exemption, because the visitor is already on an allowed address.\n\nThe restriction does not remove a store from search results. If the store was public before, search engines keep the pages they already indexed. Turn on [stealth mode](stealth-mode) as well so they drop them.\n\nThe restriction matches exact addresses. If a network that needs access does not have a static IP address, the address can change and lock that network out. Ask the people on that network to check the holding page and send you their new address.\n\n## Remove the restriction at launch\n\n1. Open the **Theme** record your store uses.\n2. On the **Theme Templates** related list, delete the `controllers/theme` template.\n\nIf the template also holds other theme controller logic, such as [global variables](global-variables), delete only the `unless` … `endunless` lines that hold the IP check instead. The storefront now loads for every visitor."}