Vue Secure Upload
The secure upload flow in Vue 3 / Nuxt. Your API key never leaves the server; the browser only ever handles a
session-scoped upload URL and the presigned storage PUT.
When AudioDN fits this job
Use AudioDN when you want direct-to-storage uploads and automatic processing without proxying large files through
your own backend. This recipe is the Vue counterpart to the runnable
Next.js secure upload example — the server boundary is identical.
Architecture
- Server route (Nuxt): mints the upload session with the API Access key.
- Client (Vue): creates the per-track URL (no key), PUTs the bytes, polls readiness via a small status proxy.
- Playback (client): the
<audiodn-player>component streams with a Client-Side Player key.
Server / client boundary
Two things stay server-side: the API Access key (mints the session) and the
track readiness check (
GET /v1/track/:id needs the Bearer key). The browser only
calls the session-gated per-track endpoint and the presigned PUT.
Environment variables
# Server-only (never exposed to the browser bundle)
ADN_API_KEY=adn_... # API Access key
ADN_COLLECTION_ID=... # collection uploads land in
# Public (safe in the client bundle) — Nuxt exposes NUXT_PUBLIC_* to the app
NUXT_PUBLIC_ADN_PLAYER_KEY=adn_player_... 1. Server route: mint the upload session
// server/api/upload-session.post.ts — mint an upload session (Bearer stays server-side).
export default defineEventHandler(async () => {
const res = await $fetch('https://api.audiodelivery.net/v1/upload_session', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.ADN_API_KEY}`,
'Content-Type': 'application/json',
},
body: { collection_id: process.env.ADN_COLLECTION_ID },
});
// Return ONLY the session id — the client needs nothing else to create a track.
return { upload_session_id: res.upload_session_id };
}); 2. Server route: readiness proxy
// server/api/track-status.get.ts — proxy readiness checks.
// GET /v1/track/:id needs the Bearer key, so the browser cannot poll it directly.
export default defineEventHandler(async (event) => {
const { trackId } = getQuery(event);
const res = await $fetch(`https://api.audiodelivery.net/v1/track/${trackId}`, {
headers: { Authorization: `Bearer ${process.env.ADN_API_KEY}` },
});
return { status: res.track?.track_status_id };
}); 3. Vue component: create track, upload, poll, play
<!-- components/SecureUpload.vue -->
<script setup lang="ts">
import { ref } from 'vue';
const API = 'https://api.audiodelivery.net/v1';
const status = ref('idle');
const trackId = ref<string | null>(null);
async function upload(file: File) {
status.value = 'creating-session';
// 1. Ask OUR server to mint the upload session (keeps the API key server-side).
const { upload_session_id } = await $fetch('/api/upload-session', { method: 'POST' });
// 2. Create a per-track upload URL. No Authorization header — the session ID authorizes it.
status.value = 'creating-track';
const { track_id, track_upload } = await $fetch(
`${API}/upload/${upload_session_id}/track`,
{ method: 'POST', body: { file_name: file.name } },
);
trackId.value = track_id;
// 3. PUT the bytes straight to storage.
status.value = 'uploading';
await fetch(track_upload.upload_url, { method: track_upload.method, body: file });
// 4. Poll our status proxy until the track is ready.
status.value = 'processing';
let s: string | undefined;
do {
await new Promise((r) => setTimeout(r, 5000));
s = (await $fetch('/api/track-status', { query: { trackId: track_id } })).status;
} while (s && !['ready', 'incomplete', 'error'].includes(s));
status.value = s === 'ready' ? 'ready' : (s ?? 'error');
}
function onChange(e: Event) {
const file = (e.target as HTMLInputElement).files?.[0];
if (file) upload(file);
}
</script>
<template>
<input type="file" accept="audio/*" @change="onChange" />
<p>Status: {{ status }}</p>
<!-- Play once ready. The Client-Side Player key is safe in the browser. -->
<audiodn-player
v-if="status === 'ready' && trackId"
:api-key="$config.public.adnPlayerKey"
scope="track"
:id="trackId"
variants="hq,lq"
/>
</template> 4. Nuxt config
// nuxt.config.ts — register the web component tag and expose the public key.
export default defineNuxtConfig({
runtimeConfig: {
public: { adnPlayerKey: '' }, // filled from NUXT_PUBLIC_ADN_PLAYER_KEY
},
vue: {
// Treat <audiodn-*> as custom elements, not Vue components.
compilerOptions: { isCustomElement: (tag) => tag.startsWith('audiodn-') },
},
app: {
head: {
script: [{ src: 'https://unpkg.com/@audiodn/components@latest/dist/player.js', type: 'module' }],
},
},
}); Expected errors
401— the server route is missing the Bearer key (checkADN_API_KEY).400— per-track create called withoutfile_name, or an expired/invalid session ID.- Presigned
PUTreturns403if the per-track URL has expired — create a fresh track and retry. 404— polling a track ID that does not exist.
Testing
- Unit-test the two server routes with a mocked
$fetchand assert noAuthorizationheader ever reaches the browser bundle. - End-to-end: upload a small file and assert status transitions
uploading → processing → ready. - Confirm the player renders only after
ready.
Deployment
Deploy Nuxt to any Node host or to Cloudflare/Vercel (Nuxt Nitro presets). Set ADN_API_KEY and
ADN_COLLECTION_ID as server secrets and NUXT_PUBLIC_ADN_PLAYER_KEY as a public var.
OpenAPI operations used
createUploadSession—POST /v1/upload_session(reference, openapi.json)createUploadSessionTrack—POST /v1/upload/:upload_session_id/track(reference)getTrack—GET /v1/track/:track_id(reference)