Tina Docs
Introduction
Core Concepts
Querying Content
Editing
Customizing Tina
Going To Production
Media
Drafts
Guides
Further Reference

Using Media with AWS S3 Bucket


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.

Installation

With Yarn

yarn add next-tinacms-s3

With NPM

npm install next-tinacms-s3

Connect with S3 Bucket

You need some credentials provided to access AWS S3 Bucket to set this up properly.

Add the following variables to an .env file.

S3_REGION=<Your S3 Bucket Name: ex. us-east-1>
S3_BUCKET=<Your S3 Bucket Name: ex. my-bucket>
S3_ACCESS_KEY=<Your S3 Bucket access key>
S3_SECRET_KEY=<Your S3 Bucket access secret>

Setup S3 Bucket

You need to setup S3 Bucket and IAM user correctly.

1. The IAM user should have at least the following permissions for your bucket.

  • s3:ListBucket
  • s3:PutObject
  • s3:PutObjectAcl
  • s3:DeleteObject

2. The S3 bucket should have ACLs enabled.

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".

3. You should ensure objects in the S3 bucket are readable by anonymous users and writable by the IAM user.

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>/*"
},
{
"Sid": "ListBucket",
"Effect": "Allow",
"Principal": {
"AWS": "<ARN of the IAM user>"
},
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::<S3-Bucket-NAME>"
}
]
}

4. You should ensure the CORS configuration is set up correctly.

Images are uploaded using S3 pre-signed URLs. This requires the bucket to have a CORS configuration that allows the origin of the request to access the bucket.

[
{
"AllowedHeaders": [
"Authorization",
"Content-Range",
"Accept",
"Content-Type",
"Origin",
"Range"
],
"AllowedMethods": [
"PUT"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": [
"Content-Range",
"Content-Length",
"ETag"
],
"MaxAgeSeconds": 3000
}
]

Register the Media Store

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
},
},
})

Set up API routes

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.

Next.js

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 TinaCloud 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.S3_ACCESS_KEY || '',
secretAccessKey: process.env.S3_SECRET_KEY || '',
},
region: process.env.S3_REGION,
},
bucket: process.env.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
}
},
})

Framework Agnostic implementations

In the above example, we showed how to host the backend handler as a NextJS API function. If you are using Vercel with another framework, the same approach applies (with the small difference that you need to use /api/... instead of /pages/api/... for your handler).

You can also check out our Netlify Functions and AWS Lambda implementations.

Using a Custom URL

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"
}
)

Update Schema

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.