Media file upload and the presign lifecycle
Every binary your tenant stores — an MMS attachment, a brand logo, a knowledge-base document, a wallet-pass image, a session-replay chunk — lives on one object-storage plane: objects in a private Google Cloud Storage bucket, keyed under your tenant’s prefix, reachable only through time-boxed signed URLs. The media asset store page covers the files API as a customer surface; this page is the plane underneath it — the bucket layout, the presign lifecycle, and the shared failure modes every consumer inherits. The mental model is three sentences. Uploads enter through authenticated multipart endpoints and are validated before their bytes are stored. Objects land privately under a tenant-prefixed key, so the leading path segment is the owning tenant. Reads never proxy bytes through the API — they mint a signed URL that dies on its own clock, so a stored link is always a snapshot with an expiry, not a durable reference.Ingestion path — POST /files
The canonical ingest isPOST /api/v1/files/upload: one
multipart/form-data body with a single file field.
- Scope and role. API-key callers must hold
files:readorfiles:writefor any file route; upload and delete additionally requirefiles:writeplus anowner,admin, ordeveloperrole. Dashboard session callers bypass the scope check and are gated on role alone. - Size. The multipart parser aborts the stream at the 25 MB
ceiling, before the body buffers into memory. An oversized upload
returns
413 PAYLOAD_TOO_LARGE. - Content. The declared MIME type must be on the allow-list —
images (JPEG, PNG, GIF, WebP), audio (MPEG, OGG, WAV, AAC, AMR),
video (MP4, 3GPP), documents (PDF, Office formats), plain text, CSV,
and ZIP. A magic-byte sniff then rejects files whose first bytes
disagree with the declared type (a renamed executable claiming to be
a JPEG fails here). Violations return
422 Unsupported file type. SVG is excluded deliberately — it can embed script and would be a stored-XSS vector if served from a platform-controlled path.
.. sequences are stripped, and any character outside
letters, digits, dash, underscore, and dot is removed.
Bucket layout — the tenant prefix
A successful upload writes exactly one object:file_-prefixed
token minted at upload; the underscore between id and filename is a
lookup anchor, so GET /api/v1/files/:id, presign, and delete all
resolve a file by scanning the ${tenantSchema}/${fileId}_ prefix and
can never match a sibling object whose id merely shares a leading
substring.
Two consequences worth building against:
- Listing is prefix enumeration.
GET /api/v1/filesscans your tenant prefix and returns cursor-paginated rows, each with a fresh one-hour signed URL. There is no separate metadata database — object metadata on the bucket is the record. - There is no cross-tenant read. Read, presign, delete, and the internal re-sign helper all resolve inside the requesting tenant’s own prefix. A URL that names another tenant’s object fails to resolve rather than silently borrowing it.
<tenant_id>/regulatory/<doc_id>
with tighter IAM and retention; session-replay overflow chunks and
recordings write their own gs:// paths. What is shared is the model:
private object, tenant-prefixed key, signed-URL egress.
Presigned URL lifecycle
Objects are stored privately; the signed URL is the only egress. The API never proxies object bytes — every read returns a URL backed by GCS V4 signing.
Upload accepts
ttl_ms when you intend to pin the returned URL into a
long-lived row. Presign mints a fresh URL for an object that already
exists:
expires_at
(the instant the link dies) and ttl_ms (the effective lifetime after
clamping), so your integration knows exactly when to re-mint.
There is no revocation primitive — a signed URL lives until its expiry.
That is why the default is one hour and the floor is sixty seconds: pick
the shortest TTL that survives your consumer’s render or playback
window, and re-mint on demand rather than pinning week-long links.
Several platform read paths re-mint for you. When an email is
re-opened, when white-label branding renders, or when a message preview
resolves archived media, the platform checks whether the stored URL
points at one of your objects and issues a fresh short-lived URL — only
for objects under your own tenant prefix, and it drops a URL it can
positively prove is already expired instead of serving a guaranteed
dead link.
Delete and garbage collection
DELETE /api/v1/files/:id removes the object and returns 204; an upload
or delete against an id outside your tenant namespace returns 404, not
someone else’s bytes. Every upload and delete emits an audit event
visible under Compliance > Audit.
Explicit delete is only half the story. Files you upload but never
attach — an abandoned MMS draft, an orphaned branding image — show up
as storage usage under your organization’s billing rather than as a
rejected request, so clean-up is your control to exercise. For
channel-owned artifacts, retention sweeps do the deletion for you:
recordings purge their media when their window closes, after firing a
pre-delete event with a final download window (see Recording
lifecycle), and backing buckets carry a
hard lifecycle cap as a safety net so no object can outlive the
platform-wide ceiling regardless of the tenant posture. Retention
windows stay tenant-owned; the sweep is the garbage collector that
enforces them.
Rate-limit surface
Limits are per method and fire at the API layer, in front of every route:
These are API-plane limits, not storage limits: bulk backfills and
attachment-heavy imports should spread uploads across minutes or accept
a 429 and retry. Signed-URL downloads do not pass through the API and
are not rate-limited here — once you hold a live URL, reading through
it is a GCS operation.
Who consumes this plane
Every consumer inherits the same properties: private storage, a
tenant-prefixed key, signed-URL-only egress, and expiry-by-default.
Surfaces that pin a URL into a durable row either request a longer TTL
at upload or re-mint on read.
Failure modes
See also
CDN assets model
The files API as a customer surface — templates, branding, and the
re-mint behavior on read paths.
Recording lifecycle
Capture-to-purge states, the retention pre-delete event, and the
safety-net bucket lifecycle.
Session replay
The inline-head plus durable-chunk split this plane stores.
Tenant isolation
The per-tenant boundary the object-key prefix enforces in storage.
Number lifecycle
Where the stricter regulatory bucket fits number provisioning.
Retention windows and deletion
The tenant-owned windows whose sweeps garbage-collect stored media.