# Shopify Admin API access (metaobjects, metafields, translations)

How an LLM (or a human) working in this repo reads and writes the store's
custom data — metaobject definitions and entries, metafields on products and
variants, and Translate & Adapt translations — on the **live store**
`bigscreenvr.myshopify.com`. There is no staging store: every write here is
production. Read the safety protocol before writing anything.

Theme *code* is a separate channel and unchanged: edit liquid in this repo,
push to `main`, the GitHub-connected theme syncs. The golden safety rule in
CLAUDE.md (never publish, never `--allow-live`) still stands. The API token
below deliberately has **no theme scopes**.

## One-time setup

```bash
bash scripts/setup-shopify-app.sh
```

Apps are created in the **Dev Dashboard** (dev.shopify.com — since Jan 2026
the store admin can no longer create custom apps). The wizard walks through
creating the `theme-agent` app there, granting exactly these scopes,
installing it on the store, and capturing its credentials:

    read_products, write_products,
    read_metaobjects, write_metaobjects,
    read_metaobject_definitions, write_metaobject_definitions,
    read_translations, write_translations

Nothing else — no orders, no customers, no themes.

Auth model: Dev Dashboard apps have **no static admin token**. The runner
exchanges the app's Client ID + Client Secret for an access token via the
client credentials grant (`POST /admin/oauth/access_token`); tokens last ~24h
and are cached in `.shopify/admin-token.json` (gitignored), re-minted
automatically on expiry or 401. This only works because the app and the store
live in the same Shopify organization.

Credentials land in the gitignored `.env` at the repo root (`SHOPIFY_STORE`,
`SHOPIFY_API_VERSION`, `SHOPIFY_CLIENT_ID`, `SHOPIFY_CLIENT_SECRET`; see
`.env.example`). Source of truth is `Server_Keys`; a fresh machine can copy
the values from there into `.env` instead of re-running the wizard.

## Running queries

```bash
# read (free)
node scripts/shopify-api.mjs --query '{ shop { name } }'
node scripts/shopify-api.mjs my-query.graphql --vars '{"handle":"bigscreen-beyond-2"}'

# write (gated)
node scripts/shopify-api.mjs my-mutation.graphql --vars-file vars.json --write
```

The runner is dependency-free (Node 18+). It refuses any document containing a
mutation unless `--write` is passed, prints the JSON response, and exits
non-zero on GraphQL errors (2) or `userErrors` (3) — an exit 3 means the write
did NOT fully apply; read the errors.

## Write protocol (binding for LLMs)

1. Reads need no permission — query freely.
2. Before any mutation: show the human the exact mutation + variables and what
   it will change, and get their confirmation **in that moment**.
3. Only then re-run with `--write`. Never pass `--write` on the first attempt,
   and never batch unreviewed mutations.
4. Prefer surgical mutations (one entry, one definition) over bulk operations.
5. After a definition change (metaobject or metafield definitions), update the
   wiki in the same sitting — see "Keeping the wiki honest" below.

## Schema lookups — don't guess field names

The repo registers Shopify's official Dev MCP (`.mcp.json`, `@shopify/dev-mcp`
— docs/schema only, no store access, no auth). Use it to verify Admin API
types, fields, and mutation shapes for the pinned `SHOPIFY_API_VERSION` before
writing queries, instead of guessing from memory.

## Canned examples

Product + variant metafields (the configurator's data):

```graphql
query ($q: String!) {
  products(first: 1, query: $q) {
    nodes {
      id
      title
      metafields(first: 50) { nodes { namespace key type value } }
      variants(first: 50) {
        nodes {
          id
          title
          metafields(first: 50) { nodes { namespace key type value } }
        }
      }
    }
  }
}
```

with `--vars '{"q":"handle:bigscreen-beyond-2"}'`.

Metaobject entry by handle (the text-collection singletons use handle `live`):

```graphql
query {
  metaobjectByHandle(handle: { type: "cart", handle: "live" }) {
    id
    fields { key value }
  }
}
```

Set metafield values (works for product and variant owners alike):

```graphql
mutation ($metafields: [MetafieldsSetInput!]!) {
  metafieldsSet(metafields: $metafields) {
    metafields { id key }
    userErrors { field message code }
  }
}
```

Create or update a metaobject entry:

```graphql
mutation ($handle: MetaobjectHandleInput!, $metaobject: MetaobjectUpsertInput!) {
  metaobjectUpsert(handle: $handle, metaobject: $metaobject) {
    metaobject { id handle }
    userErrors { field message code }
  }
}
```

Translations — read what Translate & Adapt sees (the `digest` of each
translatable field is required to register a translation for it):

```graphql
query ($id: ID!) {
  translatableResource(resourceId: $id) {
    resourceId
    translatableContent { key value digest locale }
    translations(locale: "ja") { key value }
  }
}
```

Translations — write:

```graphql
mutation ($id: ID!, $translations: [TranslationInput!]!) {
  translationsRegister(resourceId: $id, translations: $translations) {
    translations { key locale value }
    userErrors { field message }
  }
}
```

(each `TranslationInput` needs `key`, `locale`, `value`, and
`translatableContentDigest` from the read above).

Definition changes (`metaobjectDefinitionCreate` / `...Update`,
`metafieldDefinitionCreate`) follow the same pattern — verify the input types
via the Dev MCP first, and remember the wiki rule below. The pending
definition work is specced in `METAOBJECT-FIELDS.md`.

## The data model

`CONTEXT.md` is the glossary (what a metaobject vs. a metafield vs. an addon
group *is*). The full field-by-field inventory lives on the website repo's
wiki page **[Shopify ‐ Metafields & Metaobjects](https://github.com/BigscreenVR/website/wiki/Shopify-%E2%80%90-Metafields-&-Metaobjects)**
— definitive list, kept current by hand (and by you).

## Keeping the wiki honest

Rule (same spirit as the nav-twin rule): **any create/update/delete of a
metaobject definition or metafield definition via this API must update the
wiki page in the same sitting.** Entry-value edits (changing a string in
`cart.live`, setting a metafield on a variant) do NOT need wiki edits — the
wiki documents structure, not content.

The wiki is itself a git repo. On Windows a plain clone fails — one legacy
page name contains `:` (illegal on NTFS) — so clone without checkout and
exclude it:

```bash
git clone --no-checkout https://github.com/BigscreenVR/website.wiki.git
cd website.wiki
git sparse-checkout set --no-cone '/*' '!/Builder.io*'
git config core.protectNTFS false   # checkout validates even sparse-skipped paths
git checkout master
```

The page file is `Shopify-‐-Metafields-&-Metaobjects.md` — note the hyphen is
U+2010 (`‐`), not ASCII `-`. Edit, commit, push to `main`; the wiki updates
instantly. Match the page's existing structure (owner type → namespace →
field, with admin deep links).
