Tina Docs
Home
Blog
Showcase
Community
Introduction
Overview
Introduction To TinaCMS
Getting Started
Using the Tina Editor
FAQ
Core Concepts
Content Modeling
Data Fetching
Visual Editing
Querying Content
Overview
Writing custom queries
Editing
Overview
Markdown & MDX
Block-based editing
Single Document Collections
Customizing Tina
Overview
Validation
Custom Field Components
Custom List Rendering
Format and Parse Input
Filename Customization
Before Submit function
Going To Production
Overview
Tina Cloud
Self-Hosted
Drafts
Overview
Draft Fields
Editorial Workflow
Guides
Overview
Framework Guides
Separate Content Repo
Querying Tina Content at Runtime
Internationalization
Migrating From Forestry
Further Reference
Overview
Config
Schema
The "tina" folder
The TinaCMS CLI
Media
Overview
Repo-based Media
External Media Providers
Setting up Authentication
Cloudinary
Digital Ocean Spaces
AWS S3 Bucket
Search
Content API
Tina's edit state
The "tinaField" helper
Self-Hosted Components

Tina supports using external media providers, however a light backend media handler needs to be setup/hosted by the user. Tina offers some helpers to make this easy, for cloudinary,s3, & dos ("Digital Ocean Spaces")

Installation

yarn add @tinacms/auth

Depending on your site's framework & hosting provider, there are multiple ways to host the media handler.

In the following examples, replace <YOUR_MEDIA_STORE_NAME> with cloudinary,s3, or dos.

Option 1) Next.js API Routes (NextJS-only)

Set up a new API route in the pages directory of your Next.js app at pages/api/<YOUR_MEDIA_STORE_NAME>/[...media].ts.

Then add a new catch-all API route for media by calling createMediaHandler method of your media store library.

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/<YOUR_MEDIA_STORE_NAME>/[...media].ts
import { createMediaHandler } from 'next-tinacms-<YOUR_MEDIA_STORE_NAME>/dist/handlers'
import { isAuthorized } from '@tinacms/auth'
export default createMediaHandler({
// ...
authorized: async (req, _res) => {
try {
if (process.env.NODE_ENV == 'development') {
return true
}
const user = await isAuthorized(req)
return user && user.verified
} catch (e) {
console.error(e)
return false
}
},
})

Option 2) Vercel API Routes

Vercel supports creating Serverless functions by creating a /api directory at the project root. To set this up, follow the above NextJS-specific instructions, but use /api/<YOUR_MEDIA_STORE_NAME>/[...media].ts instead of /pages/api/<YOUR_MEDIA_STORE_NAME>/[...media].ts

Note: You may notice that the package names may contain "next" (e.g: next-tinacms-cloudinary). You can still use these packages for other frameworks.

Option 3) Netlify Functions

If your site is hosted on Netlify, you can use "Netlify Functions" to host your media handler.

First, you must set up redirects so that all requests to /api/* can be redirected to Netlify Functions. You can set up redirects at netlify.toml. We are also going to build our functions with esbuild so we will set that in the netlify.toml as well.

Add the following to the netlify.toml file in the root of your project.

[[redirects]]
from = '/api/*'
to = '/.netlify/functions/api/:splat'
status = 200
[functions]
node_bundler = 'esbuild'

Next, you must set up api routes for the media handler.

Install the following dependencies.

yarn add serverless-http express @tinacms/auth next-tinacms-<YOUR_MEDIA_STORE_NAME>

Make a new file called netlify/functions/api/api.js and add the following code.

Note: the file path could be different if you are using a different functions directory.
import ServerlessHttp from 'serverless-http'
import express, { Router } from 'express'
import { isAuthorized } from '@tinacms/auth'
import { createMediaHandler } from 'next-tinacms-<YOUR_MEDIA_STORE_NAME>/dist/handlers'
const app = express()
const router = Router()
const mediaHandler = createMediaHandler({
// ...
// See the next section for more details on what goes in the createMediaHandler
authorized: async (req, _res) => {
try {
if (process.env.NODE_ENV == 'development') {
return true
}
const user = await isAuthorized(req)
return user && user.verified
} catch (e) {
console.error(e)
return false
}
},
})
router.get('/cloudinary/media', mediaHandler)
router.post('/cloudinary/media', mediaHandler)
router.delete('/cloudinary/media/:media', (req, res) => {
req.query.media = ['media', req.params.media]
return mediaHandler(req, res)
})
app.use('/api/', router)
app.use('/.netlify/functions/api/', router)
export const handler = ServerlessHttp(app)

Option 3) AWS Lambda

If your site is hosted on AWS, you can use AWS Lambda to host your media handler. The following example uses the S3 media handler, but you can use any media handler.

Prerequisites

npm install express @vendia/serverless-express @tinacms/auth body-parser

Lambda Function
  1. To connect TinaCMS endpoints to AWS services, you'll need to create a Lambda Function in Node 14.x. Here's the code you'll need:
// index.ts
import express, { Router } from 'express'
import serverlessExpress from '@vendia/serverless-express'
import { isAuthorized } from '@tinacms/auth'
import { createMediaHandler } from 'next-tinacms-s3/dist/handlers'
import bodyParser from 'body-parser'
// Configure TinaCMS
const mediaHandler = createMediaHandler({
config: {
credentials: {
accessKeyId: process.env.TINA_AWS_ACCESS_KEY_ID || '',
secretAccessKey: process.env.TINA_AWS_SECRET_ACCESS_KEY || '',
},
region: process.env.TINA_AWS_REGION,
},
bucket: process.env.TINA_AWS_BUCKET_NAME || '',
authorized: async (req, _res): Promise<any> => {
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
}
},
})
// Set up the express app and router
const app = express()
const router = Router()
app.use(bodyParser.json())
// Define routes for media handling
router.get('/s3/media', mediaHandler)
router.post('/s3/media', mediaHandler)
router.delete('/s3/media/:media', (req, res) => {
req.query.media = ['media', req.params.media]
return mediaHandler(req, res)
})
// Mount the router on the app
app.use('/api/', router)
// Export the handler function
exports.handler = serverlessExpress({ app })
  1. Be sure to configure the necessary environment variables:
TINA_AWS_ACCESS_KEY_ID=******************
TINA_AWS_BUCKET_NAME=******************
TINA_AWS_REGION=********
TINA_AWS_SECRET_ACCESS_KEY=******************
API Gateway
  1. With the Lambda Function in place, you can proceed to create an API Gateway:
    1. Click on Create API
    2. Select REST API
  2. Once the API is created, create a resource under the root path named /api by going to Resources and selecting Create Resource
  3. Next, create a nested child path that takes all /api child paths by creating a resource that uses the {proxy+} special syntax. Make sure to tick the Configure as proxy resource.
    When setting up the ANY method, pass the Lambda Function that handles the TinaCMS media manager logic.
    Click on save and allow API Gateway to add permission to the Lambda Function
  4. Deploy your API by clicking on the Action dropdown and selecting Deploy API
    1. Select [New Stage] for the Deployment Stage and type a Stage name
    2. Once the API is deployed, you can see the Invoke URL in the Stages menu by clicking on the stage you've created.
  5. Configure Binary Media Types by going to the Settings Menu and adding the /* wildcard
CloudFront
  1. To complete the connection, create a new Origin for CloudFront using the Invocation URL of the API Gateway that was just created. Set the Origin Path to the name of the stage where the API was deployed.
  2. Create a new Behaviour for CloudFront that will intercept requests with the /api/s3/media* path and use the API Gateway origin that was just created. Make sure to allow the following HTTP methods: GET, HEAD, OPTIONS, PUT, POST, PATCH, and DELETE.
    1. Under the Cache key and origin requests section, select the Cache policy and origin request policy option. For the Cache policy, select CachingDisabled. For the Origin request policy, select AllViewerExceptHostHeader.
  3. Repeat the above process to create another behaviour that intercepts requests with the /api/s3/media/* path.

Register the Media Store on the frontend

Now, you can replace the default repo-based media with the external media store. You can register a media store via the loadCustomStore prop.

The loadCustomStore prop can be configured within tina/config file.

// tina/config.{ts,js,jsx}
// ...
export default defineConfig({
// ...
media: {
- tina: {
- publicFolder: "",
- mediaRoot: ""
- },
+ loadCustomStore: async () => {
+ const pack = await import("next-tinacms-<YOUR_MEDIA_STORE_NAME>");
+ return pack.TinaCloud<YOUR_MEDIA_STORE>;
+ },
}
})
Make sure you commit your changes to the config and tina-lock.json file at the same time as you push to production on TinaCloud as otherwise your assets will still be prefixed with https://assets.tina.io as if you were still using repo based media

Now you can manage external media store inside TinaCMS. To learn more about each media store in detail, please refer to the next sections.

Product

Showcase
TinaCloud
Introduction
How Tina Works
Roadmap

Resources

Blog
Examples
Support
Media

Whats New
TinaCMS
TinaCloud
Use Cases
Agencies
Documentation
Teams
Jamstack CMS
Benefits
MDX
Markdown
Git
Editorial Workflow
Customization
SEO
Comparisons
TinaCMS vs Storyblok
TinaCMS vs Sanity
TinaCMS vs DecapCMS
TinaCMS vs Contentful
TinaCMS vs Builder.io
TinaCMS vs Strapi
Integrations
Astro
Hugo
NextJS
Jekyll