A recording is not a file with a video player. It is a long-lived artifact whose access rules outlive the call, the membership, and sometimes the room itself. Here's a small framework for getting those rules right — and what Nextcloud Talk actually does today.
A meeting recording sits in an uncomfortable place. It looks like a file, but a file's access model — "whoever can open the path can play it" — is wrong for it. It looks like a chat message, but a chat message's access model — "anyone currently in the room" — is also wrong for it. Recordings outlive memberships. They are evidence. They are subject to legal hold. New users joining a room next year should not automatically gain access to last quarter's HR review, and old users removed from a room should not retain access to anything that was discussed after they left.
Most of the hard work in shipping a meeting recorder is therefore not capturing media. It is deciding, before any code is written, what authority controls who can later watch the result — and where that decision lives in the system. Below is a small framework for thinking about that authority, written for an engineer who knows Nextcloud Talk well enough to recognise the moving parts but is about to make architectural choices that will outlast the v1 ship date.
Before access control, language. Three concepts in Talk get confused with each other, and confusing them is where most recording bugs start.
ONE_TO_ONE, GROUP, PUBLIC, plus NOTE_TO_SELF, CHANGELOG, and historical variants), an object association (breakout, event, instant-meeting, file, email), listability (LISTABLE_NONE/USERS/ALL), read-only state, lobby state, default participant permissions, password and public access, federation state (HAS_FEDERATION_*), recording state (RECORDING_NONE/VIDEO/AUDIO/STARTING/FAILED), and recording-consent settings. It is durable. It is where recording metadata should hang.Attendee enumerates nine actor types:2 users, groups, guests, emails, circles, bridged, bots, federated_users, phones. An attendee row says this actor has a standing relationship with this room; it does not mean they were on the call. It is the correct anchor for "who can come back later and replay."Authorize off the attendee table or the room's policy. Never authorize off the session table. Sessions tell you who is in the building today; they cannot tell you who is allowed to come back tomorrow.
Before designing v2 it is worth being honest about v1, because v1 is what every reader is replacing or extending.
The recorder itself is not part of the PHP server. It's a separate HTTP daemon — nextcloud-talk-recording3 — that joins the call as a participant via the standalone signalling server (HPB).4 It captures the media stream out-of-process, encodes a file, and then POSTs that file back to spreed at /api/{v}/recording/{token}/store, owned by a chosen user. The file lands in that user's Nextcloud Files.5 A second endpoint, /api/{v}/recording/{token}/share-chat, publishes it into the conversation as a chat-attached file-share.5
Three consequences worth holding in mind as you design anything new:
Any future "room-scoped recordings" model has to either keep this pipeline and reframe what the file means, or replace the pipeline and re-implement the recorder. The interesting work is the reframing.
Most product debates about recording features go in circles because they are arguing about a UX outcome — "can Alice see the recording?" — when the underlying question is about which moment in time Alice's membership is being evaluated. There are two coherent answers. They behave differently when membership changes, and the choice is load-bearing.
Access is evaluated at view time. If the actor is in the room now, they can see all of the room's recordings.
Access is frozen at stop time. The set of authorized viewers is recorded with the recording.
The article you may be reading this to replace recommended current-room ACL as the v1 default. Don't do that. Current-room ACL is the right default for chat (which Talk already has) but it is the wrong default for recordings: recordings are the part of the conversation that outlives the conversation, and binding their visibility to today's membership silently leaks historical context every time a project room reorganises.
Default to snapshot ACL evaluated at recording stop, for authenticated room participants at that time. Offer current-room ACL as an explicit room-level opt-in, suitable for open collaboration rooms where shared history is the point. Treat "publish to room" as a separate, explicit act in either mode.
Snapshot is harder to implement — group and circle membership has to be expanded and persisted, federation has to be resolved into stable identifiers, deleted/disabled users need explicit cleanup — but the difficulty is doing the right thing, not doing the wrong thing.
Where the blob lives and who can read it are different decisions. The temptation is to let them collapse into each other: "we already have Files, just write a file and let Files permissions sort it out." That works for trivial cases and breaks for everything else. Decide them separately.
The decision affects more than tidiness. A non-exhaustive list of things storage choice changes:
A first draft of who-sees-what. These are defaults; specific deployments will want to relax or tighten them, but the defaults should be honest about what they imply.
| Actor or situation | View | Why this default |
|---|---|---|
| Logged-in attendee, present at recording stop | yes | Authorized at the moment of capture. Loses access on offboard/disable. |
| Logged-in attendee, added after recording | no | Snapshot default. Joining a room does not grant retroactive replay. |
| Logged-in attendee, removed after recording | no | Disable/offboard events re-resolve the snapshot. |
| Room moderator at view time | yes | Matches Talk's moderator-centric recording controls; can also manage. |
| Anonymous guest in a public room | no | Replay is a stronger capability than live-call join. |
| User who can discover a listable room | no | Discoverability is not membership. |
| Holder of the room password | no | A password grants entry, not archive access. Different capability. |
| Federated user from a peer instance | policy | Requires cross-instance identity resolution. Default no until the federation layer can prove identity at view time. |
| Instance administrator (operational UI) | no | Admins can manage retention, storage, legal hold, audit. Playback requires an explicit, audited capability. |
| Instance administrator (legal hold export) | yes | An audited, justified capability — not silent playback. |
| Recording bot / daemon (write) | yes | Distinct actor; the upload path is its own auth surface. |
"Recordings are durable room artifacts" is a good slogan. It is also a half-truth, because some rooms are not the kind of thing that holds artifacts.
Once the policy is decided, the implementation tries to do one thing: make every read of a recording resolve to the same access check. Not duplicated in five controllers. Not different between WebDAV, the chat surface, and the recordings list. Not subtly different in the background job that builds previews. One service. Every entrypoint.
Room id, room token, owner/recorder actor, start and stop timestamps, the file reference, media type, size, the ACL mode at capture, and the resolved snapshot of authorized actor identifiers. Separate from the file. Survives file moves.
Files-backed or app-private — pick deliberately. Do not let path visibility imply recording visibility.
Call it RecordingAccessPolicy or whatever you like. Modeled after Talk's own ParticipantService and Nextcloud's IShareManager: a single class with canRead(), canManage(), canPublish(), canDelete() methods. Controllers, jobs, WebDAV plugins, share-to-chat, transcripts, summaries, deletion, previews — all call into it.
A RoomAccessAdapter resolves room/participant facts on behalf of the policy. Hides the direct dependency on Talk internals so the policy survives spreed refactors and federation changes.
Its upload path is a distinct authentication surface (HPB-shared-secret today). Audit it separately. A compromised recorder is a compromised every recording.
Recordings appear in the room sidebar and in a per-user "recordings I can access" view. Hidden recordings are filtered server-side: the client never receives metadata for things the policy says it cannot see.
The compact version, for PR reviews and architecture discussions:
RoomAccessAdapter.One sentence: recordings are durable artifacts whose visibility is decided by a central recording-access policy that authorizes against a snapshot of room access at capture time, treats storage as an implementation detail, and refuses to let any client surface bypass the check. Everything else is plumbing.
spreed/lib/Room.php: types, object types, listable, recording state, federation, read-only, mention permissions.spreed/lib/Model/Attendee.php: users, groups, guests, emails, circles, bridged, bots, federated_users, phones. (Plus the system pseudo-actors cli, system, sample, changelog, which are not user-facing.)RecordingController.php exposes POST /api/{v}/recording/{token}/store for the daemon's upload and POST /api/{v}/recording/{token}/share-chat to publish into the chat. Storage flows through RecordingService.php into $rootFolder->getUserFolder($owner) — Files-backed by construction.ParticipantService centralizes room/participant permission checks; the Nextcloud platform's OCP\Share\IManager centralizes share-permission checks. A RecordingAccessPolicy is a domain-specific instance of the same idea — not a new architectural pattern.Cross-LLM fact-check on this piece: GPT-5.5, Gemini 3.1 Pro, and Kimi K2.6 each pushed back on the original draft's "current-room ACL by default" recommendation; all three flagged the missing actor types and the HPB/daemon trust boundary. Where one model offered claims I could not verify (a fourth GUEST access level on rooms, a Podman-version detail), I read the source directly rather than averaging votes.