SPDX-License-Identifier: AGPL-3.0-or-later
File format: etherpad-nextcloud/1
The .pad file consists of:
- YAML frontmatter (metadata)
- Snapshot body with text and optional HTML
Required fields:
formatfile_idpad_idaccess_mode(public|protected)state(active; legacy files may still containtrashedorpurged)created_at(ISO8601)updated_at(ISO8601)snapshot_rev(int,-1before first successful sync)
Additional fields:
deleted_at(nullor ISO8601)pad_url(optional, absolutehttp(s)URL)pad_origin(optional, origin of external Etherpad server, e.g.https://pad.example.org)remote_pad_id(optional, actual pad ID on external server)
Example:
---
format: "etherpad-nextcloud/1"
file_id: 994
pad_id: "g.TmDeyA334sIq2LQh$p-4k9x2m7q8r1t5v6n3d0c"
access_mode: "protected"
state: "active"
deleted_at: null
created_at: "2026-03-05T00:40:36+00:00"
updated_at: "2026-03-05T11:10:21+00:00"
snapshot_rev: 42
---Legacy migration:
- Old Ownpad format
[InternetShortcut]URL=https://.../p/<pad-id>- is auto-migrated on first open to
etherpad-nextcloud/1. The migration branches on the URL origin (same vs. cross) and the pad-id format; GroupPad IDs (g.<group>$<name>) re-bind as protected, free-form IDs re-bind as public, cross-origin URLs route through the external-pad flow asext.*. - A claim-collision check protects against legacy files being used to claim pads already bound to another user's file — see
docs/legacy-ownpad-migration.mdfor the full state table.
Body layout:
[TEXT]
<plain text snapshot>
[HTML-BEGIN]
<html snapshot>
[HTML-END]
Notes:
- Text is the primary restore snapshot.
- HTML is an additional structure/format snapshot.
- External pads (
pad_origin+remote_pad_id) are imported and synced as text only for security reasons; HTML sections are omitted when the app writes external snapshots. - The parser expects exact
[HTML-BEGIN] ... [HTML-END]markers for the HTML part. - Viewer/API responses never expose stored HTML directly.
SnapshotExtractorrunsSnapshotHtmlSanitizer, which allowlists simple formatting tags only and drops every attribute before HTML reaches the frontend.
- Internal + Protected
access_mode: protectedpad_id: GroupPad (g.<group>$<name>)pad_url: internal Etherpad URL
- Internal + Public
access_mode: publicpad_id: public pad ID (for examplenc-...)pad_url: internal Etherpad URL
- External + Public
access_mode: publicpad_id: external marker (ext.<remote_pad_id>)pad_origin+remote_pad_idare setpad_url: external URL used for viewer open- no row in
ep_pad_bindings; the.padfrontmatter is the source of truth for the remote target
Protected + external is not supported.
active- normal editing state
trashed/purged- legacy parser compatibility only; new writes do not use these states
The DB binding table uses active and pending_delete. Successful trash deletes
the binding row; restore can recreate it from the .pad frontmatter and snapshot.
External pads are not managed in the DB binding table, so trash/restore only moves
the Nextcloud file and never creates, deletes, or restores anything on the remote
Etherpad server.
Implementation: lib/Service/PadFileService.php
parsePadFile(string $content): array{frontmatter, body}serialize(array $frontmatter, string $body): stringwithExportSnapshot(...)updates export metadata + snapshot bodywithStateAndSnapshot(...)updates state + snapshotgetTextSnapshotForRestore(...)andgetHtmlSnapshotForRestore(...)read stored snapshots from the body
Snapshot write flow:
PadFileService::withExportSnapshot(...)builds the new.padcontent after an Etherpad export.PadFileLockRetryService::putContentWithSyncLockRetry(...)writes that content back to the Nextcloud file with bounded lock retry.SnapshotExtractoris read-only: it extracts text + sanitized HTML for viewer responses and does not mutate.padfiles.- External public pad create/sync paths both use the validated, host-pinned
/export/txtfetch internally (viaExternalPadExportFetcher) and store no HTML snapshot:- create uses
ExternalPadExportFetcher::normalizeAndFetchExternalPublicPadTextOrEmpty(...), allowing the.padfile to be created with an empty initial snapshot if the export is not available yet. - sync uses
ExternalPadExportFetcher::normalizeAndFetchExternalPublicPadText(...), keeping later export failures visible.
- create uses
- Sync writes only when the upstream snapshot actually differs.
force=1requests an immediate upstream re-check, but unchanged snapshots are still not rewritten.- On successful sync:
snapshot_revis updated- body is replaced:
- internal pads: current text + HTML
- external pads: current text, no HTML