Launch Sale: Creator and Business plans are discounted for a limited time. View pricing

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 (check ADN_API_KEY).
  • 400 — per-track create called without file_name, or an expired/invalid session ID.
  • Presigned PUT returns 403 if 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 $fetch and assert no Authorization header 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

  • createUploadSessionPOST /v1/upload_session (reference, openapi.json)
  • createUploadSessionTrackPOST /v1/upload/:upload_session_id/track (reference)
  • getTrackGET /v1/track/:track_id (reference)