Add Uppy Dashboard + Transloadit uploads to a Next.js (App Router) app, with server-side signature generation and optional /s3/store export.
Add this skill
npx mdskills install transloadit/integrate-uppy-transloadit-s3-uploading-to-nextjsComprehensive Next.js + Uppy + Transloadit integration guide with secure server-side signing and clear code examples
1---2name: integrate-uppy-transloadit-s3-uploading-to-nextjs3description: Add Uppy Dashboard + Transloadit uploads to a Next.js (App Router) app, with server-side signature generation and optional /s3/store export.4---56# Inputs78- Required env (server-only): `TRANSLOADIT_KEY`, `TRANSLOADIT_SECRET`9- Optional env: `TRANSLOADIT_TEMPLATE_ID` (recommended once you create a template)1011For local dev, put these in `.env.local`. Never expose `TRANSLOADIT_SECRET` to the browser.1213# Install1415```bash16npm i @transloadit/utils @uppy/core @uppy/dashboard @uppy/transloadit17```1819# Implement (Golden Path)2021Pick the root:22- If your project has `src/app`, use `src/app/...`23- Else use `app/...`2425## 1) Server: return signed Assembly options to the browser2627Create `app/api/transloadit/assembly-options/route.ts` (or `src/app/api/transloadit/assembly-options/route.ts` if you use `src/`):2829```ts30import { NextResponse } from 'next/server'31import { signParamsSync } from '@transloadit/utils/node'3233export const runtime = 'nodejs'3435function reqEnv(name: string): string {36 const v = process.env[name]37 if (!v) throw new Error(`Missing required env var: ${name}`)38 return v39}4041function formatExpiresUtc(minutesFromNow: number): string {42 const ms = Date.now() + minutesFromNow * 60_00043 return new Date(ms).toISOString().replace(/\.\d{3}Z$/, 'Z')44}4546export async function POST() {47 try {48 const authKey = reqEnv('TRANSLOADIT_KEY')49 const authSecret = reqEnv('TRANSLOADIT_SECRET')50 const templateId = process.env.TRANSLOADIT_TEMPLATE_ID5152 const params: Record<string, unknown> = {53 auth: { key: authKey, expires: formatExpiresUtc(30) },54 }5556 if (templateId) {57 params.template_id = templateId58 } else {59 // Minimal "known good" steps (works without pre-creating a template).60 params.steps = {61 resized: {62 robot: '/image/resize',63 use: ':original',64 width: 320,65 },66 }67 }6869 const paramsString = JSON.stringify(params)70 const signature = signParamsSync(paramsString, authSecret)7172 // Uppy expects `{ params: <string|object>, signature: <string> }`.73 return NextResponse.json({ params: paramsString, signature })74 } catch (err) {75 const message = err instanceof Error ? err.message : 'Unknown error'76 return NextResponse.json({ error: message }, { status: 500 })77 }78}79```8081## 2) Client: mount Uppy Dashboard + Transloadit plugin8283Add the CSS (for App Router, do it in your root layout):8485```ts86import '@uppy/core/css/style.min.css'87import '@uppy/dashboard/css/style.min.css'88```8990Create a client component like `app/upload-demo.tsx`:9192```tsx93'use client'9495import { useEffect, useMemo, useRef, useState } from 'react'96import Uppy from '@uppy/core'97import Dashboard from '@uppy/dashboard'98import Transloadit, { type AssemblyOptions } from '@uppy/transloadit'99100export default function UploadDemo() {101 const dashboardEl = useRef<HTMLDivElement | null>(null)102 const [results, setResults] = useState<unknown>(null)103 const [uploadPct, setUploadPct] = useState<number>(0)104105 const uppy = useMemo(() => {106 const instance = new Uppy({107 autoProceed: true,108 restrictions: { maxNumberOfFiles: 1 },109 })110111 instance.use(Transloadit, {112 waitForEncoding: true,113 alwaysRunAssembly: true,114 assemblyOptions: async (): Promise<AssemblyOptions> => {115 const res = await fetch('/api/transloadit/assembly-options', { method: 'POST' })116 if (!res.ok) throw new Error(`Failed to get assembly options: ${res.status}`)117 return (await res.json()) as AssemblyOptions118 },119 })120121 return instance122 }, [])123124 useEffect(() => {125 if (!dashboardEl.current) return126127 uppy.use(Dashboard, {128 target: dashboardEl.current,129 inline: true,130 proudlyDisplayPoweredByUppy: false,131 hideUploadButton: true,132 hideProgressDetails: false,133 height: 350,134 })135136 const onResult = (stepName: string, result: unknown) =>137 setResults((prev: unknown) => {138 const base: Record<string, unknown> =139 typeof prev === 'object' && prev !== null ? { ...(prev as Record<string, unknown>) } : {}140 const existing = base[stepName]141 base[stepName] = Array.isArray(existing) ? existing.concat([result]) : [result]142 return base143 })144145 const onUploadProgress = (_file: unknown, progress: { bytesUploaded: number; bytesTotal: number | null }) => {146 if (!progress?.bytesTotal) return147 setUploadPct(Math.round((progress.bytesUploaded / progress.bytesTotal) * 100))148 }149150 uppy.on('transloadit:result', onResult)151 uppy.on('upload-progress', onUploadProgress)152153 return () => {154 uppy.off('transloadit:result', onResult)155 uppy.off('upload-progress', onUploadProgress)156 uppy.getPlugin('Dashboard')?.uninstall()157 uppy.destroy()158 }159 }, [uppy])160161 return (162 <section>163 <div ref={dashboardEl} />164 <div style={{ marginTop: 12 }}>{uploadPct}%</div>165 <pre style={{ marginTop: 12 }}>{results ? JSON.stringify(results, null, 2) : '(no results yet)'}</pre>166 </section>167 )168}169```170171# Optional: /s3/store Export172173Recommended approach: create Template Credentials in Transloadit (so you don’t ship AWS keys anywhere) and reference them in `/s3/store`.174175Example steps:176177```json178{179 "resized": { "robot": "/image/resize", "use": ":original", "width": 320 },180 "exported": {181 "robot": "/s3/store",182 "use": "resized",183 "credentials": "YOUR_TRANSLOADIT_TEMPLATE_CREDENTIALS_NAME",184 "path": "uppy-nextjs/${unique_prefix}/${file.url_name}",185 "acl": "private"186 }187}188```189190If you intentionally want public objects, change `"acl"` to `"public-read"` (and consider bucket policy, access logs, and data retention).191192Then create a template and set `TRANSLOADIT_TEMPLATE_ID`:193194```bash195npx -y @transloadit/node templates create uppy-nextjs-resize-to-s3 ./steps.json -j196```197198# References (Internal)199200- Working reference implementation: `https://github.com/transloadit/skills/tree/main/scenarios/integrate-uppy-transloadit-s3-uploading-to-nextjs`201- Proven steps JSON: `https://github.com/transloadit/skills/blob/main/scenarios/integrate-uppy-transloadit-s3-uploading-to-nextjs/transloadit/steps/resize-only.json`, `https://github.com/transloadit/skills/blob/main/scenarios/integrate-uppy-transloadit-s3-uploading-to-nextjs/transloadit/steps/resize-to-s3.json`202203Tested with (see the scenario lockfile for the exact versions):204- Next.js 16.1.6 (App Router)205- React 19.2.3206- @transloadit/utils 4.3.0 (Assembly signing)207- @uppy/core 5.2.0, @uppy/dashboard 5.1.1, @uppy/transloadit 5.5.0208
Full transparency — inspect the skill content before installing.