Skip to content

Repository files navigation

Rephrame — italic r monogram with copper framing rule

Rephrame

A private, offline-first CBT journal.
No account · no server · no tracking · everything stays on your device.

Live demo PWA License


A single-page, offline-first cognitive behavioral therapy journal you can install to your home screen and run as a local, private app. No account, no server, no analytics — everything lives in your browser's localStorage. You can import and export your entries as JSON for backup.

Four ways to capture. Pick whichever fits the moment:

  • Thought record — built on the Mind Over Mood 7-column thought record (Greenberger & Padesky), with a distortion check from Burns and Beck, a guided Socratic question you answer, a follow-up action, and mood re-ratings after the reframe and after you act. Captures situation, multiple thoughts and moods, body sensations, evidence for/against, and a balanced reframe in your own words. The shift in mood intensity + belief % is the work showing up.
  • Free write — open page, no structure. For moments without a clear thought to challenge. Optional title + optional mood tag.
  • Plan activity — behavioral activation: pick something concrete (a category + a datetime), predict pleasure + mastery on a 0–10 scale (Beck's activity scheduling), then come back after to log actual values. Comparing the two can show that low mood underestimates how things go; doing the activity is what matters most.
  • Park a worry — worry postponement (Borkovec): write the worry, set urgency, schedule it for your worry-window time. A calm banner surfaces in-window with three resolutions: dissolved on its own, work it through or make a plan (opens a thought record, which ends in one concrete step), or postpone again. After two postponements the worry suggests working it through instead.

The "Patterns" tab surfaces recurring distortions, the average mood intensity drop across re-rated entries, the activity categories that lift the mood most, the % of worries that dissolved without action, and a 30-day activity heatmap. The "Reference" tab is an in-app primer plus a crisis-resources block.

When even a free-form entry is too much in the moment, the ⚡ icon in the top-right opens a 30-second quick capture (just the thought + intensity). The entry lands in your journal flagged for finishing later.

First-time users get an onboarding card with a "load example entry" button so you can see what a complete thought record looks like before writing your own.

Coping cards. Tap the ★ on any entry to pin its reframe as a coping card. Pinned reframes surface as a horizontal carousel at the top of the Journal so you can re-read what's landed for you in past similar moments. Tap a card to jump to the full entry.

Pivot follow-up. When you check a pivot as done, the entry expands a "What happened?" reflection field. The dread-vs-actual gap is the part to write down — it's what teaches you next time. Saved on blur, included in Markdown export, searchable.

Scope filters. Above the search, chips let you narrow to All / ★ Coping / ⚡ Unfinished / Free writes / Activities / Worries / This week / Pivoted / Pivot due. Each chip auto-hides when its count is zero. Stacks with the per-distortion chips below the search. Picking a kind-filter with zero matching entries shows a friendly empty state with a "Start one" CTA that drops straight into Capture with the right mode preselected.

Settings. The gear icon in the top-right opens a settings modal:

  • Theme — Auto (follow OS) / Light / Dark.
  • Gentle nudge — Off / Daily / Every 3 days / Weekly. Shows a soft banner the next time you open the app if your last entry is older than the chosen interval. No notifications go out. "Not today" snoozes the banner for 18 hours.
  • Worry window — the time of day parked worries reappear for review. Defaults to 18:00 (6pm). Worry time lasts 20 minutes from that time; keep it the same every day and away from bedtime. If the time has already passed today, parking a new worry schedules it for tomorrow.

Print / Save as PDF. The Export modal has a "Print / Save as PDF" option that expands every entry, opens your browser's print dialog, and switches to a printer-friendly layout. Pick "Save as PDF" in the dialog to keep an offline archival copy.

Copy a single entry. Each expanded entry has a "Copy" button that puts the entry's Markdown on your clipboard — useful for pasting one record into a message to a clinician without exporting the whole journal.

Undo delete. Deleting an entry now shows an "Undo" toast for 6 seconds before it sticks. Click Undo to restore the entry to its original position in the list.

Privacy lock (PIN). Optional 4–8 digit PIN gated on each new tab session. Stored as a SHA-256 hash on the device only. The entries themselves aren't encrypted — this is a soft lock that stops casual snooping on a shared phone, not an attacker with developer-tools access. There's no PIN recovery: if you forget, the only way back in deletes everything, so export to JSON regularly.

SW update banner. When a new version of the app is deployed, the service worker installs it in the background and you'll see a "new version is ready — reload?" banner at the top. Your entries are untouched by the reload.

Sync between devices (optional). Settings → Sync turns on a peer-to-peer link so two of your devices hold the same journal. Pairing: enable Sync on both devices; the first shows an 18-character pairing code (formatted XXX-XXX-YYYY-YYYY-YYYY), which you enter on the second. The first six characters are the device's room id; the remaining twelve are a random secret that never leaves the two devices. The code is shown only right after it is generated (it disappears once a device has paired or the page is closed), and neither device stores the secret itself — each keeps only the encryption and authentication keys derived from it, in IndexedDB. Dashes and letter case don't matter when typing it. Once the connection opens, both ends prove they hold the secret (an HMAC challenge–response in each direction) before a single entry is sent, then exchange a full snapshot encrypted with AES-GCM under a key derived from the secret; after that every save broadcasts the same way. Conflicts are resolved last-write-wins per entry by updatedAt; deletions carry tombstones so a stale peer can't resurrect a removed entry. If the link drops, the app auto-reconnects with exponential backoff (five attempts) and the sync panel shows a live status — connecting, connected, reconnecting, or "Reconnect failed". Tap Reconnect to retry manually, Generate new pairing code to rotate (a fresh room id and secret; the other device must be re-paired with the new code), or Disable sync to take the device offline.

Pairings made before this scheme (a room id with no secret) are retired: Sync shows a one-time "re-pairing needed" notice and neither dials nor accepts anything until you generate a new code on one device and enter it on the other. Old 6-character codes are refused. If a PIN is set, no peer is registered and nothing is dialled while the journal is locked; sync starts after a successful unlock and stops again on Lock now.

The link uses WebRTC data channels via PeerJS, with public STUN servers for NAT traversal — once negotiated, journal data flows device-to-device and never touches the matchmaking server. PeerJS ids are first-come on a public broker with no proof of ownership, which is why the secret exists: someone who registers your room id learns nothing without it. The pairing code is still sensitive while it is on screen — anyone who copies it can pair with your journal until you generate a new one — so don't share it over channels others can read.

Live demo

This repo is GitHub Pages-ready. To publish your own copy:

  1. Push the branch to GitHub.
  2. In the repo's Settings → Pages, select the branch (e.g. main) and / (root) as the source.
  3. Wait ~30 seconds. The site is live at https://<your-user>.github.io/<repo>/.

That URL is a fully working PWA — installable, offline-capable, with service-worker auto-updates and the in-app reload banner when a new version ships. The .nojekyll file is included so GitHub Pages serves files verbatim; all asset paths in manifest.json, sw.js, and the HTML are relative, so the app works at any subpath.

Install

Desktop (Chrome, Edge, Brave, Arc…)

  1. Open index.html over HTTP/HTTPS — e.g. python3 -m http.server 8000 and visit http://localhost:8000/.
  2. The "+ Install" pill in the top-right will appear once the browser confirms the manifest + service worker are healthy. Click it.
  3. Rephrame launches as a standalone window. It works offline from then on.

iPhone / iPad (Safari)

  1. Open the site in Safari (the PWA install path on iOS only works through WebKit/Safari).
  2. Tap the Share button → Add to Home Screen → Add.
  3. Launching from the home screen opens Rephrame fullscreen with no browser chrome. Data is sandboxed to that installed app.

Android (Chrome)

  1. Open the site in Chrome over HTTPS or localhost.
  2. Either tap the in-app "+ Install app" pill or open Chrome's menu (⋮) → Install app / Add to Home screen.

Run it locally (fully private)

Rephrame is a static site — index.html, styles.css, app.js, manifest.json, sw.js, and a few icons. Any local web server works. Two one-liners:

# Python 3
python3 -m http.server 8000

# Node (one-shot, no install needed)
npx --yes http-server -p 8000 .

Then open http://localhost:8000/ and install. After the first load the service worker caches the app shell — HTML, JS, icons, manifest, fonts — so you can disconnect entirely. The fonts (Fraunces, Manrope, JetBrains Mono, all SIL OFL) are self-hosted under fonts/ and precached with the shell, so no visit ever contacts Google Fonts and the typography matches identically offline. On a brand-new install with no network, the app falls back to system fonts after a 1.5 s cap and stays usable.

file:// mode

Opening index.html straight from disk also works for journaling, but desktop browsers won't allow a "real" PWA install from file://. iOS Safari has the same limitation — use a local server (or host the folder somewhere private) if you want home-screen install.

When this tool fits

Rephrame is built for ordinary distress — frustration, embarrassment, catastrophic thinking, social worries, post-event rumination, the daily weather of being a person. It's deliberately not built for active trauma processing, severe depression, psychosis, or acute suicidal ideation, where solo cognitive work can be unhelpful or harmful. Use it alongside a clinician for those, and use the crisis resources below first if you need them now.

Three deliberate concessions to make the tool helpful rather than harmful:

  • "The facts seem to back these thoughts up" tile on Step 3 — opts out of the distortion frame. Grief, real anger and an honest look at a real mistake don't need arguing away; the evidence step still follows, since distorted thoughts can feel true too.
  • Grounding gate at intensity ≥80 (the Severe band) — surfaces a 5-4-3-2-1 prompt at the top of Step 4 and inside quick-capture, because cognitive work tends to land better after the body has settled.
  • "Just venting" checkbox in quick-capture — sometimes naming what's there is the intervention; no obligation to finish a 7-step entry later.

The re-rate is awareness, not a grade. Small shifts are still real shifts; no shift is information too. Patterns and pivot-completion are framed the same way — "each one is information, whether followed or not."

Safety

Rephrame is a journaling tool, not a substitute for therapy or crisis care. If you can't pause and write, please reach out:

  • 988 Suicide & Crisis Lifeline (US): call or text 988, chat at chat.988lifeline.org
  • 9-8-8: Suicide Crisis Helpline (Canada): call or text 988
  • Text lines: HOME to 741741 (US, Crisis Text Line) · SHOUT to 85258 (UK, Shout) · 50808 (Ireland, Text About It) · CONNECT to 686868 (Canada, Kids Help Phone, for young people)
  • Samaritans (UK / Ireland): 116 123
  • Other countries: findahelpline.com

The full list is also inside the app under Reference → If you're in crisis, and reachable from a link in every empty-state and capture modal.

Your data

  • Stored in localStorage under the keys reframe-journal-v1 (entries), reframe-journal-draft-v1 (in-progress capture), reframe-settings-v1 (theme, nudge interval, worry-window time), and reframe-pin-hash-v1 (PBKDF2-SHA256 hash + salt of your PIN, if set). Never sent anywhere except, if you've enabled Sync, to the device you paired with — and only directly, over WebRTC, encrypted with keys derived from the pairing secret.
  • Sync keeps everything about a pairing in IndexedDB (database rephrame-sync, store keys): a pairing record with the room id, the paired device's id and a verified flag, and a keys record with the AES-GCM and HMAC keys derived from the pairing secret, stored as non-extractable WebCrypto keys so nothing can read them back as bytes. The secret itself is never stored anywhere. localStorage keeps only rephrame_peer_id_v1 (this device's id), rephrame_entry_dels (deletion tombstones) and rephrame_sync_enabled — nothing that came from a pairing code. Disabling sync removes the pairing record and the keys.
  • A one-time onboarding flag lives under reframe-onboarded-v1.
  • The unlock token (reframe-unlocked) lives in sessionStorage and clears when the tab closes, so the PIN gate re-arms on each new session.
  • The Import / Export buttons in the top-right round-trip the full journal as JSON. Use Export to back up, and Import (Replace or Merge) to restore on a new device.
  • Uninstalling the PWA or clearing site data deletes everything. Export first.

Files

index.html                 markup shell (links styles.css + app.js)
styles.css                 all app styles
app.js                     app UI + logic
manifest.json              PWA manifest (name, icons, shortcuts)
sw.js                      service worker (offline cache)
js/pwa.js                  install prompt + SW registration + file:// fallback
js/sync.js                 optional P2P sync (WebRTC via PeerJS)
js/vendor/peerjs.min.js    vendored PeerJS — no npm supply chain
fonts/                     self-hosted web fonts (woff2 + fonts.css)
icons/                     SVG app icons
tests/                     static checks, a Node test of the sync crypto/handshake, Playwright smoke/flow/robustness/sync/regression walks
eslint.config.js           lint config (run via `npm run lint`)

License

MIT — see LICENSE.

About

A offline-first cognitive behavioral therapy journal you can install to your home screen and run as a local, private app. No account, no server, no analytics.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Packages

Used by

Contributors

Languages