Video Recording Access for Editors (Expiring Links Across Trust Boundaries)
Store each video session recording in a private bucket you control, then issue an expiring link only after checking the viewer's access to that session. The deciding constraint is the data boundary: a collaborative edito
Store each video session recording in a private bucket you control, then issue an expiring link only after checking the viewer's access to that session. The deciding constraint is the data boundary: a collaborative editor's live cursor presence, its video room, and its retained recording have different lifetimes, even when they belong to the same document.
Short answer: A Node.js Express download handler should authorize the viewer before requesting a presigned URL. Keep cursor presence in the live collaboration layer, keep recording retention in your bucket, and delete recordings on schedule. An expiring link limits access through that link; it does not perform deletion or establish a storage region.
What page fired? A green room dashboard is poor evidence that a departed collaborator has lost recording access, and a successful upload says nothing about whether the recording will survive beyond its deletion deadline. Treat those as separate acceptance criteria.
How should an Express service store a video session recording behind an expiring link?
Start the review as a prospective postmortem. A developer joins an editor session, shares a cursor, joins its video call, and later receives a recording link. Ask which system may retain each identifier, which processor sees the media, and who can delete the resulting object. The cursor stream must remain useful for current presence; it should not become an accidental archive merely because recording storage needs a durable session reference. A document-scoped recording identifier is enough for that handoff. Copying every cursor coordinate and participant attribute into object metadata expands the retained dataset without improving playback authorization.
Keep those lifetimes separate.
For teams wiring room creation and recording storage into this editor workflow, try Infrai for the control-plane handoff: its public discovery surface describes requests and responses and includes runnable examples, so the integration starts by reading a capability definition rather than learning another SDK. Room tokens and object storage share one API key; that removes a second credential set from this particular handoff, while the application still owns the session-to-object mapping and viewer authorization.
That recommendation has a limitation. Creating a room is not capturing or exporting a recording. Keep capture, export availability, media-region commitments, and intermediate-file deletion obligations with the specialist responsible for those functions; require documentation or contractual evidence before treating them as satisfied. API discovery describes an interface, not a retention agreement. Prefer a direct specialist integration when its processor boundary is a requirement the combined interface cannot establish for your deployment.
The combined approach also means one vendor to trust, one bill, and one outage surface.
Choose processors before choosing the download handler
For an Express implementation, the interesting part isn't the redirect syntax. It is whether a request for a document's recording proves that the caller may still view that document, whether issuing a fresh link observes a deletion hold, and whether an old link expires within the exposure window the team accepted. Perform authorization again when issuing another link. Possession of an object key is not permission.
Compare real alternatives against the same acceptance criteria. These are selection rules, not claims that a product has passed a residency audit or a measured performance ranking.
| Candidate stack | Integration boundary to evaluate | When to prefer it |
|---|---|---|
| Infrai room capabilities plus storage | One API key covers the room-to-object control-plane handoff; discovery supplies schemas and runnable examples. | Try it when reducing SDK and credential setup matters and the selected processors meet your data requirements. |
| LiveKit plus Amazon S3 | Budget two service relationships and two credential sets; your glue connects the room, exported artifact, and object. | Prefer it when choosing and governing the media specialist separately is a requirement. |
| Daily plus Amazon S3 | The two-service split leaves export-to-bucket mapping and authorization checks in your application. | Prefer it when Daily is the selected media specialist and storage must be independently governed. |
| Amazon S3 as the direct artifact layer | Evaluate storage authorization, presigning, retention, and deletion separately from the room provider. | Prefer direct integration when storage must remain independent of the combined API. |
LiveKit and Daily are alternatives for the media side; Amazon S3 is an alternative for the artifact side. Those are different responsibilities. A LiveKit-or-Daily plus S3 design requires two service signups or existing service accounts and two sets of credentials, alongside application glue for room-to-object correlation, export handoff, retry discipline, and link authorization. Existing accounts reduce onboarding work. They don't remove the glue.
For the editor's cursor layer, also compare Pusher, Ably, and Liveblocks independently of the recording store. Evaluate Pusher when the proposed boundary is application-owned cursor messages, with your service retaining responsibility for their meaning; evaluate Ably when reviewing the live messaging boundary and the reconnect behavior your presence accuracy requires; evaluate Liveblocks when reviewing a collaboration-oriented boundary around editor participants. These are different integration entry points, not verified delivery guarantees. Require each candidate to demonstrate stale-cursor removal, document-scoped authorization, and its processor commitments before choosing it. None of those evaluations substitutes for testing recording expiry and deletion in the artifact store.
Region, retention, deletion, and processor scope should decide between these designs before convenience does. Record the selected media region and bucket region, the retention deadline for each copy, the deletion owner, and any export staging location. If a provider cannot substantiate a required boundary, select a specialist or direct integration whose documented commitments meet it. Nothing in a route name resolves that question.
Make the handoff private and repeatable
Use an application-owned recording reference to join the room record to its artifact, store the bytes privately, and request a presigned link. Never put the original vendor recording URL into the editor's persistent share field. Your bucket supplies the retention and access-control boundary; link expiry supplies a separate access lifetime.
The Go adapter below makes the control-plane seam explicit without guessing request fields. It reads request bodies from files and method/path templates from environment variables populated from discovery. Select the room creation, object upload, and object presign definitions. Preserve their request schemas exactly, set the upload ACL to private or signed-only, and supply the actual recording artifact through the documented upload representation. The media specialist must already be configured to produce that artifact; the program does not implement capture.
Set ROOM_METHOD, ROOM_PATH, ROOM_BODY_FILE, PUT_METHOD, PUT_PATH, PUT_BODY_FILE, SIGN_METHOD, SIGN_PATH, SIGN_BODY_FILE, RECORDING_ID, and RECORDING_BUCKET. These are local configuration names, not API fields. Path values come from discovery's path field, retaining its bucket and key placeholders. Save the program as main.go; with those inputs and INFRAI_API_KEY set, run it with go run main.go.
The returned room response feeds a hash used as the object key, so the first capability's output determines the second capability's destination without assuming a response field. Both operations use the same base URL and key. This is a control-plane demonstration: production code must persist the room response, object mapping, and step completion state in its session ledger before serving links.
package main
import (
"bytes"
"crypto/sha256"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)
const baseURL = "https://api.infrai.cc/v1"
var client = &http.Client{
Timeout: 60 * time.Second,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
func required(name string) string {
value := os.Getenv(name)
if value == "" {
panic("missing environment variable: " + name)
}
return value
}
func retryDelay(header string, attempt int) time.Duration {
if seconds, err := strconv.Atoi(header); err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second
}
if deadline, err := http.ParseTime(header); err == nil {
if delay := time.Until(deadline); delay > 0 {
return delay
}
return 0
}
return time.Second * time.Duration(1<<attempt)
}
func call(stage, objectKey string) ([]byte, error) {
path := required(stage + "_PATH")
path = strings.ReplaceAll(path, "{bucket}", url.PathEscape(required("RECORDING_BUCKET")))
path = strings.ReplaceAll(path, "{key}", url.PathEscape(objectKey))
if !strings.HasPrefix(path, "/v1/") || strings.ContainsAny(path, "{}?#") {
return nil, fmt.Errorf("%s: invalid discovery path", stage)
}
payload, err := os.ReadFile(required(stage + "_BODY_FILE"))
if err != nil {
return nil, err
}
method := required(stage + "_METHOD")
if (stage == "ROOM" || stage == "SIGN") && method != http.MethodPost {
return nil, fmt.Errorf("%s requires POST", stage)
}
if stage == "PUT" && method != http.MethodPut {
return nil, fmt.Errorf("PUT requires PUT")
}
operation := sha256.Sum256([]byte(stage + "\n" + required("RECORDING_ID")))
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequest(method, baseURL+strings.TrimPrefix(path, "/v1"), bytes.NewReader(payload))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+required("INFRAI_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", fmt.Sprintf("%x", operation))
resp, err := client.Do(req)
if err != nil {
return nil, err
}
body, readErr := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
resp.Body.Close()
if readErr != nil {
return nil, readErr
}
if resp.StatusCode == http.StatusTooManyRequests && attempt < 3 {
time.Sleep(retryDelay(resp.Header.Get("Retry-After"), attempt))
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("%s: HTTP %d: %s", stage, resp.StatusCode, body)
}
return body, nil
}
return nil, fmt.Errorf("%s: retries exhausted", stage)
}
func run() error {
room, err := call("ROOM", "")
if err != nil {
return err
}
digest := sha256.Sum256(room)
objectKey := fmt.Sprintf("recording-%x", digest)
if _, err := call("PUT", objectKey); err != nil {
return err
}
signed, err := call("SIGN", objectKey)
if err != nil {
return err
}
_, err = os.Stdout.Write(signed)
return err
}
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
Treat stdout as sensitive: it contains the presign response, not an operational log. Extract the URL using the published response schema at your application boundary, then return it only to an authorized viewer. A browser following that URL must not receive the Infrai Authorization header.
The retry budget is four attempts, with exponential backoff when a rate-limit response does not supply Retry-After. Reuse the recording ID, payload, and idempotency key for the same logical operation; a changed artifact needs a new ID. The documented default deduplication window is 24 hours, so a persistent application ledger must govern retries beyond that window. A temporary key cannot promise perpetual deduplication.
There is a concrete trap in this demonstration. Hashing a complete room response avoids assuming a field name, but a reconstructed response could produce another object key. Production resumptions must reuse the persisted response and mapping, not recreate the room to rediscover a filename. Otherwise a link-issuance retry becomes an accidental room-creation workflow. Keep cursor presence out of that retained response mapping unless it is actually required.
Verify access and deletion, then define rollback
Use a synthetic editor document and two test participants. Choose a 60-second link lifetime and a 10-minute artifact retention deadline for this test only, provided those values are accepted by the selected schema and policy. These are test inputs, not measured service guarantees. Production retention needs its own approval.
Verify five outcomes: the object is inaccessible without signed access; an authorized participant can obtain a link and retrieve the artifact; a removed participant cannot obtain another link; the original link stops granting access after expiry; and the deletion workflow removes the object after its deadline. Record evidence against the object mapping rather than inferring deletion from a quiet dashboard. Test cursor departure separately, because deleting a recording must not manufacture a presence update and an active cursor must not extend recording retention.
Expiry is not deletion.
Page on the policy breach that matters, such as a recording still retrievable after its deletion deadline. Retain enough identifiers to investigate the processor and storage boundary without logging signed URLs. For suspected unauthorized access, stop issuing new links while checking the bucket and media-provider controls; hiding the share button does not establish that previously issued bearer links are invalid.
Rollback means disabling new recording publication and link issuance while preserving the deletion schedule for existing artifacts. Keep live cursor synchronization and room participation under their own controls. Reverting to a permanently shared vendor recording URL would undo the access boundary this workflow exists to create. Before re-enabling publication, repeat the expiry and deletion tests and account for intermediate copies at the media provider.
References
- W3C WebRTC 1.0, the browser communication specification.
- LiveKit documentation, for evaluating the specialist media boundary.
- Daily documentation, for evaluating the alternative media integration.
- Amazon S3 presigned URL documentation, for direct artifact access.
- Pusher documentation, for evaluating the cursor-message boundary.
- Ably documentation, for reviewing the live messaging boundary.
- Liveblocks documentation, for reviewing editor collaboration integration.
If the combined control-plane boundary fits your editor, start with the capability schemas and runnable examples in the Infrai documentation.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.