Skip to content

Repository files navigation

brainwritR brainwritR hex sticker

R-CMD-check License: MIT

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.

Install and start

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"
)

Individual-device workflow

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.

Weigh contributions in the plenum

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.

Run several sessions

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.

Screenshot gallery

Actual browser captures with English selected and English example content.

Prepare the activity Individual device
English setup with mode selection and example questions English mobile participant sheet with two questions and timer
Shared group device Hot-seat handover
English group claim buttons English hot-seat handover and start button
Session overview Plenum weighting
English session overview with open, QR code, restart, archive and delete actions English weighting sliders distributing 100 percent per topic

English plenum overview with ranked contributions and chart

English descriptive analytics with indicators, contribution chart and term figures

Modi: match the devices in the room

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.

Prepare once, reuse with YAML

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.

Analytics and reports

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
PDF 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.

How rotation works

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 1

A 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.

Mobile robustness

  • 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 localStorage for 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.

Configuration

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.

Docker and Traefik

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.0

For 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 --build

Replace 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).

Export and privacy

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.

Development and quality

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.

About

Resilient classroom brainwriting with a Shiny app, SQLite persistence, and mobile reconnect support

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages