Skip to main content

Metafield write using offline token context

Detects metafield writes (metafieldsSet) in offline-token contexts. Online tokens provide user attribution and expiry.

DetailValue
Check IDMETAFIELD_OFFLINE_TOKEN
SeverityHigh
How it runsAgent review

app/routes/app.products.$id.tag.tsx

import type { ActionFunctionArgs } from "react-router";

import { unauthenticated } from "../shopify.server";

export const action = async ({
request,
params,
}: ActionFunctionArgs) => {
const formData = await request.formData();
const shop = String(formData.get("shop"));

// An offline token writes with the app's standing authority:
// the write carries no user attribution and outlives the user's
// session.
const { admin } = await unauthenticated.admin(shop);

return admin.graphql(
`#graphql
mutation Tag($metafields: [MetafieldsSetInput!]!) {
metafieldsSet(metafields: $metafields) {
userErrors { message }
}
}`,
{
variables: {
metafields: [
{
ownerId: `gid://shopify/Product/${params.id}`,
namespace: "app",
key: "tag",
value: "reviewed",
type: "single_line_text_field",
},
],
},
},
);
};

Use authenticate.admin(request) instead of unauthenticated.admin() for metafield writes.

Switching helpers isn't enough on its own: shopifyApp defaults useOnlineTokens to false and the app template leaves it off, so set useOnlineTokens: true as well. Without it, authenticate.admin still hands you the offline session and the write carries the app's standing authority rather than the user's.

For more detail, see Online access tokens.

app/routes/app.products.$id.tag.tsx

import type { ActionFunctionArgs } from "react-router";

import { authenticate } from "../shopify.server";

export const action = async ({
request,
params,
}: ActionFunctionArgs) => {
// Use the request-scoped Admin context, not a standing offline
// one. For the write to carry user attribution and expire with
// the session, shopifyApp also needs useOnlineTokens: true,
// which the template leaves off by default. Keep
// unauthenticated.admin for genuine background work such as
// webhook processing.
const { admin } = await authenticate.admin(request);

return admin.graphql(
`#graphql
mutation Tag($metafields: [MetafieldsSetInput!]!) {
metafieldsSet(metafields: $metafields) {
userErrors { message }
}
}`,
{
variables: {
metafields: [
{
ownerId: `gid://shopify/Product/${params.id}`,
namespace: "app",
key: "tag",
value: "reviewed",
type: "single_line_text_field",
},
],
},
},
);
};


Was this page helpful?