A quiet workspace for building on each other’s ideas.
brainwritR brings a classroom adaptation of Rohrbach’s 6-3-5 brainwriting method onto participants’ smartphones. A facilitator sets up topics, invites the room with a QR code, and runs timed rounds. Participants read earlier contributions on their assigned sheet, answer the configured questions, and move to the next topic together. The app offers German, English and French; documentation is English.
The app keeps drafts and the round clock in SQLite, resumes participants after a mobile reconnect, and avoids replacing textareas during routine polling. One instance runs any number of sessions side by side, each with its own address and QR code, managed from a moderator overview. No accounts, webfonts, or external application services are required.
Use the DE / EN / FR language slider to change the interface without replacing answer fields or translating contributed text. The button next to it switches between a light and a dark theme; the choice is remembered in that browser, and without one the device setting decides. Charts and QR codes stay light for legibility and scanning. German is the default. The text-analysis method and export schemas retain their documented German defaults; the in-app chart labels follow the selected language.
Requires R 4.4 or newer. This is a development release, not a CRAN release.
# install.packages("remotes")
remotes::install_github("CTTIR/brainwritR")
brainwritR::run_app()For a local checkout:
Rscript -e 'install.packages("remotes", repos="https://cloud.r-project.org")'
Rscript -e 'remotes::install_deps(dependencies = TRUE)'
R CMD INSTALL .
Rscript -e 'brainwritR::run_app()'Open http://localhost:3838 as a participant and
http://localhost:3838/?mod=1 as moderator. The local default PIN is
635. On first launch the app creates data/brainwriting.sqlite
relative to the working directory. Nothing is created when the package
is merely loaded.
For a classroom, set the public URL and a private moderator PIN. A
phone’s localhost refers to the phone itself; the QR code must point
to a reachable server. Use HTTPS on a public deployment.
brainwritR::run_app(
base_url = "https://brainwriting.example.de",
mod_pin = Sys.getenv("MY_CLASSROOM_PIN"),
db_path = "/srv/brainwriting/data/session.sqlite"
)- Set up: open the moderator route, enter the PIN, and enter a title and choose Anzahl der Fragen for each topic (at least one; two by default). Enter every question before saving. Choose 2–6 groups, 1–12 rounds, and 30–1800 seconds per round. Enter the planned number of participants: below the fields the format is described live, for example “Format 15-2-5: 3 topics, 3 rounds, 300 s per round. 5 people per group.” (participants, questions, minutes). Names are optional; participants can enter a name or pseudonym when joining.
- Invite: open the lobby. Participants scan the QR code and enter a name or pseudonym. Watch the live roster, then start once at least K people have joined.
- Write and rotate: groups are balanced and randomized. Participants read previous rounds on their sheet and answer the configured questions. Drafts save after 1.2 seconds of quiet; Abgeben explicitly submits all configured answers. Submitted answers remain editable until the round ends. The moderator can add 60 seconds or end a round early; repeated clicks or a second moderator tab end only the round that is on screen, never the next one.
- Weigh (optional): in Analyse & Plenum, start the weighting. Everyone distributes 100 % per topic across the anonymous contributions; the moderator ends it and discusses the ranked overview with the room.
- Debrief: the moderator sees results by topic and sheet, downloads CSV, Markdown, RDS, XLSX or the PDF report, and can reset the session (Zurücksetzen …) or archive it and start the next one from the session overview.
After the writing phase, the finished moderator view has an Analyse & Plenum tab. Gewichtung starten switches every participant’s device to the weighting: all nonempty contributions of each topic appear without names and in a personal random order. Each person has 100 % per topic and distributes it with sliders in steps of 5 %; a slider cannot pass the remaining budget, and every change saves at once. Each slider is named by its question and contribution for screen readers and announces the topic’s remaining budget. If the same person has the page open twice, or a group device is taken over, every open page follows the stored weights within a few seconds. Everyone may weight all contributions, including their own. In group-device mode each group weights as one author. On a shared hot-seat device the weighting passes from person to person (Weitergeben an: …, Los geht’s, Fertig — weitergeben); the moderator can skip an absent person.
While the weighting runs, the moderator sees per topic how many people have used their full budget; results stay hidden so they cannot steer the vote. Gewichtung beenden shows the collected overview: a chart of the highest-weighted contributions per topic and the full ranking with share of all points given in the topic, mean per person and number of supporters. Wieder öffnen continues with the saved weights. The weighting is a snapshot of the group’s opinion, not a measure of idea quality.
The weights are stored with the voter’s participant ID so that each
person keeps one budget; the app never shows who gave which weight.
Gewichtung (CSV) exports the ranking, and the XLSX workbook (sheet
Gewichtung), the Markdown protocol and a third PDF page include it;
all of these are aggregated per contribution. Only the RDS snapshot
keeps the individual weights (table votes), linked to participant IDs
and therefore to names unless Namen pseudonymisieren is ticked,
which replaces the voter IDs with the same codes as the authors.
One instance holds any number of sessions, and several can run at the
same time, for example two courses in parallel. The bare address always
shows the standard session, exactly as before. Every further session
has its own short code, address and QR code, such as
https://brainwriting.example.de/?s=k7m3pq.
Open All sessions (German: Alle Sessions) from the moderator
bar, or go to /?mod=1&view=sessions. The overview lists every session
with its state, mode, participant and contribution counts, topics and
address, and refreshes as they change.
| Action | Effect |
|---|---|
| New session | Creates an empty session with an optional label and opens its setup |
| Open | Opens the session’s moderator view |
| QR code | Shows a large QR code with the address; download it as PNG for slides or handouts |
| Restart | Creates a new session prefilled with the same mode, topics, timing and names, and opens its setup for review; the original session and its results stay unchanged |
| Archive / Restore | Finished sessions only. Archived sessions leave the active list, refuse participants and stay readable with all exports; restoring returns them to the list |
| Delete | Permanently removes the session and its database file after confirmation; export first |
The standard session keeps the bare address, so it is never deleted or archived in place. Reset clears it after confirmation. Archive saves its finished results as a new archived session and then clears it for the next activity.
A moderator enters the PIN once per browser tab. The server then issues a login token that lives only in server memory and in the tab’s session storage, so moving between sessions and reconnecting do not ask for the PIN again. Abmelden (sign out) ends it; a server restart signs everyone out. After five wrong PINs, new logins pause for 15 seconds, doubling up to five minutes; signed-in moderators continue. A login lasts 12 hours: an open moderator tab is signed out within a minute once it expires. Because the PIN now also protects deletion, use a long private PIN.
Participants keep a separate stored identity for each session, so one device can join several sessions. Unknown codes show Session nicht gefunden; open pages of a deleted session switch to that notice within a few seconds.
Storage. DB_PATH holds the standard session and the session
catalog. Every further session is a separate SQLite file with the
unchanged schema in a sessions/ folder next to it. Back up the whole
folder. Existing databases migrate automatically when the app starts,
including every catalogued session file; their session becomes the
standard session. Back up before upgrading.
Actual browser captures with English selected and English example content.
| Prepare the activity | Individual device |
|---|---|
![]() |
![]() |
| Shared group device | Hot-seat handover |
|---|---|
![]() |
![]() |
| Session overview | Plenum weighting |
|---|---|
![]() |
![]() |
| Device situation | Mode | Author and timing |
|---|---|---|
| Everyone has a phone or laptop | individual (default) |
One person per author; parallel rounds |
| One device at each group table | group_device |
One group per author; one sheet per topic; parallel rounds |
| One shared computer for everyone | hot_seat |
People take sequential timed turns within passes |
Choose the mode during setup; it is frozen once setup is saved. All modes use the same topic and sheet rotation formulas and preserve submitted flags through later edits. Existing deployments need no settings file or new arguments.
In group-device mode, devices claim a named group. Reclaiming an existing group asks for confirmation and hands over its existing identity, preserving all earlier attribution. A stale device can still edit: concurrent writes use last-write-wins. Close the old tab after taking over. Start normally requires all K groups; the moderator may choose Ohne alle Gruppen starten. Unclaimed groups can join later. Each topic has exactly one sheet.
In hot-seat mode, enter a roster (one name per line). Setup starts directly at Weitergeben an: …, without a QR-code lobby. Los geht’s starts that person’s timer; Fertig — weitergeben saves all configured answers and advances. Überspringen skips an absent person without creating entries. People are assigned virtual groups using the same balanced randomization as individual mode; turns follow roster arrival order. Each pass visits everyone once.
Keep a PIN-authenticated /?mod=1 tab open on the same machine for
+30 s, skipping a turn, ending the rest of a pass, adding
latecomers, or finishing early while retaining results. Latecomers
append to the remaining passes. This mode uses the current turn as
identity and does not require browser identity storage. The duration
estimate excludes handovers and pauses: passes × people × turn_secs.
With fewer people than topics, empty starting groups receive one empty sheet. This deliberate extension makes the requested one-person hot-seat session usable; all other starting sheet counts remain frozen. The last-1–2-seconds autosave boundary applies to hot-seat turns as well as parallel rounds.
For a quick demonstration, select Use example questions (German: Beispiel-Fragenset verwenden). Three classroom topics appear in the selected interface language. They follow the 6-3-5 principle within the two answer fields: each topic is one open “How might we …?” problem, the first field asks for up to three new ideas, and the second asks participants to take up an idea already on the sheet (or one of their own) and develop it further. Edit them freely; clearing the checkbox restores your previous questions and topic count. Mode, roster and timers stay as configured. Loading a YAML file replaces the example selection. Changing interface language never translates existing prompts.
The setup form and Einstellungen laden (YAML) prepare the same configuration. Loading a file validates it and prefills the form; it never starts a session. Review the fields before saving. Einstellungen exportieren (YAML) is available in setup and the lobby. Save it before a hot-seat session starts if you need a standalone reusable file; the finished RDS snapshot also contains the settings.
# Portable session preparation. The moderator PIN is never included.
# Topics follow 6-3-5: one open problem per sheet. Each round adds new ideas
# and develops an idea that is already on the sheet.
format: brainwriting635-settings/1
mode: individual
rounds: 3
round_secs: 300
turn_secs: 90
topics:
- title: "Lernen im Kurs"
q1: "Wie könnten wir das gemeinsame Lernen im Kurs stärken? Notiere bis zu drei neue Ideen."
q2: "Greife eine Idee von oben auf (oder eine eigene) und entwickle sie weiter."
- title: "Wissen teilen"
q1: "Wie könnten wir unser Wissen untereinander leichter teilen? Notiere bis zu drei neue Ideen."
q2: "Greife eine Idee von oben auf (oder eine eigene) und entwickle sie weiter."
- title: "Transfer in den Alltag"
q1: "Wie könnten wir die Ideen aus dem Kurs im Alltag anwenden? Notiere bis zu drei neue Ideen."
q2: "Greife eine Idee von oben auf (oder eine eigene) und entwickle sie weiter."
groups:
- "Gruppe Nord"
- "Gruppe Süd"
- "Gruppe West"
participants:
- "Alice"
- "Bob"For a variable question count, a topic can use a questions sequence:
title: Working together
questions:
- What helps our team?
- Which resources do we need?
- How will we measure progress?Legacy topics with q1 and q2 remain supported. Counts can differ
between topics; answer fields, autosave, voting and exports follow the
configured questions. Results and plenum rankings show the original
question above each answer. The analysis tab also lists the questions
used. Existing session databases migrate automatically at startup.
The compatibility identifier remains exactly
brainwriting635-settings/1. There must be 2–6 topics, each with a
nonempty title and at least one nonempty question. Rounds are integers
1–12; parallel-round duration is 30–1800 seconds and turn duration
20–600 seconds (default 90). Optional group names must be unique and
match the number of topics. Roster names must be nonempty and unique
after trimming. Hot-seat start requires at least one name.
Individual-mode roster names appear as one-tap join buttons; taken names
are disabled, and the free-text join field remains available.
The upload limit is 100 KiB. YAML expressions are never evaluated. All validation errors appear together in the selected interface language; unknown keys produce warnings and are ignored. The PIN is never read from or exported to settings. Keep it in runtime configuration.
Database initialization migrates older sessions in place. It adds mode
(default individual), current_turn (default 0), settings_yaml, and
for the plenum plenum (default none) and plenum_turn (default 0),
and creates the votes table, without changing the other tables or
removing data. run_app() applies this to the main database and to
every catalogued session file at startup. The additional settings column
is a deliberate extension to keep custom names, the prepared roster, and
turn duration in SQLite across restarts. No sidecar file is required.
Back up before upgrading; an older package version should not be used to
operate a newer-mode database.
The moderator’s finished screen has Beiträge, Auswertung and Analyse & Plenum tabs. Four indicators summarize nonempty saved answers: contribution count, average raw word count, submitted share, and distinct filtered terms. Drafts count as contributions; Abgabequote is submitted nonempty entries divided by all nonempty entries, not a measure of attendance or the proportion of possible answers completed.
The figures show contributions by topic/round, the top eight terms per topic, a term network, a wordcloud, and adjacent-round lexical overlap. Network and wordcloud share a topic selector. The network defaults to at least two entries containing a pair and at most 40 frequent terms; the cloud uses at most 60 terms. Layouts use fixed seeds. The same plotting functions create app and report figures. Empty or insufficient input produces a labelled placeholder instead of an error. On phones the app switches to single-column term charts, a smaller network, horizontal continuity bars and a rank-numbered weighting chart whose height follows the bars shown. Contributions consisting only of whitespace (including tabs, line breaks, no-break or ideographic spaces and zero-width characters) never count as contributions.
Text processing is deliberately simple: Unicode letter boundaries, German lowercasing, at least three letters, German Snowball stopwords, no stemming and no embeddings. Terms from each topic’s title and questions are excluded by default; the app provides a toggle. Term counts retain repeated occurrences. Network edges count entries containing both terms, once per entry.
Anknüpfungsgrad is the mean Jaccard overlap of token sets from consecutive rounds on each sheet/question, pooling authors who share that sheet. Missing or empty rounds contribute NA, not zero; available comparisons are averaged by topic. This is lexical continuity, not evidence of idea quality or conceptual elaboration. Discuss the original contributions alongside the charts.
On macOS, the CRAN R build may require XQuartz for Cairo graphics. Install it before using PDF reports; see the R Cairo device documentation.
The PDF has two A4 portrait pages, including for an empty session: overview/parameters/KPIs/contributions/top terms, then networks/wordcloud/topic summary. A third page with the plenum weighting follows when weights exist. Up to three topics receive separate networks; larger sessions receive one combined network. Cairo and patchwork compose the report directly: the runtime container needs no LaTeX, Pandoc, or R Markdown report toolchain.
| Format | Use | Contents |
|---|---|---|
| CSV | Spreadsheet or statistical import | Existing UTF-8 long contribution table |
| Markdown | Readable classroom protocol | Questions and nonempty answers by topic/sheet |
| RDS | Lossless re-analysis in R | Raw tables (plus votes), settings, generation time, package version |
| XLSX | Workbook for review | Beiträge, Teilnehmer, Themen, Kennzahlen (plus Gewichtung); styled headers |
| Shareable debrief | Session overview and the shared figures; plenum page when weighted | |
| Gewichtung (CSV) | Plenum ranking | Topic, rank, contribution, points, share, mean, supporters |
Namen pseudonymisieren (TN-01, TN-02, …) applies stable
arrival-order codes to all five downloads; groups receive Gruppe-01,
etc. RDS/XLSX persistent author IDs and stored roster settings are
replaced consistently. The result view updates immediately when the
checkbox changes; the same codes are used in downloads. This is
author-field pseudonymization: identifying information typed inside
answers, topic titles or questions is not detected or redacted. Review
free text before sharing. The PDF contains aggregate results, not an
author list.
K groups work on K topics in parallel. Group g in round r receives
topic ((g - 1 + r - 1) %% K) + 1:
Round 1 Round 2 Round 3
Group 1 Topic 1 Topic 2 Topic 3
Group 2 Topic 2 Topic 3 Topic 1
Group 3 Topic 3 Topic 1 Topic 2
brainwritR::topic_for(1:3, round = 2, n_groups = 3)
#> [1] 2 3 1
brainwritR::sheet_for(1:6, n_sheets = 5)
#> [1] 1 2 3 4 5 1A topic starts with one sheet per member of its starting group. That
count is frozen at session start. A participant with index i
receives sheet ((i - 1) %% n_sheets) + 1. Six people visiting five
sheets share the first sheet; their separate contributions are both
retained. Five people visiting six sheets leave the sixth sheet
untouched that round. Thus every group visits every topic over K rounds,
but unequal groups do not guarantee a contribution to every sheet in
every round. Completion of answers is voluntary.
Latecomers join the smallest group with a new within-group index. They use the same modulo mapping without changing sheet counts. The full K-topic guarantee applies to participants present for all K rounds, not to someone joining halfway through. With fewer than K rounds, some topics are not visited; with more than K, the cycle repeats. This classroom adaptation is not the literal six-person, three-ideas-per-round protocol; the example questions keep its core, new ideas plus building on the sheet, within two fields.
- SQLite is authoritative. Each operation opens and closes a connection; WAL and a 5000 ms busy timeout are enabled. Restarting the process retains topics, participants, assignments, drafts, submissions, and the clock.
- Any connected client drives the clock. Every poll tries the guarded round transition. A moderator disconnection does not stop an active room. With no clients connected, advancement resumes on the next connection; missed rounds are not silently skipped.
- Reconnect resumes identity. The client reloads 1.5 seconds after a
Shiny disconnect and resends the participant ID it stored in
localStoragefor that session. Reset IDs are cleared. Keep the same browser and origin to resume. With storage disabled, automatic resume is unavailable; use one tab per participant. - Typing stays in place. Two database polls feed live subregions. Main views change only on status, round, or assignment transitions. Debounced edits carry the context in which they were typed; later autosaves never clear submission.
- Usable on phones. System fonts, Bootstrap 5, a 720 px content limit, labelled inputs, touch-sized buttons, zoom support, and a prominent countdown.
Accepted limits: keystrokes in the last roughly 1–2 seconds can lose the race with a round change, as with a paper sheet being taken away. A delayed save may only become visible on the next sheet render. Edits cannot reach the server while offline. Use Abgeben before the timer expires. Multiple tabs sharing one participant identity can overwrite each other’s edits.
Explicit arguments override environment defaults. ENV is read when
run_app() is called, not when the package loads.
| Argument | Environment | Default |
|---|---|---|
db_path |
DB_PATH |
data/brainwriting.sqlite |
mod_pin |
MOD_PIN |
635 |
base_url |
BASE_URL |
http://localhost:3838 |
port |
PORT |
3838 |
host |
— | 0.0.0.0 |
poll_ms |
— | 2500 ms |
Exactly one R process per data folder. Parallel courses can share
one instance as separate sessions. Do not use replicas, ShinyProxy, or
docker compose --scale. Use local persistent disk; a network
filesystem is not a supported SQLite deployment.
Build from the repository root. The image uses rocker/r-ver:4.4.2 and
runs as unprivileged UID 10001. A standalone local smoke run needs no
Traefik:
docker build -f docker/Dockerfile -t brainwritr:0.5.0 .
docker volume create brainwritr-data
docker run --rm --name brainwritr -p 3838:3838 \
-e MOD_PIN=choose-a-private-pin \
-v brainwritr-data:/app/data brainwritr:0.5.0For an existing Traefik installation with an external proxy network:
mkdir -p docker/data
sudo chown 10001:10001 docker/data
export BW_HOST=brainwriting.example.de
export TRAEFIK_CERTRESOLVER=letsencrypt
export MOD_PIN='choose-a-private-pin'
docker compose -f docker/docker-compose.yml up -d --buildReplace the example hostname and certificate resolver. DNS, TLS and the
existing Traefik websecure entrypoint must already be configured.
BASE_URL supplies the QR-code URL. The bind mount is docker/data/
relative to the Compose file; it must be writable by UID 10001. It holds
the main database and the sessions/ folder. Keep the volume when
updating images. Set TZ=Europe/Berlin for local export timestamps
(already set in Compose).
CSV is a UTF-8 long table with Thema, Bogen, Runde, Frage,
Teilnehmer, Beitrag, abgegeben, and Zeit. Zeit uses SQLite
local time; abgegeben is 0 or 1. Both drafts and submitted entries are
included. Empty saved entries are included in CSV and omitted from the
grouped Markdown protocol. Markdown groups Thema → Bogen →
R{round} · F{question} · {name}: text.
Use pseudonyms and avoid personal or sensitive information in contributions. The app stores names/pseudonyms, an opaque participant ID, joining and editing times, group assignments, plenum weights, and text on your server. Browser storage holds one participant ID per session and the language and light/dark theme preferences; a moderator tab also keeps its login token in session storage. It makes no application calls to external services and has no external usage tracking. The moderator can export all contributions and reset the session. Participants can read earlier drafts as well as submissions on their assigned sheet; this is a collaborative activity, not a confidential survey.
This supports data-minimizing, self-hosted use; it is not a blanket
GDPR/DSGVO compliance guarantee. The operator defines access, notice,
retention and backup policy. Reset deletes the session’s logical records
and Delete removes a session’s database file, but neither is secure
forensic erasure of SQLite pages, filesystem snapshots, exports or
backups. Archived sessions keep their data until deleted. Protect those
separately. Treat exported user text as untrusted when importing into
spreadsheet software or rendering Markdown; use text-only spreadsheet
import and a safe Markdown renderer. The CSV downloads stay raw by
default for statistical use. CSV für Tabellenkalkulation absichern
prefixes =, +, -, @, a tab or a carriage return with an
apostrophe (OWASP CSV-injection guidance) at the start of a cell and
after an embedded comma, semicolon, tab or line break, so spreadsheets
that split on ; or start a new record at a line break see no live
formula; RDS stays lossless.
devtools::document()
devtools::test()
devtools::check(args = "--as-cran")
lintr::lint_package()
rmarkdown::render("README.Rmd")
pkgdown::build_site()Tests cover rotation, lifecycle, balanced assignment, late arrivals,
upserts, clock advancement, authorization, exports, reset, the session
catalog, routing, overview actions, login throttling, plenum budgets,
results and exports. shinytest2 exercises a phone-sized browser, PIN
rejection, reload/resume, polling stability, a participant-driven round
change, separate identities per session, moderator navigation between
sessions and the weighting sliders’ budget in the browser. Browser tests
skip on CRAN or if Chrome is unavailable; set CHROMOTE_CHROME to a
Chromium executable to enable them locally. CI checks Linux, macOS and
Windows, lint, and builds the CTTIR-themed pkgdown site.
See the classroom and deployment guide for facilitation, persistence and operational details. Report bugs at GitHub Issues. MIT licensed.








