How AI is powering better personalization in e-commerce [with Vercel]. Sign up now

How to safely upload user-submitted images to Sanity from a static site?

1 replyLast updated: Nov 29, 2025

On my content site, I’d like to have a section where users can submit a short post and upload an image to go with it. In the admin, I can approve certain posts and they appear on the main page. I can see there’s a way to upload assets though I’m wondering how to do that safely from a static site. Have you guys done this before? I’m thinking I’ll need a lambda to receive the file and do the actual upload so as not to expose client credentials in the browser. Can you guys point me to any examples?
AI Update

You're absolutely right to be thinking about security! Exposing write tokens in client-side code is a big no-no, and the pattern you're describing (using a serverless function as a proxy) is exactly the recommended approach for user-generated content with Sanity.

The Standard Pattern

Here's the typical flow:

Security Best Practices

According to Sanity's documentation on API tokens, write tokens should never be exposed in frontend code. Instead:

Implementation Example

Here's a basic pattern for a serverless function (this example uses Next.js API routes, but the concept applies to Netlify Functions, Vercel Functions, AWS Lambda, etc.):

// pages/api/submit-post.js (or equivalent for your platform)
import { createClient } from '@sanity/client'

const client = createClient({
  projectId: process.env.SANITY_PROJECT_ID,
  dataset: process.env.SANITY_DATASET,
  token: process.env.SANITY_WRITE_TOKEN, // Robot token with Editor permissions
  apiVersion: '2024-01-01',
  useCdn: false
})

export default async function handler(req, res) {
  if (req.method !== 'POST') return res.status(405).end()
  
  const { text, imageFile } = req.body // You'll need to handle multipart/form-data
  
  // 1. Validate input
  if (!text || !imageFile) {
    return res.status(400).json({ error: 'Missing required fields' })
  }
  
  try {
    // 2. Upload asset to Sanity
    const asset = await client.assets.upload('image', imageFile, {
      filename: imageFile.name
    })
    
    // 3. Create document with approval field
    const doc = await client.create({
      _type: 'userPost',
      text: text,
      image: {
        _type: 'image',
        asset: {
          _type: 'reference',
          _ref: asset._id
        }
      },
      approved: false, // You'll toggle this in Studio
      submittedAt: new Date().toISOString()
    })
    
    return res.status(200).json({ success: true, id: doc._id })
  } catch (error) {
    return res.status(500).json({ error: 'Upload failed' })
  }
}

Schema Example

// schemas/userPost.js
export default {
  name: 'userPost',
  type: 'document',
  title: 'User Submissions',
  fields: [
    {
      name: 'text',
      type: 'text',
      validation: Rule => Rule.required().max(500)
    },
    {
      name: 'image',
      type: 'image'
    },
    {
      name: 'approved',
      type: 'boolean',
      initialValue: false
    },
    {
      name: 'submittedAt',
      type: 'datetime'
    }
  ]
}

Platform-Specific Notes

Alternative: Presigned URLs

While the direct upload approach above works well for most cases, if you're dealing with very large files, you might consider a presigned URL pattern (similar to S3 presigned URLs). However, for typical user-generated content (images under a few MB), the direct approach is simpler and sufficient.

Asset Upload Details

The key API you'll use is client.assets.upload(), which handles the actual file upload to Sanity's CDN. The asset gets stored, you get back an asset document with an _id, and you reference that in your content document.

This pattern is well-established in the Sanity community - you're definitely on the right track! The serverless function acts as your secure gateway, keeping credentials safe while enabling user contributions.

Show original thread
1 reply

Was this answer helpful?

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Related contributions