SAFD Collection Submission Platform
A complete reference for setting up the Shopify integration, understanding the data flow, deploying the platform, and resolving common issues.
The platform is a full-stack web application built on React 19, tRPC, and Express. When a student submits a collection, the following pipeline executes:
stagedUploadsCreate mutation to obtain secure, temporary upload URLs.fileCreate with the resourceUrl values returned by the staged targets.nodes(ids:) until all files reach READY status, collecting their MediaImage GIDs.metaobjectCreate is called with the designer name, description, and a JSON array of image GIDs for the images field.This two-stage approach (S3 → Shopify) ensures that large or multiple images are reliably transferred even under slow network conditions, and that the browser never communicates directly with the Shopify Admin API (keeping the access token server-side only).
The integration requires a Shopify Custom App with Admin API access. Follow these steps to create one:
| Scope | Purpose |
|---|---|
| write_metaobjects | Create new entries in the Collections metaobject |
| read_metaobjects | Verify metaobject definition exists |
| write_files | Upload images via stagedUploadsCreate and fileCreate |
| read_files | Poll file processing status after upload |
After saving, go to API credentials and click Install app. Copy the Admin API access token — it is only shown once. Store it as the SHOPIFY_ACCESS_TOKEN environment variable.
To find your metaobject type handle, go to Content → Metaobjects in Shopify Admin, click on your Collections definition, and look at the URL — the last segment is the type handle (e.g., collections or safd_collections). Set this as SHOPIFY_METAOBJECT_TYPE.
Required environment variables
SHOPIFY_STORE_DOMAIN=your-store.myshopify.com
SHOPIFY_ACCESS_TOKEN=shpat_xxxxxxxxxxxxxxxxxxxx
SHOPIFY_METAOBJECT_TYPE=collectionsThe form fields map directly to the Shopify metaobject field keys. The table below shows the exact mapping used in the metaobjectCreate mutation:
| Form Field | Metaobject Key | Field Type | Value Format |
|---|---|---|---|
| Designer Name | designer | single_line_text_field | Plain string |
| Description | description | multi_line_text_field | Plain string (newlines preserved) |
| Images | images | list.file_reference | JSON array of MediaImage GID strings |
The images field expects a JSON-serialised array of Shopify Global IDs (GIDs) in the format gid://shopify/MediaImage/<id>. These GIDs are obtained after uploading images through the staged upload pipeline and calling fileCreate.
If your metaobject uses different field keys (e.g., designer_name instead of designer), update the keys in server/shopify.ts → createCollectionMetaobject().
mutation metaobjectCreate($metaobject: MetaobjectCreateInput!) {
metaobjectCreate(metaobject: $metaobject) {
metaobject { id handle }
userErrors { field message code }
}
}
# Variables:
{
"metaobject": {
"type": "collections",
"fields": [
{ "key": "designer", "value": "Jane Doe" },
{ "key": "description", "value": "A collection inspired by…" },
{ "key": "images", "value": "["gid://shopify/MediaImage/123"]" }
]
}
}The complete server-side flow is implemented across two tRPC procedures in server/routers/collection.ts and the Shopify helper in server/shopify.ts:
Procedure 1 — collection.uploadImagesToS3
Accepts base64-encoded file payloads, decodes them to buffers, and stores each one in S3 with a unique key. Returns the array of S3 keys for use in the next step.
Procedure 2 — collection.submitToShopify
stagedUploadsCreate to get temporary Shopify upload targets.fileCreate with the resourceUrl from each staged target.nodes(ids:) every 1.5 s until all files are READY (max 20 attempts = 30 s).metaobjectCreate with the collected MediaImage GIDs.// Staged upload input (one entry per image)
{
filename: "collection-01.jpg",
mimeType: "image/jpeg",
resource: "FILE",
httpMethod: "POST",
fileSize: "2048000" // bytes as string
}
// fileCreate input
{
originalSource: "https://shopify-staged-uploads.s3.amazonaws.com/…",
contentType: "IMAGE"
}
// metaobjectCreate field values
{ key: "images", value: "["gid://shopify/MediaImage/987654321"]" }The platform is deployed via the Manus publishing workflow. No external hosting configuration is required.
SHOPIFY_STORE_DOMAIN, SHOPIFY_ACCESS_TOKEN, SHOPIFY_METAOBJECT_TYPE).*.manus.space domain.To embed the form in your Shopify store, you can link to the published URL from a custom page, a navigation menu item, or a Shopify theme button. The form operates as a standalone web application and does not require Shopify theme integration.
Embedding in Shopify (optional)
In your Shopify theme, add a button or link pointing to the published Manus URL. Students click the link, complete the form, and the submission is created directly in your metaobject store — no Shopify theme code changes are required.
Error: Value must be a file reference string
This occurs when the images field receives a raw URL or an incorrect GID format. Ensure you are using GIDs in the form gid://shopify/MediaImage/<numeric-id> and that the value is a JSON-serialised array (e.g., ["gid://shopify/MediaImage/123"]).
Error: metaobjectCreate errors: Field 'images' is invalid
The metaobject field key does not match. Open Shopify Admin → Content → Metaobjects → your Collections definition and verify the exact field key for the image field. Update the key in server/shopify.ts → createCollectionMetaobject() accordingly.
Error: Shopify API HTTP 401
The access token is invalid or has expired. Re-install the custom app in Shopify Admin to generate a new token, then update the SHOPIFY_ACCESS_TOKEN secret in the Manus Secrets panel.
Error: Timed out waiting for Shopify to process uploaded files
Shopify's file processing pipeline occasionally takes longer than expected for large images. The platform polls for up to 30 seconds (20 × 1.5 s). If this error persists, try reducing image file sizes to under 5 MB each, or increase maxAttempts in server/shopify.ts → pollFilesUntilReady().
Error: write_files access scope required
The custom app is missing the write_files scope. Go to Shopify Admin → Settings → Apps → your app → Configuration → Admin API integration and enable write_files and read_files, then reinstall the app.
Images upload but the metaobject has no images
This usually means the metaobject's images field type is set to file_reference (single) rather than list.file_reference (multiple). In Shopify Admin, edit the metaobject definition and change the field type to support multiple files. No code changes are required.