> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hiveku.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Connecting Shopify

> Connect a Shopify store via OAuth — at the account level (the usual starting point) or per project — and understand inherit/override/disabled modes plus the credentials Hiveku stores.

Connecting Shopify is a one-time per-store action. Most accounts connect a single store at the **account level** and every project inherits it. Agencies that run multiple brands in one workspace can additionally connect (or bind) a store **per project**. Either way, deploys automatically receive the right credentials for the effective connection.

## Two ways to connect

<CardGroup cols={2}>
  <Card title="Account level (start here)" icon="building">
    Connect a store once under **Settings → Commerce → Shopify**. Every project on the account inherits it. This is the right path for one account = one brand = one store.
  </Card>

  <Card title="Project level (multi-shop)" icon="folder-tree">
    Inherit, override, or disable Shopify per project from the project's **Shopify** panel. Override binds a project to a specific store — ideal when one workspace manages several brands.
  </Card>
</CardGroup>

<Info>
  Both paths use the same OAuth handshake and the same connection store. The only difference is what the connection is *bound to* — the account default, or a single project. Project-level connect requires that a Shopify OAuth app is already registered on the account (see prerequisites).
</Info>

## Prerequisites

<Steps>
  <Step title="A Shopify store">
    A live or development Shopify store. Hiveku works with any plan (Basic and up). For native subscriptions you'll also need Shopify Payments turned on — see [Subscriptions](/integrations/shopify/subscriptions#hard-preconditions).
  </Step>

  <Step title="Owner or admin role on the Hiveku account">
    Connecting a store writes account-scoped credentials. Member-role users can't initiate the OAuth flow.
  </Step>

  <Step title="A Shopify OAuth app registered in Hiveku settings">
    One-time setup per Hiveku account: register a Shopify app in your Shopify Partner dashboard, then add its `client_id` + `client_secret` under **Settings → OAuth apps** in Hiveku. Subsequent stores reuse the same app. **Both the account and project flows require this** — the project panel refuses to start OAuth if no `provider=shopify` app exists yet.
  </Step>
</Steps>

## Connect at the account level

This is the usual starting point. The first store you connect on an account becomes the **account default** — every project under that account uses it with no per-project setup.

<Steps>
  <Step title="Open the Shopify settings page">
    Navigate to `/dashboard/commerce/settings/shopify`.
  </Step>

  <Step title="Click &#x22;Connect a store&#x22;">
    A dialog asks for two inputs:

    * **Shop domain** — the permanent `*.myshopify.com` domain (not your custom domain). Example: `acme.myshopify.com`.
    * **OAuth app** — picked from the apps you registered in Hiveku settings. If you only have one, it's preselected.
  </Step>

  <Step title="Authorize on Shopify">
    Hiveku opens a popup pointing at `https://{shop}.myshopify.com/admin/oauth/authorize`. The merchant approves the requested scopes (`read_products`, `write_products`, `read_orders`, `read_customers`, and more — the full list is in the developer section below).
  </Step>

  <Step title="Hiveku finishes the handshake">
    The popup closes automatically. Behind the scenes:

    * Hiveku exchanges the OAuth code for an offline Admin API access token.
    * A Storefront API token is minted via the Admin GraphQL API.
    * Both tokens are encrypted at rest (`SHOPIFY_TOKEN_ENCRYPTION_KEY`, AES-256-GCM).
    * Webhook subscriptions are registered for the topics Hiveku needs (orders, products, subscription events, GDPR compliance).
    * The connection is saved with `purpose = 'account_default'` and appears as **Active** in the settings UI.
  </Step>
</Steps>

<Note>
  The shop domain you enter must match what Shopify's OAuth callback returns. The callback verifies the `?shop=` query param against the domain the flow was started for, to defend against shop-substitution attacks.
</Note>

## The account-default vs project-override model

Hiveku resolves a project's Shopify connection with a simple precedence, optimized for agency users:

**project override → account default → none.**

```
Account: Locus Digital (agency)
└── Account default: locus-digital.myshopify.com (the agency's own shop)
    ├── Project: Roastline Coffee site → override → roastline.myshopify.com
    ├── Project: Beachwear Co site     → override → beachwear-co.myshopify.com
    ├── Project: Locus internal blog   → inherit (uses account default)
    └── Project: Marketing microsite   → disabled (no Shopify at all)
```

When one Hiveku account maps to one brand and one store, you never touch the project settings — everything inherits the account default. When a single workspace manages multiple brands, you set the connection per project.

## Connect (or bind) at the project level

Each project has a **Shopify** panel with three modes. You'll find it in two places — they're the same panel:

| Surface                          | Path                               |
| -------------------------------- | ---------------------------------- |
| Project Settings → Shopify panel | `/dashboard/[projectId]/settings`  |
| Ecommerce tab (v3 editor nav)    | `/dashboard/[projectId]/ecommerce` |

### The three modes

| Mode                               | What it does                                                                  | Effective connection                           |
| ---------------------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------- |
| **Inherit from account** (default) | Use the account-default store, if any.                                        | Account default, or none if no default exists. |
| **Override with a specific store** | Bind this project to one of the account's connected stores.                   | The chosen `project_override` connection.      |
| **Disabled**                       | Opt this project out of Shopify entirely — even if an account default exists. | None.                                          |

### Pick a mode (uses an existing connection)

<Steps>
  <Step title="Open the project's Shopify panel">
    From `/dashboard/[projectId]/settings` (or the **Ecommerce** tab). The panel shows the currently **effective** connection and its source — *account default* or *project override*.
  </Step>

  <Step title="Choose Inherit, Override, or Disabled">
    Selecting a mode saves immediately. **Override** additionally shows a **Store** dropdown listing every active connection on the account; pick one and the project's deploys pull credentials from it going forward.
  </Step>
</Steps>

<Note>
  If **no** store is connected to the account yet, the panel says so and links you to **Set up Shopify for this account** (`/dashboard/commerce/settings/shopify`). Connect at the account level first, then inherit or override here.
</Note>

### Connect a project-specific store (new OAuth)

Sometimes the store you want isn't connected to the account yet and you want it bound *directly* to this project. The panel's **Connect a project-specific store** button runs a fresh OAuth handshake and, on success, binds the result to this project as an override — no separate mode-selection step needed.

<Steps>
  <Step title="Click “Connect a project-specific store”">
    You'll be asked for the `*.myshopify.com` shop domain (validated against the `acme.myshopify.com` shape). The panel uses the account's registered Shopify OAuth app automatically.
  </Step>

  <Step title="Authorize on Shopify">
    Same popup handshake as the account flow. The merchant approves the requested scopes.
  </Step>

  <Step title="Hiveku binds it to this project">
    On success the connection is saved with `purpose = 'project_override'` and the project is switched to **Override** mode, pointed at the new connection. The panel refreshes to show it as the effective store.
  </Step>
</Steps>

<Warning>
  If the account has **no** Shopify OAuth app registered, this button stops with: *"No Shopify OAuth app registered. Add one under Settings → OAuth apps first."* Register the app once (see prerequisites), then retry.
</Warning>

<Tip>
  Prefer the account-default path unless you genuinely run multiple brands from one workspace. It's fewer moving parts, and every project just works. Reach for project-level override only when different projects legitimately need different stores.
</Tip>

## Under the hood (for developers)

<AccordionGroup>
  <Accordion title="OAuth routes and intent types" icon="route">
    Both flows share one start → callback pair, distinguished by `intent_type`:

    | Flow      | Start                           | `intent_type`             | Result                                                                                                                                |
    | --------- | ------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
    | Account   | `POST /api/oauth/shopify/start` | `shopify_account_connect` | Connection row with `purpose = 'account_default'` (retires any prior active default).                                                 |
    | Project   | `POST /api/oauth/shopify/start` | `shopify_project_connect` | Connection row with `purpose = 'project_override'`, plus an upsert into `project_shopify_settings` with `override_mode = 'override'`. |
    | Reconnect | `POST /api/oauth/shopify/start` | `shopify_reconnect`       | Updates the existing row in place; rotates its `revalidation_token`.                                                                  |

    The project flow's start body carries `intent_data.project_id`. The shared callback is `GET /api/oauth/shopify/callback`; it verifies in three layers — signed-state HMAC (CSRF/replay), Shopify callback HMAC (keyed by the app's `client_secret`), and a shop-domain match — then exchanges the code, mints tokens, persists, and registers webhooks.
  </Accordion>

  <Accordion title="Data model + resolver precedence" icon="database">
    Two tables back the whole thing:

    * **`shopify_connections`** — one row per connected store. `purpose` is `'account_default'` or `'project_override'`. `shop_domain` is globally unique, so connecting a shop already bound to a *different* account is rejected.
    * **`project_shopify_settings`** — one row per project. `override_mode` is `'inherit' | 'override' | 'disabled'`; when `'override'`, `shopify_connection_id` points at the bound connection.

    `resolveShopifyCredentials({ accountId, projectId })` in `src/lib/shopify/credentials.ts` applies the precedence: `override_mode = 'override'` (and a live connection) wins; `override_mode = 'disabled'` returns none even if an account default exists; otherwise it falls back to the account default (`purpose = 'account_default'`, not disconnected). Results are cached in-process for 60 seconds and busted via `bustShopifyCredentialsCache` on connect / disconnect / override changes.
  </Accordion>

  <Accordion title="Panel API surface" icon="plug">
    The project Shopify panel talks to:

    * `GET /api/builder/shopify/{projectId}/status` — the effective connection + the project's stored `override_mode` / `shopify_connection_id`.
    * `PUT /api/builder/shopify/{projectId}/settings` — save a mode (body: `override_mode`, optional `shopify_connection_id`).
    * `GET /api/builder/shopify/connections` — the account's connections. Returns only non-secret fields (`id`, `shop_domain`, `shop_name`, `purpose`, `disconnected_at`) — never token columns.
    * `GET /api/oauth-apps?provider=shopify` — the registered Shopify OAuth app(s) used to start OAuth.
  </Accordion>

  <Accordion title="Scopes, API version, and tokens" icon="key">
    * **Requested scopes** live in `DEFAULT_SHOPIFY_SCOPES` (`src/lib/shopify/auth.ts`) — the single source of truth shared by the OAuth start route and the scope-drift cron: `read_products`, `write_products`, `read_orders`, `read_customers`, `write_customers`, `read_content`, `write_content`, `read_themes`, `write_themes`. Your Shopify app's configured scopes must match.
    * **API version** is pinned by `SHOPIFY_API_VERSION = '2025-04'`.
    * The Admin token is **offline** (long-lived, valid until the app is uninstalled) — no `grant_options[]=per-user`, because Hiveku makes server-to-server Admin API calls.
    * **Env vars (builder web service):** `SHOPIFY_TOKEN_ENCRYPTION_KEY` (32-byte base64, AES-256-GCM for stored tokens) and `NEXT_PUBLIC_APP_URL` (drives the callback and the `/api/webhooks/shopify` endpoint). The OAuth app's `client_id`/`client_secret` live per-account in `oauth_apps`, not in env.
  </Accordion>
</AccordionGroup>

## What gets stored

Hiveku stores the minimum needed to operate the integration, on the `shopify_connections` row:

| Field                                                | Storage                                                   |
| ---------------------------------------------------- | --------------------------------------------------------- |
| Admin API access token (`admin_access_token_enc`)    | Encrypted at rest (`SHOPIFY_TOKEN_ENCRYPTION_KEY`)        |
| Storefront API token (`storefront_access_token_enc`) | Encrypted at rest                                         |
| Webhook secret (`webhook_secret_enc`)                | Encrypted at rest                                         |
| Shop domain, shop id, shop name                      | Plain (routing webhooks + UI display)                     |
| Granted OAuth scopes (`scope`)                       | Plain (compared against required scopes to detect drift)  |
| `revalidation_token`                                 | Plain (consumed by the deployed site's `/api/revalidate`) |

Tokens never appear in the API responses to your browser or in logs — the connection-list endpoint returns only non-secret fields.

## Multiple stores on one account

You can connect any number of Shopify stores. Only one can be the account default at a time (`purpose = 'account_default'`); the rest are `purpose = 'project_override'` and selectable per project via **Override** mode.

To swap defaults: disconnect the current default, then reconnect the store you want as the new default. Connecting a fresh account default automatically retires the previous active default for that account.

## Disconnecting

<Steps>
  <Step title="Disconnect from Hiveku">
    Click **Disconnect** on the connection in `/dashboard/commerce/settings/shopify`. Hiveku marks the row `disconnected_at = now()`, stops registering webhook events, and clears the credentials from the next deploy. The Admin token is **not** force-revoked at Shopify — your Shopify admin can also un-install the app to be thorough.
  </Step>

  <Step title="(Optional) Uninstall in Shopify admin">
    Visit Shopify admin → Apps → uninstall Hiveku. Shopify revokes the token and fires `app/uninstalled` to Hiveku, which sets `disconnected_at` if it wasn't already.
  </Step>
</Steps>

<Warning>
  After disconnect, your deployed sites lose access to Shopify credentials on the **next deploy**. The currently-running site keeps working until then. Plan accordingly if you need a clean cutover.
</Warning>

## Reconnecting

Re-running OAuth for a previously connected store (a reconnect — `intent_type = 'shopify_reconnect'`, keyed by the existing `connection_id`) updates that same connection row in place — encrypted tokens, scopes, shop metadata, and timestamps all refresh, and the `revalidation_token` is rotated. The connection's `purpose` and any project-level bindings survive.

## What's next

<CardGroup cols={2}>
  <Card title="Build the storefront" icon="hammer" href="/integrations/shopify/storefront">
    What the AI agent scaffolds after a connection is active.
  </Card>

  <Card title="Adding to an existing Next.js project" icon="arrow-up-from-square" href="/integrations/shopify/retrofitting">
    Pre-flight compat check + dry-run for retrofits.
  </Card>
</CardGroup>
