Manage S3 Bucket media assets in TinaCMS.
The following guide relies on NextJS's API functions to authenticate the 3rd-party media interactions. We hope to document a framework-agnostic approach soon.
yarn add next-tinacms-s3
npm install next-tinacms-s3
You need some credentials provided to access AWS S3 Bucket to set this up properly.
next-tinacms-s3 uses environment variables within the context of a Next.js site to properly access your S3 Bucket account.
Add the following variables to an .env
file.
NEXT_PUBLIC_S3_REGION=<Your S3 Bucket Name: ex. us-east-1>
NEXT_PUBLIC_S3_BUCKET=<Your S3 Bucket Name: ex. my-bucket>
NEXT_PUBLIC_S3_ACCESS_KEY=<Your S3 Bucket access key>
S3_SECRET_KEY=<Your S3 Bucket access secret>
You need to setup S3 Bucket and IAM user correctly.
"s3:ListBucket", "s3:PutObject", "s3:DeleteObject"
You should be able to go to the AWS S3 console and navigate to the bucket details for the bucket you try to write objects to. You'll see a tab called 'Permissions'. There you have the option to change the "Object Ownership" at a block with the same title.
Once there, you can choose the option "ACLs enabled".
i.e. You can disable block public access settings
and set up the bucket policy like following:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicRead",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::<S3-Bucket-NAME>/*"
},
{
"Sid": "LimitedWrite",
"Effect": "Allow",
"Principal": {
"AWS": "<ARN of the IAM user>"
},
"Action": ["s3:PutObject", "s3:PutObjectAcl", "s3:DeleteObject"],
"Resource": "arn:aws:s3:::<S3-Bucket-NAME>/*"
}
]
}
You can register the S3 Media store via the loadCustomStore
prop.
The loadCustomStore
prop can be configured within tina/config.{js,ts,tsx}
.
//tina/config.{ts,js}
//...
export default defineConfig({
//...
media: {
loadCustomStore: async () => {
const pack = await import('next-tinacms-s3')
return pack.TinaCloudS3MediaStore
},
},
})
Tina's "external media provider" support requires a light backend media handler, that needs to be setup/hosted by the user. There are multiple ways to do this, including the framework-agnostic Netlify Functions implementation.
NOTE: this step will show you how to set up an API route for Next.js. If you are using a different framework, you will need to set up your own API route.
Set up a new API route in the pages
directory of your Next.js app, e.g. pages/api/s3/[...media].ts
.
Then add a new catch all API route for media.
Call createMediaHandler
to set up routes and connect your instance of the Media Store to your S3 Bucket.
Import isAuthorized
from @tinacms/auth
.
The authorized
key will make it so only authorized users within Tina Cloud can upload and make media edits.
// pages/api/s3/[...media].ts
import {
mediaHandlerConfig,
createMediaHandler,
} from 'next-tinacms-s3/dist/handlers'
import { isAuthorized } from '@tinacms/auth'
export const config = mediaHandlerConfig
export default createMediaHandler({
config: {
credentials: {
accessKeyId: process.env.NEXT_PUBLIC_S3_ACCESS_KEY || '',
secretAccessKey: process.env.S3_SECRET_KEY || '',
},
region: process.env.NEXT_PUBLIC_S3_REGION,
},
bucket: process.env.NEXT_PUBLIC_S3_BUCKET || '',
authorized: async (req, _res) => {
if (process.env.NODE_ENV === 'development') {
return true
}
try {
const user = await isAuthorized(req)
return user && user.verified
} catch (e) {
console.error(e)
return false
}
},
})
For Netlify use case, please read how to set up Netlify Functions here
If you're using a custom URL for your S3 bucket, you can pass in a cdnUrl
value to createMediaHandler
.
export default createMediaHandler({
config: ...,
bucket: ...,
authorized: ...,
},
{
cdnUrl: "https://my-custom-domain.com"
}
)
Now that the media store is registered and the API route for media set up, let's add an image to your schema.
In your schema add a new field for the image, e.g:
{
name: 'hero',
type: 'image',
label: 'Hero Image',
}
Now, when editing your site, the image field will allow you to connect to your S3 Bucket via the Media Store to manage your media assets.
Resources
© TinaCMS 2019–2025