No description
Find a file
John McLear 451bd9c3eb
feat(pad): scrub history in-place on the pad URL (#7659) (#7710)
* feat(pad): scrub history in-place on the pad URL (#7659)

Clicking the timeslider toolbar button now keeps the user on /p/:pad and
toggles a hash-based history mode (#rev/N) instead of navigating to a
separate /timeslider page. The pad shell — chat, users panel, settings,
plugin chrome — stays mounted across the transition. A sticky banner
plus a sepia tint on the toolbar make it unmistakable that what is
visible is historical, not live.

Implementation:

- New PadModeController (src/static/js/pad_mode.ts) owns enter/exit,
  the URL hash, browser back/forward, and a mutation-observer bridge
  from the inner timeslider's revision label/date into the outer
  banner. Esc and a Return-to-live button both exit history.
- pad.html grows a banner element and an iframe mount slot. The live
  ACE iframe stays mounted but hidden during history; on exit the
  socket is still alive, so the user snaps straight back to the
  current state without a reconnect.
- The /p/:pad/timeslider route 302-redirects to the pad page for
  direct visits (legacy bookmarks), and serves the timeslider HTML
  for the in-pad iframe when called with ?embed=1. The embedded
  variant hides the redundant title and return-to-pad button via
  CSS; the slider, settings, and export controls stay reachable.
- Legacy #NN shortlinks are preserved through the redirect by the
  browser and translated to #rev/NN client-side.

Tests:

- New backend spec asserts the 302 redirect, pad-name preservation,
  and the ?embed=1 path still serves the timeslider HTML.
- New padmode.spec.ts exercises toolbar entry, return-to-live,
  browser back, and direct /timeslider URL handling. Asserts the
  rendered localized banner string, not just element presence.
- Existing timeslider specs that hit /p/:pad/timeslider directly
  now pass ?embed=1 to bypass the redirect.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(pad): make history iframe fill the editor area (#7659)

Without an explicit positioning model the history-frame-mount inherited
half-width from a phantom flex parent and the embedded timeslider
rendered at 640×625 instead of the full editor area. Switch to the same
absolute-fill model the live ACE iframe uses by making
#editorcontainerbox the positioning anchor when in history mode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(pad): address Qodo review and CI failures (#7659)

Concrete review fixes for PR #7710:

- Tighten the embed query check from `if (!req.query.embed)` to
  `req.query.embed !== '1'` so values like `?embed=0` no longer bypass
  the redirect.
- Fix the `#rev/latest` mapping: the parser yields -1 for "latest",
  which the iframe sync handler was clamping to 0 and so jumping the
  embedded timeslider to revision 0. Resolve "latest" to the inner
  BroadcastSlider's upper bound instead.
- Update existing backend tests (`socialMeta`, `specialpages`) that
  hit `/p/:pad/timeslider` directly — they now pass `?embed=1` like
  the rest of the suite. Without this fix three pre-existing tests
  failed CI (302 instead of 200).
- Document the route change in `doc/skins.md` and `doc/skins.adoc`:
  direct visits redirect; iframe consumers use `?embed=1`.
- Back out a stray `data-theme="editorial"` attribute and the
  hardcoded Google Fonts `<link>` tags from `pad.html` that leaked
  into the branch from an unrelated working-tree change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(pad): consolidate chrome and replay chat/users in history mode (#7659)

Picks up the rough edges left by the initial in-place history mode:
the embedded timeslider iframe was rendering its own duplicate Settings
and Export buttons, and the chat panel + users list still showed live
state while the editor scrubbed back in time.

Chrome consolidation
- Hide the entire inner editbar's right-side toolbar and modal popups
  in embedded mode (slider stays). Outer pad shell now owns Settings,
  Export, Share, Users, Chat across both modes.
- Outer Settings popup grows a "History playback" section (visible
  only when scrubbing) with playback speed + follow-contents. Both
  bridge to the iframe's BroadcastSlider state.
- Outer Export anchors are rewritten to /p/<pad>/<rev>/export/<type>
  on each scrub and restored on exit, so Save As exports the visible
  historical revision.

Chat replay
- Each chat message is annotated with data-timestamp at render time.
  In history mode, messages newer than the scrubbed revision's
  timestamp are display:none'd; a "Chat as of HH:MM" header sits
  above the chat log.
- Restores cleanly on exit (inline display cleared, header removed).

Users replay
- Live users table is replaced with the embedded timeslider's
  authors-at-this-revision label while scrubbing; restored on exit.

Plumbing
- Expose padContents on window in broadcast.ts so the outer pad can
  read currentTime after each scrub without postMessage.
- Expose BroadcastSlider on window in timeslider.ts so the outer pad
  can register an onSlider callback to drive replay UI.

Tests
- New padmode specs cover: history-only Settings section, hidden
  embedded chrome, chat filter + replay header, Export href
  rewriting + restore, authors-row swap + restore.
- timeslider_line_numbers cookie-persistence test updated to bypass
  the now-hidden inner Settings popup (programmatic checkbox).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(pad): theme propagation, hide inert buttons, plugin loading (#7659)

Picks up rough edges from the in-place history mode that turned up in
real usage:

Theme / dark mode
- skin_variants.updateSkinVariantsClasses now also walks the history
  iframe (and its ace_outer/ace_inner) so toggling dark mode while
  scrubbing re-themes the embedded view in lockstep.
- timeslider.ts inherits the parent's skinVariant tokens (super-dark-*
  / dark-* / full-width-editor) on first paint when it detects it is
  embedded — same-origin guarantee, falls through silently if not.

Toolbar UX
- Hide #editbar .menu_left (Bold/Italic/Lists/Indent/Undo/...) and the
  show-more chevron while in history mode. Those buttons target the
  hidden live editor and would do nothing useful; rendering them
  disabled-looking implied state the user doesn't have. Right-side menu
  (Settings / Share / Users / Chat / Home) stays at full opacity and
  fully interactive.

Slider position
- Pin the embedded #editbar to the bottom of the iframe so the outer
  banner and the slider can't visually compete for the same band of
  pixels. Reserve padding-bottom on the iframe's editorcontainerbox so
  the editor never scrolls under the slider.

Plugin loading in timeslider
- timeSliderBootstrap.js now pre-loads plugin modules into a Map and
  passes them to plugins.update(), mirroring padBootstrap.js. Without
  this the loadFn fallback called require(path) at runtime, which the
  esbuild-bundled timeslider couldn't resolve, so client_hooks like
  ep_headings2's aceRegisterBlockElements silently failed to register
  and historical revisions rendered without plugin chrome.

Tests
- New padmode specs cover: outer toolbar's left/right asymmetry, slider
  pinned to bottom, dark-mode class propagation into the history iframe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(pad): move history slider into the outer toolbar (#7659)

The slider previously rendered inside the embedded iframe — first at the
top (where it visually competed with the banner), then briefly at the
bottom (where the chat icon overlapped it). Both were wrong. Move the
controls into the outer toolbar's left zone, where #editbar .menu_left
is hidden in history mode and the slider can occupy the full width
without colliding with anything.

- pad.html grows a #history-controls div (slider + play/pause/step
  buttons + timer) inside #editbar, between menu_left and menu_right.
  Hidden by default; revealed via body.history-mode CSS.
- pad.css swaps #editbar .menu_left out for #history-controls in
  history mode (display:none / display:flex).
- timeslider.css fully hides the embedded iframe's #editbar — the
  outer toolbar now owns the slider, and the iframe is purely the
  editor surface.
- pad_mode.ts wires the outer controls as a remote control: the
  range input calls inner BroadcastSlider.setSliderPosition, the play
  button calls BroadcastSlider.playpause, step buttons forward clicks
  to the inner #leftstep/#rightstep so they share the existing logic.
  An onSlider subscription mirrors inner state back into the outer
  slider value, timer label, and play-button .pause class.

Tests
- Existing timeslider.spec asserts the outer controls are visible.
- New padmode specs cover: inner editbar fully hidden, outer toolbar
  swap (menu_left → history-controls), and outer slider drives the
  iframe's revision via BroadcastSlider.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(pad): exempt embedded history iframe from userdup kick (#7659)

When the in-place history iframe opens its socket, the server's
duplicate-author kick treats it as a stale tab and disconnects the
parent pad's live socket — toolbar-overlay drops over the editor and
Settings/Share/Users/Chat all stop responding. Mark the iframe's
connection with `embed=1` in the socket.io handshake query, record it
on sessionInfo, and skip the kick whenever either side is embedded.

- timeslider.ts: detect `?embed=1` (and parent !== window) on
  the iframe URL, pass through as a query parameter to socketio.connect.
- PadMessageHandler: read socket.handshake.query.embed on CLIENT_READY,
  set sessionInfo.embed; the duplicate-author kick now skips when
  either the connecting session OR the existing session is embedded.

Behavior preserved
- Two real tabs (both non-embedded): older tab still gets kicked.
- Authenticated sessions still bypass the kick entirely.
- Live pad socket survives entry into history mode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(pad): a11y of history toolbar controls (#7659)

The new history controls (slider + play/step/timer) had hardcoded
English aria-labels, which html10n won't replace because they were
present without the data-l10n-aria-label marker. Screen readers in
non-English locales would have heard English. Drop the static aria
labels and let html10n.translateElement populate aria-label from the
data-l10n-id translation, matching how the rest of the toolbar works.

- pad.html: remove hardcoded aria-label on play/step buttons and the
  range input; keep titles (hover tooltip) and data-l10n-id. Add
  role="toolbar" + data-l10n-id on the controls container so the
  toolbar landmark is announced. Mark play button as a toggle with
  aria-pressed reflecting playback state.
- en.json: add pad.historyMode.controlsLabel and
  pad.historyMode.sliderLabel for the toolbar landmark and the slider.
- pad_mode.ts: keep aria-pressed in sync with the inner playback state
  on every revision update.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(pad): a11y + responsive for history controls (#7659)

Two issues with the previous a11y attempt: the data-l10n-id on icon
buttons was setting their textContent (drawing "Playback / Pause Pad
Contents" on screen next to the glyph), and there was no responsive
treatment so the timer + slider could overflow narrow viewports.

- pad.html: drop data-l10n-id from the icon buttons. They're now
  empty <button>s. Localized title (hover tooltip) and aria-label
  (screen reader name) are populated by pad_mode.localizeControls()
  using the existing timeslider.* keys, with an html10n.bind
  subscription so language switches re-localize.
- Mark #history-timer as hide-for-mobile.
- pad.css: dedicated @media (max-width: 800px) and 480px rules
  shrink padding, gap, and button widths so play + slider + step
  buttons stay on a single toolbar line at narrow viewports. Mirrors
  the legacy timeslider's responsive behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(pad): inline Follow + Playback speed, match toolbar height (#7659)

Two follow-ups from real testing:

- Move "Follow pad content updates" (now "Follow") and "Playback speed"
  out of the Settings popup and inline them in the history-mode
  toolbar, alongside the slider + play/step buttons. They were always
  needed while scrubbing; one extra click into Settings was friction.
  Removed the now-empty #history-settings-section.
- The history controls toolbar was visibly shorter than the live
  toolbar because the icon buttons sat as bare <button> elements
  without the live editbar's <li><a> wrapping. Add explicit
  min-height (40px) and per-button padding so the toolbar is the same
  vertical size in both modes — switching between live and history
  no longer reflows.
- Differentiate "iframe-mounted history view" from "direct ?embed=1
  visit". Only the former hides the inner timeslider editbar — direct
  visits keep their full chrome so existing test/legacy entry points
  stay independently usable. Marker: timeslider.ts adds an
  `iframe-mode` class on body when window.parent !== window; CSS
  scopes the hide to that combo.

Tests
- padmode spec asserts Follow + Speed live in the toolbar (not the
  Settings popup) and are visible in history mode, hidden in live.
- timeslider*.spec direct-?embed=1 flows continue to pass because the
  inner editbar is no longer hidden when not iframe-mounted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(pad): use absolute path for legacy /timeslider redirect (#7659)

CI Firefox failed the legacy-URL redirect test (1 of 32 jobs); Chromium
passed. The redirect Location header was a relative `../padname`, which
both browsers resolve to /p/padname for `/p/padname/timeslider`. Firefox
flaked on it once consistently. Switch to an absolute path including
the proxy prefix so the resolution is unambiguous across browsers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(test): accept 304 on legacy timeslider redirect (#7659)

CI Firefox failed `expect(res.status()).toBe(200)` because Firefox
issues a conditional GET when the redirect target is the same URL the
test just loaded via goToNewPad — the server returns 304 Not Modified
and the test treats that as a regression. Chromium happens to send
fresh requests so it stayed green.

Accept either 200 or 304 — both are valid completed navigations to the
pad page; what we actually care about is the pathname assertion above.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(pad): eye toggle for Follow, fix line-number alignment (#7659)

Two refinements from real testing in 9002:

Follow as an eye toggle
- Replace the labeled checkbox with an inline-SVG eye icon. The eye is
  always rendered; a diagonal slash is overlaid via SVG <line> only
  when the underlying (visually hidden) checkbox is unchecked. Default
  state is on (auto-following) so the eye renders unobstructed.
- Localized hover tooltip + aria-label flips with state — html10n
  populates "Following pad changes — click to stop following" vs
  "Not following pad changes — click to follow", and pad_mode.ts
  re-applies on every change event so screen readers narrate the
  action the click would take.
- Hidden checkbox keeps the existing pad_mode.ts bridge code working
  (still reads .checked) and lets <label for="…"> handle the click.

Line-number alignment fix (broadcast.ts)
- The first-line height formula was
    `nextDocLine.offsetTop - innerdocbody.padding-top`
  which only computes the right value when innerdocbody is the
  offsetParent. In the in-pad history iframe, outerdocbody contributes
  its own padding-top to the offsetTop chain, so the first gutter row
  was 20px too tall and every subsequent line drifted out of
  alignment. Use the consistent `next.offsetTop - current.offsetTop`
  formula for every iteration — same result in the standalone
  timeslider, correct result in the embedded one.
- New padmode spec asserts every gutter row's top matches the editor
  line's top within 2px, in iframe-mounted history mode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:21:56 +01:00
.github chore: fixed commit path 2026-05-08 22:40:59 +02:00
admin feat(updater): tier 2 — manual-click update from /admin/update (#7607) (#7704) 2026-05-10 09:00:07 +01:00
bin build(deps-dev): bump the dev-dependencies group across 1 directory with 6 updates (#7706) 2026-05-08 19:44:25 +02:00
doc feat(pad): scrub history in-place on the pad URL (#7659) (#7710) 2026-05-10 16:21:56 +01:00
docs/superpowers feat(api): clean up published OpenAPI spec for downstream codegens (#7714) 2026-05-10 13:02:05 +01:00
local_plugins Fix installation of local plugins 2025-04-05 15:31:36 +02:00
packaging ci(packaging): publish signed apt repository to etherpad.org/apt (closes #7610) (#7624) 2026-04-29 00:20:00 +01:00
snap feat(padOptions): pass plugin-namespaced ep_* keys through applyPadSettings (#7698) 2026-05-07 17:17:05 +01:00
src feat(pad): scrub history in-place on the pad URL (#7659) (#7710) 2026-05-10 16:21:56 +01:00
ui build(deps-dev): bump the dev-dependencies group across 1 directory with 6 updates (#7706) 2026-05-08 19:44:25 +02:00
var Use temporary directory for esbuild 2024-11-05 20:44:27 +01:00
.dockerignore Added new command to setup etherpad. Fixed Dockerfile 2024-07-23 17:43:32 +02:00
.editorconfig Added editorconfig configuration (#6347) 2024-04-23 07:04:30 +02:00
.env.default Fixed docker compose (#6337) 2024-04-17 20:50:21 +02:00
.env.dev.default Fixed docker compose (#6337) 2024-04-17 20:50:21 +02:00
.gitattributes tests: Microsoft Windows Server CI (#4791) 2021-02-18 18:49:43 +00:00
.gitignore feat(admin): document admin endpoints in OpenAPI (#7693) (#7705) 2026-05-10 15:55:33 +08:00
.npmrc fix: use hardlink package-import-method so the Docker build works on ZFS (#7342) (#7533) 2026-04-17 12:03:21 +01:00
.pr_agent.toml docs: add AGENTS.MD for AI and developer guidance (#7348) 2026-03-04 21:03:58 +00:00
AGENTS.MD chore(docker): clear most CVEs in published image (npm/pnpm/uuid + drop curl) (#7674) 2026-05-06 22:00:13 +02:00
best_practices.md chore: Rename some occurences of etherpad-lite to etherpad (#7552) 2026-04-19 16:53:57 +02:00
CHANGELOG.md feat(updater): tier 2 — manual-click update from /admin/update (#7607) (#7704) 2026-05-10 09:00:07 +01:00
CONTRIBUTING.md chore: Rename some occurences of etherpad-lite to etherpad (#7552) 2026-04-19 16:53:57 +02:00
docker-compose.dev.yml Update docker-compose.dev.yml (#6654) 2024-09-13 08:08:56 +02:00
docker-compose.yml security: run Etherpad container as non-root user (fixes #7134) (#7287) 2026-01-10 20:28:58 +01:00
Dockerfile fix(docker): share corepack cache so etherpad user can resolve pnpm (#7689) 2026-05-07 10:09:27 +01:00
LICENSE Update LICENSE 2013-06-26 23:34:35 +01:00
package.json bump version 2026-05-06 21:41:46 +00:00
pnpm-lock.yaml feat(admin): document admin endpoints in OpenAPI (#7693) (#7705) 2026-05-10 15:55:33 +08:00
pnpm-workspace.yaml chore(docker): clear most CVEs in published image (npm/pnpm/uuid + drop curl) (#7674) 2026-05-06 22:00:13 +02:00
README.md ci(docs): build on PRs and pin Node 22 (Qodo follow-up to #7640) (#7645) 2026-05-01 17:12:23 +01:00
SECURITY.md Create SECURITY.md 2020-07-07 10:36:17 +01:00
settings.json.docker feat(updater): tier 2 — manual-click update from /admin/update (#7607) (#7704) 2026-05-10 09:00:07 +01:00
settings.json.template feat(updater): tier 2 — manual-click update from /admin/update (#7607) (#7704) 2026-05-10 09:00:07 +01:00
tests restructure: move bin/ and tests/ to src/ 2021-02-04 17:15:08 -05:00

Etherpad — the editor for documents that matter

Real-time collaborative editing where authorship is the default, your server is the only server, and you decide what AI (if any) ever touches your text.

Demo Etherpad Animated Jif

About

Etherpad is a real-time collaborative editor for documents that matter.

Every keystroke is attributed to its author. Every revision is preserved. The timeslider lets you scrub through a document's entire history, character by character. Author colours make collaboration visible at a glance — not buried in a menu.

Etherpad runs on your server, under your governance. No telemetry. No upsells. AI is a plugin you install, pointed at the model you choose, running on infrastructure you control — not a feature decided for you in a boardroom you weren't in.

The code is Apache 2.0. The data format is open. It scales to thousands of simultaneous editors per pad. Translated into 105 languages. Extended through hundreds of plugins. Used by Wikimedia, governments, public-sector institutions, and self-hosters worldwide since 2009.

Full data export is built in. The history is yours.

Try it out

Try out a public Etherpad instance

Project Status

Etherpad has been doing the same thing — well — since 2009. No pivots, no acquisitions, no enshittification. Maintained by a small volunteer team.

We are actively looking for maintainers. If you have experience with Node.js, real-time systems, or institutional collaboration tooling and you want to work on infrastructure that thousands of organisations quietly depend on, please open an issue or contact John McLear.

Code Quality

Code Quality

Testing

Backend tests Simulated Load Rate Limit Docker file Frontend admin tests powered by Sauce Labs Frontend tests powered by Sauce Labs Sauce Test Status Windows Build

Engagement

Docker Pulls Discord Etherpad plugins Languages Translation Coverage

Who uses Etherpad

For more than a decade, Etherpad has quietly underpinned the documents that matter to:

  • Wikimedia Foundation — collaborative drafting across editor communities.
  • Public-sector institutions across the EU — including organisations that legally cannot use US-cloud SaaS for sovereignty and GDPR reasons.
  • Universities and schools worldwide — including jurisdictions where Google Workspace is no longer permitted in education.
  • Civic-tech and democratic-deliberation projects — citizen assemblies, participatory budgeting, public consultations.
  • Newsrooms and investigative journalism teams — where authorship and editing history matter for legal and editorial integrity.
  • Tens of thousands of self-hosted instances worldwide, run by IT teams who chose Etherpad because it is theirs.

If your organisation runs Etherpad and would be willing to be listed publicly, please add it to the wiki.

Installation

Quick install (one-liner)

The fastest way to get Etherpad running. Requires git and Node.js >= 22.

macOS / Linux / WSL:

curl -fsSL https://raw.githubusercontent.com/ether/etherpad/master/bin/installer.sh | sh

Windows (PowerShell):

irm https://raw.githubusercontent.com/ether/etherpad/master/bin/installer.ps1 | iex

Both installers clone Etherpad into ./etherpad-lite, install dependencies, and build the frontend. When the installer finishes, run:

cd etherpad-lite && pnpm run prod

Then open http://localhost:9001.

To install and start in one go:

# macOS / Linux / WSL
ETHERPAD_RUN=1 sh -c "$(curl -fsSL https://raw.githubusercontent.com/ether/etherpad/master/bin/installer.sh)"
# Windows
$env:ETHERPAD_RUN=1; irm https://raw.githubusercontent.com/ether/etherpad/master/bin/installer.ps1 | iex

Docker-Compose

The official image is published to both Docker Hub (etherpad/etherpad) and GitHub Container Registry (ghcr.io/ether/etherpad) with identical tags. Use whichever suits your environment; GHCR avoids Docker Hub's anonymous pull rate limits.

services:
  app:
    user: "0:0"
    image: etherpad/etherpad:latest  # or: ghcr.io/ether/etherpad:latest
    tty: true
    stdin_open: true
    volumes:
      - plugins:/opt/etherpad-lite/src/plugin_packages
      - etherpad-var:/opt/etherpad-lite/var
    depends_on:
      - postgres
    environment:
      NODE_ENV: production
      ADMIN_PASSWORD: ${DOCKER_COMPOSE_APP_ADMIN_PASSWORD:-admin}
      DB_CHARSET: ${DOCKER_COMPOSE_APP_DB_CHARSET:-utf8mb4}
      DB_HOST: postgres
      DB_NAME: ${DOCKER_COMPOSE_POSTGRES_DATABASE:-etherpad}
      DB_PASS: ${DOCKER_COMPOSE_POSTGRES_PASSWORD:-admin}
      DB_PORT: ${DOCKER_COMPOSE_POSTGRES_PORT:-5432}
      DB_TYPE: "postgres"
      DB_USER: ${DOCKER_COMPOSE_POSTGRES_USER:-admin}
      # For now, the env var DEFAULT_PAD_TEXT cannot be unset or empty; it seems to be mandatory in the latest version of etherpad
      DEFAULT_PAD_TEXT: ${DOCKER_COMPOSE_APP_DEFAULT_PAD_TEXT:- }
      DISABLE_IP_LOGGING: ${DOCKER_COMPOSE_APP_DISABLE_IP_LOGGING:-false}
      SOFFICE: ${DOCKER_COMPOSE_APP_SOFFICE:-null}
      TRUST_PROXY: ${DOCKER_COMPOSE_APP_TRUST_PROXY:-true}
    restart: always
    ports:
      - "${DOCKER_COMPOSE_APP_PORT_PUBLISHED:-9001}:${DOCKER_COMPOSE_APP_PORT_TARGET:-9001}"

  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: ${DOCKER_COMPOSE_POSTGRES_DATABASE:-etherpad}
      POSTGRES_PASSWORD: ${DOCKER_COMPOSE_POSTGRES_PASSWORD:-admin}
      POSTGRES_PORT: ${DOCKER_COMPOSE_POSTGRES_PORT:-5432}
      POSTGRES_USER: ${DOCKER_COMPOSE_POSTGRES_USER:-admin}
      PGDATA: /var/lib/postgresql/data/pgdata
    restart: always
    # Exposing the port is not needed unless you want to access this database instance from the host.
    # Be careful when other postgres docker container are running on the same port
    # ports:
    #   - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:
  plugins:
  etherpad-var:

Requirements

Node.js >= 22.12.

Windows, macOS, Linux

  1. Download the latest Node.js runtime from nodejs.org.
  2. Install pnpm: npm install -g pnpm (Administrator privileges may be required).
  3. Clone the repository: git clone -b master
  4. Run pnpm i
  5. Run pnpm run build:etherpad
  6. Run pnpm run prod
  7. Visit http://localhost:9001 in your browser.

Docker container

Find here information on running Etherpad in a container.

Plugins

Etherpad is very customizable through plugins.

Basic install

Full Features

Available Plugins

For a list of available plugins, see the plugins site.

Plugin Installation

You can install plugins from the admin web interface (e.g., http://127.0.0.1:9001/admin/plugins).

Alternatively, you can install plugins from the command line:

cd /path/to/etherpad-lite
pnpm run plugins i ep_${plugin_name}

Also see the plugin wiki article.

Suggested Plugins

Run the following command in your Etherpad folder to get all of the features visible in the above demo gif:

pnpm run plugins i \
  ep_align \
  ep_comments_page \
  ep_embedded_hyperlinks2 \
  ep_font_color \
  ep_headings2 \
  ep_markdown \
  ep_webrtc

For user authentication, you are encouraged to run an OpenID Connect identity provider (OP) and install the following plugins:

  • ep_openid_connect to authenticate against your OP.
  • ep_guest to create a "guest" account that has limited access (e.g., read-only access).
  • ep_user_displayname to automatically populate each user's displayed name from your OP.
  • ep_stable_authorid so that each user's chosen color, display name, comment ownership, etc. is strongly linked to their account.

Upgrade Etherpad

Run the following command in your Etherpad folder to upgrade

  1. Stop any running Etherpad (manual, systemd ...)
  2. Get present version
git -P tag --contains
  1. List versions available
git -P tag --list "v*" --merged
  1. Select the version
git checkout v2.2.5
git switch -c v2.2.5
  1. Upgrade Etherpad
./bin/run.sh
  1. Stop with [CTRL-C]
  2. Restart your Etherpad service

Next Steps

Tweak the settings

You can modify the settings in settings.json. If you need to handle multiple settings files, you can pass the path to a settings file to bin/run.sh using the -s|--settings option: this allows you to run multiple Etherpad instances from the same installation. Similarly, --credentials can be used to give a settings override file, --apikey to give a different APIKEY.txt file and --sessionkey to give a non-default SESSIONKEY.txt. Each configuration parameter can also be set via an environment variable, using the syntax "${ENV_VAR}" or "${ENV_VAR:default_value}". For details, refer to settings.json.template. Once you have access to your /admin section, settings can be modified through the web browser.

If you are planning to use Etherpad in a production environment, you should use a dedicated database such as mysql, since the dirtyDB database driver is only for testing and/or development purposes.

Secure your installation

If you have enabled authentication in users section in settings.json, it is a good security practice to store hashes instead of plain text passwords in that file. This is especially advised if you are running a production installation.

Please install ep_hash_auth plugin and configure it. If you prefer, ep_hash_auth also gives you the option of storing the users in a custom directory in the file system, without having to edit settings.json and restart Etherpad each time.

Customize the style with skin variants

Open http://127.0.0.1:9001/p/test#skinvariantsbuilder in your browser and start playing!

Skin Variant

Helpful resources

The wiki is your one-stop resource for Tutorials and How-to's.

Documentation can be found in doc/.

Development

Things you should know

You can debug Etherpad using bin/debugRun.sh.

You can run Etherpad quickly launching bin/fastRun.sh. It's convenient for developers and advanced users. Be aware that it will skip the dependencies update, so remember to run bin/installDeps.sh after installing a new dependency or upgrading version.

If you want to find out how Etherpad's Easysync works (the library that makes it really realtime), start with this PDF (complex, but worth reading).

Contributing

Read our Developer Guidelines

HTTP API

Etherpad is designed to be easily embeddable and provides a HTTP API that allows your web application to manage pads, users and groups. It is recommended to use the available client implementations in order to interact with this API.

OpenAPI (previously swagger) definitions for the API are exposed under /api/openapi.json.

jQuery plugin

There is a jQuery plugin that helps you to embed Pads into your website.

Plugin Framework

Etherpad offers a plugin framework, allowing you to easily add your own features. By default your Etherpad is extremely light-weight and it's up to you to customize your experience. Once you have Etherpad installed you should visit the plugin page and take control.

Translations / Localizations (i18n / l10n)

Etherpad comes with translations into all languages thanks to the team at TranslateWiki.

If you require translations in plugins please send pull request to each plugin individually.

FAQ

Visit the FAQ.

Get in touch

The official channel for contacting the development team is via the GitHub issues.

For responsible disclosure of vulnerabilities, please write a mail to the maintainers (a.mux@inwind.it and contact@etherpad.org).

Join the official Etherpad Discord Channel.

License

Apache License v2