Storage + Cron

Doc Status: Good | ✓ Clear summary | ✓ Easy to read | ✓ Matches code | ✓ Good structure | ✓ Professional look | ✓ Visual components

Convex Storage (Small Files)

Convex storage is for small admin uploads (avatars, attachments under ~5MB) that need auth-gated access tied to the Convex Auth session.
// packages/backend/convex/files.ts

// Mutation: generate a signed upload URL
export const generateUploadUrl = mutation({
  args: {},
  handler: async (ctx) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error('UNAUTHENTICATED');
    return await ctx.storage.generateUploadUrl();
  },
});

// Query: resolve storage ID to public URL
export const getUrl = query({
  args: { storageId: v.id('_storage') },
  handler: async (ctx, { storageId }) => await ctx.storage.getUrl(storageId),
});
Client uploads to the signed URL, then stores the storageId on the row that owns it.

R2 (Heavy Media)

R2 is for heavy media (instrument HD photos, video clips) where Sanity CDN would be uneconomical. R2 is provisioned via Terraform. Uploads go through a Convex action that signs a PUT URL — the bucket stays private; the secret is never exposed to the browser.
// packages/backend/convex/storage.ts
'use node';
import { action } from './_generated/server';

export const getUploadUrl = action({
  args: { filename: v.string(), contentType: v.string() },
  handler: async (ctx, { filename, contentType }) => {
    // Generate a signed R2 PUT URL
    // Return { uploadUrl, publicUrl }
  },
});

Cron Jobs

Cron jobs run without user identity. ctx.auth.getUserIdentity() returns null. Use environment variables for credentials:
// packages/backend/convex/crons.ts
import { cronJobs } from 'convex/server';
import { internal } from './_generated/api';

const crons = cronJobs();

// Purge WebRTC signaling messages past their 1h TTL
crons.interval('purge expired signaling', { hours: 1 }, internal.signaling.purgeExpired, {});
export default crons;
// ✅ GOOD — env var for credentials
const lark = new LarkClient(process.env.LARK_TOKEN!);

// ❌ BAD — auth identity in a cron (always null)
const identity = await ctx.auth.getUserIdentity(); // null

Schedule Syntax

MethodExample
crons.interval{ seconds: 30 }, { minutes: 5 }, { hours: 1 }
crons.cron"0 16 * * *" (standard cron, UTC)
crons.hourlyAt minute 0 of every hour
crons.dailyAt midnight UTC

Cloudflare TURN Credentials

Piano Mirror needs TURN (NAT relay) for ~20% of connections. Ephemeral credentials are minted per-session via a Convex action backed by Cloudflare Realtime:
// packages/backend/convex/signaling.ts
export const turnConfig = action({
  args: {},
  handler: async (): Promise<{ iceServers: RTCIceServer[] }> => {
    // Falls back to STUN-only if CLOUDFLARE_TURN_TOKEN_ID not set
    // ...
  },
});
Set via npx convex env set CLOUDFLARE_TURN_TOKEN_ID and CLOUDFLARE_TURN_API_TOKEN.