If you are building a Shopify app that handles user-generated audio—whether it's pitch notes, product demos, or customer reviews—stop trying to store base64 strings in metafields. You will hit the 100KB metafield value limit immediately, bloat your database, and create a nightmare for future migrations.
The correct architecture for any media-heavy Shopify app is simple: store the file in an external object store (AWS S3, GCS, or R2) and store the resulting URL in a Shopify metafield. This post explains how to implement this pattern using GraphQL Admin API, S3, and a secure upload flow.
The Problem: Why Base64 Fails
Developers often reach for base64 encoding because it feels "self-contained." You encode the audio file, pass it to the metafieldsSet mutation, and you're done.
This fails for three reasons:
- Size Limits: Shopify metafields have a strict 100,000 character limit. A 10-second MP3 file at 128kbps is roughly 160KB in binary. Base64 encoding increases size by ~33%, pushing it to ~213KB. You cannot fit a single short audio clip into a single metafield.
- Performance: Storing large blobs in your database (or Shopify's metafield store) slows down queries. Even if you split the data across multiple metafields, you’re making dozens of network requests to reconstruct one file.
- Scalability: You cannot stream, process, or transcribe base64 strings efficiently. You have to decode them in memory first, which is CPU-intensive and memory-hungry at scale.
The Solution: S3 + Metafield URL Pattern
The standard pattern for media in Shopify apps is:
- Generate a pre-signed URL for your S3 bucket.
- Upload the file directly from the client (or server) to S3.
- Store the public (or signed) S3 URL in a Shopify metafield.
This keeps your database lightweight, leverages S3’s global CDN for fast delivery, and allows you to use S3 lifecycle policies for cost management.
Step 1: Define the Metafield
First, define a metafield definition for your app. Use the metafieldDefinitionsSet mutation to create a definition that stores a single URL.
mutation {
metafieldDefinitionsSet(input: {
type: "Product",
namespace: "pitch_tracker",
key: "audio_url",
type: "single_line_text_field",
description: "S3 URL for the pitch audio file"
}) {
metafieldDefinition {
id
namespace
key
type
}
userErrors {
field
message
}
}
}
Note: Use single_line_text_field for URLs. Do not use file type unless you are uploading via the Shopify File API, which has its own size and format restrictions.
Step 2: Generate a Pre-Signed S3 URL
In your app’s backend (Node.js/Remix), create an endpoint that generates a pre-signed PUT URL. This prevents clients from uploading arbitrary files to your bucket.
typescript
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
const s3 = new S3Client({
region: "us-east-1",
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
});
export async function generateUploadUrl(fileName: string): Promise<string> {
const key = `uploads/${Date.now()}-${fileName}`;
const command = new PutObjectCommand({
Bucket: process.env.S3_BUCKET_NAME!,
Key: key,
ContentType: "audio/mpeg", // Restrict to MP3
});
const presignedUrl = await getSignedUrl(s3, command, {
---
*Originally published at [arslanraza.com](https://arslanraza.com/blog/shopify-app-audio-api-store-pitch-notes-correctly)*
Top comments (0)