diff --git a/Atomic-Blog-Generation-Pipeline/visuals/.gitignore b/Atomic-Blog-Generation-Pipeline/visuals/.gitignore new file mode 100644 index 0000000..3774a4c --- /dev/null +++ b/Atomic-Blog-Generation-Pipeline/visuals/.gitignore @@ -0,0 +1,2 @@ +.chrome-profile/ +__pycache__/ diff --git a/Atomic-Blog-Generation-Pipeline/visuals/atomic_viz.py b/Atomic-Blog-Generation-Pipeline/visuals/atomic_viz.py new file mode 100644 index 0000000..d03e88d --- /dev/null +++ b/Atomic-Blog-Generation-Pipeline/visuals/atomic_viz.py @@ -0,0 +1,191 @@ +"""Blog visuals in the Atomic "Technical Editorial" design system. + +Every figure is an HTML page rendered to PNG by headless Chrome: +ink #15171B on paper #F4F5F1, one Signal accent #3A2FF0, Bebas Neue headlines, +Hanken Grotesk body, JetBrains Mono labels, ink borders and hard offset shadows +(no blur, no gradients, dot grid on banners only). + +Figures are drawn on a 1200 px wide canvas and shown about 760 px wide in an article +(a 0.63 scale), so text is sized up: body >= 22 px, mono labels >= 19 px. + + python visuals/render.py what-is-a-local-first-notes-app +""" +import os +import subprocess + +HERE = os.path.dirname(os.path.abspath(__file__)) +PIPELINE = os.path.dirname(HERE) +COMMUNITY = os.path.dirname(PIPELINE) +WORKSPACE = os.path.dirname(COMMUNITY) +CHROME = os.environ.get("CHROME", "C:/Program Files/Google/Chrome/Application/chrome.exe") +# The app repository ships the three brand fonts. Override with ATOMIC_FONTS. +FONTS = os.environ.get("ATOMIC_FONTS", os.path.join(WORKSPACE, "Atomic-Notes-App-V0.2", "assets", "fonts")) +PUBLIC = os.path.join(COMMUNITY, "public") + + +def file_url(path): + return "file:///" + os.path.abspath(path).replace(os.sep, "/") + + +def mockup(n): + return file_url(os.path.join(PUBLIC, "mockups", f"atomic-notes-mockup-image-{n:02d}.png")) + + +ATOM = file_url(os.path.join(PUBLIC, "icons", "atom.svg")) +ICON = file_url(os.path.join(PUBLIC, "icon.png")) + +CSS = """ +@font-face { font-family: Bebas; src: url("__F__/BebasNeue-Regular.ttf"); } +@font-face { font-family: Hanken; font-weight: 400; src: url("__F__/HankenGrotesk-Regular.ttf"); } +@font-face { font-family: Hanken; font-weight: 500; src: url("__F__/HankenGrotesk-Medium.ttf"); } +@font-face { font-family: Hanken; font-weight: 700; src: url("__F__/HankenGrotesk-Bold.ttf"); } +@font-face { font-family: Mono; font-weight: 400; src: url("__F__/JetBrainsMono-Regular.ttf"); } +@font-face { font-family: Mono; font-weight: 500; src: url("__F__/JetBrainsMono-Medium.ttf"); } +:root { --ink:#15171B; --paper:#F4F5F1; --white:#FFFFFF; --surface:#EDEEE8; --signal:#3A2FF0; + --slate:#4A4D55; --line:#C6C6CB; --light:#8F88FF; --mist:#D8D6FF; --deep:#1D14A0; + --error:#BA1A1A; --error-bg:#FFDAD6; --on-error:#93000A; --high:#EB7D00; } +* { box-sizing:border-box; margin:0; padding:0; } +body { width:__W__px; height:__H__px; overflow:hidden; background:var(--paper); color:var(--ink); + font-family:Hanken, sans-serif; -webkit-font-smoothing:antialiased; } +.stage { position:relative; width:__W__px; height:__H__px; overflow:hidden; background:var(--paper); } +.stage.dots { background-image:radial-gradient(rgba(21,23,27,.12) 1.6px, transparent 1.8px); background-size:30px 30px; } +.stage.dark { background:var(--ink); color:var(--paper); } +.hdr { position:absolute; left:56px; right:56px; top:40px; display:flex; justify-content:space-between; align-items:center; } +.eyebrow { font-family:Mono; font-size:19px; letter-spacing:3px; text-transform:uppercase; color:var(--signal); } +.dark .eyebrow { color:var(--light); } +.mark { display:flex; align-items:center; gap:10px; font-family:Mono; font-size:17px; letter-spacing:2.5px; color:var(--slate); } +.dark .mark { color:rgba(244,245,241,.62); } +.mark img { width:30px; height:30px; border-radius:7px; box-shadow:2px 2px 0 var(--ink); } +.dark .mark img { box-shadow:2px 2px 0 var(--signal); } +h1.t { position:absolute; left:56px; right:56px; top:78px; font-family:Bebas; font-weight:400; font-size:62px; + line-height:.95; letter-spacing:.5px; text-transform:uppercase; } +.s { color:var(--signal); } +.dark .s { color:var(--light); } +.rule { position:absolute; left:56px; right:56px; height:0; border-top:1.5px solid var(--ink); } +.dark .rule { border-color:rgba(244,245,241,.3); } +.foot { position:absolute; left:56px; right:56px; bottom:30px; font-family:Mono; font-size:18px; letter-spacing:.6px; color:var(--slate); } +.dark .foot { color:rgba(244,245,241,.62); } +.box { position:absolute; background:var(--white); border:2.5px solid var(--ink); border-radius:9px; + box-shadow:8px 8px 0 var(--ink); padding:20px 22px; } +.box.sigshadow { box-shadow:8px 8px 0 var(--signal); } +.box.flat { box-shadow:none; } +.box.surface { background:var(--surface); } +.box.signal { background:var(--signal); color:#fff; } +.box.ink { background:var(--ink); color:var(--paper); box-shadow:8px 8px 0 var(--signal); } +.box.warn { background:var(--error-bg); border-color:var(--error); box-shadow:8px 8px 0 var(--error); color:var(--on-error); } +.dark .box { box-shadow:8px 8px 0 var(--signal); border-color:var(--paper); } +.dark .box.panel { background:rgba(244,245,241,.05); border:2px solid rgba(244,245,241,.32); box-shadow:none; color:var(--paper); } +.lbl { font-family:Mono; font-size:19px; letter-spacing:1.6px; text-transform:uppercase; color:var(--signal); } +.box.signal .lbl, .box.ink .lbl { color:var(--mist); } +.box.ink .lbl { color:var(--light); } +.dark .box.panel .lbl { color:var(--light); } +.box.warn .lbl { color:var(--error); } +.muted { color:var(--slate); } +.box.ink .muted, .dark .muted { color:rgba(244,245,241,.7); } +.box.signal .muted { color:var(--mist); } +.ttl { font-family:Bebas; font-size:40px; line-height:.95; text-transform:uppercase; margin-top:8px; } +.ttl.big { font-size:54px; } +.txt { font-size:23px; line-height:1.38; margin-top:8px; } +.num { font-family:Bebas; font-size:60px; line-height:.9; color:var(--signal); } +.box.signal .num { color:#fff; } +.chip { display:inline-block; font-family:Mono; font-size:17px; letter-spacing:1.4px; text-transform:uppercase; + padding:6px 12px; border:2px solid var(--ink); border-radius:999px; background:var(--paper); } +.chip.on { background:var(--signal); border-color:var(--signal); color:#fff; } +.chip.ink { background:var(--ink); color:var(--paper); } +.chip.bad { background:var(--error-bg); border-color:var(--error); color:var(--on-error); } +.chip.ghost { background:transparent; } +.dark .chip { border-color:var(--paper); background:transparent; color:var(--paper); } +.dark .chip.on { background:var(--signal); border-color:var(--signal); } +.phone { position:absolute; background:var(--ink); border-radius:34px; padding:9px; box-shadow:10px 10px 0 var(--signal); } +.phone.inkshadow { box-shadow:10px 10px 0 var(--ink); } +.phone img { display:block; width:100%; border-radius:26px; } +.phone .cam { position:absolute; top:16px; left:50%; width:9px; height:9px; margin-left:-4px; border-radius:50%; background:#2c2f36; } +table.d { position:absolute; border-collapse:separate; border-spacing:0; background:var(--white); border:2.5px solid var(--ink); + border-radius:9px; overflow:hidden; box-shadow:8px 8px 0 var(--ink); } +table.d th, table.d td { padding:15px 18px; border-bottom:1.5px solid var(--line); text-align:left; vertical-align:middle; } +table.d thead th { font-family:Mono; font-weight:500; font-size:18px; letter-spacing:1.4px; text-transform:uppercase; background:var(--surface); + border-bottom:2.5px solid var(--ink); } +table.d thead th.hl { background:var(--signal); color:#fff; } +table.d tbody th { font-family:Bebas; font-weight:400; font-size:30px; line-height:1; text-transform:uppercase; white-space:nowrap; } +table.d td { font-size:21px; line-height:1.3; } +table.d td.hl { background:rgba(58,47,240,.06); font-weight:500; } +table.d tr:last-child th, table.d tr:last-child td { border-bottom:0; } +.yes { color:var(--signal); font-family:Mono; font-weight:500; } +.no { color:var(--error); font-family:Mono; font-weight:500; } +.part { color:var(--slate); font-family:Mono; font-weight:500; } +svg.lines { position:absolute; inset:0; overflow:visible; } +""" + +HEAD = '' + + +def css(w, h): + return CSS.replace("__F__", file_url(FONTS)).replace("__W__", str(w)).replace("__H__", str(h)) + + +def figure(eyebrow, title, body, foot="", w=1200, h=760, theme="", lines="", title_top=78): + """A standard figure: eyebrow + brand mark, a split headline (wrap the accent in ), content, footnote.""" + hdr = (f'
{eyebrow}' + f'ATOMIC NOTES
') + t = f'

{title}

' if title else "" + f = f'
{foot}
' if foot else "" + svg = f'{ARROWS}{lines}' if lines else "" + return HEAD.format(css=css(w, h)) + f'
{hdr}{t}{svg}{body}{f}
', w, h + + +ARROWS = """ + + + + +""" + +COLORS = {"ink": "#15171B", "sig": "#3A2FF0", "err": "#BA1A1A", "light": "#8F88FF"} + + +def arrow(x1, y1, x2, y2, color="ink", dash=False, width=3.5, curve=None): + c = COLORS[color] + d = f"M{x1} {y1} Q{curve[0]} {curve[1]} {x2} {y2}" if curve else f"M{x1} {y1} L{x2} {y2}" + da = ' stroke-dasharray="10 8"' if dash else "" + return f'' + + +def box(x, y, w, h, inner, cls="", style=""): + return f'
{inner}
' + + +def phone(n, x, y, w, crop=None, shadow="sig"): + """A product screenshot in the phone frame. crop = visible screen height in px.""" + inner = f'' + if crop: + inner = f'
{inner}
' + cls = "phone" + (" inkshadow" if shadow == "ink" else "") + return f'
{inner}
' + + +def banner(eyebrow, title, sub, art, w=1200, h=630, title_size=92, text_width=640): + """Article cover and social card: dot-grid paper, giant split headline with an ink offset shadow, art on the right.""" + body = (f'
{eyebrow}' + f'ATOMIC NOTES
' + f'

{title}

' + f'

{sub}

' + f'
' + f'
ATOMIC-NOTES.DEVBEHINDYOU.COM
{art}') + return HEAD.format(css=css(w, h)) + f'
{body}
', w, h + + +def render(name, page, out_dir, scale=1.5): + """Writes .html next to the PNG (for review), then screenshots it at the given device scale.""" + html, w, h = page + os.makedirs(out_dir, exist_ok=True) + src = os.path.join(out_dir, "_src") + os.makedirs(src, exist_ok=True) + path = os.path.join(src, name + ".html") + with open(path, "w", encoding="utf-8") as f: + f.write(html) + png = os.path.join(out_dir, name + ".png") + subprocess.run([CHROME, "--headless=new", "--disable-gpu", "--hide-scrollbars", "--allow-file-access-from-files", + f"--force-device-scale-factor={scale}", f"--window-size={w},{h}", "--virtual-time-budget=4000", + f"--user-data-dir={os.path.join(HERE, '.chrome-profile')}", + f"--screenshot={png}", file_url(path)], check=True, capture_output=True) + return png diff --git a/Atomic-Blog-Generation-Pipeline/visuals/posts/__init__.py b/Atomic-Blog-Generation-Pipeline/visuals/posts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Atomic-Blog-Generation-Pipeline/visuals/posts/notes_app_privacy_red_flags.py b/Atomic-Blog-Generation-Pipeline/visuals/posts/notes_app_privacy_red_flags.py new file mode 100644 index 0000000..5fd1781 --- /dev/null +++ b/Atomic-Blog-Generation-Pipeline/visuals/posts/notes_app_privacy_red_flags.py @@ -0,0 +1,129 @@ +"""Figures for P1: 9 Privacy Red Flags to Check Before Trusting Any Notes App in 2026.""" +from atomic_viz import figure, banner, box, phone, arrow + +FLAGS = [ + ("ACCOUNT FIRST", "No note until you sign up."), + ("NO EXPORT", "No way out in an open format."), + ("VAGUE ENCRYPTION", "\"Encrypted\", but who holds the key?"), + ("GREEDY PERMISSIONS", "Contacts, location, call logs."), + ("AD SDKS", "Ads need a profile of you."), + ("TRACKERS", "Analytics that watch you write."), + ("FORCED AI", "Your notes sent to a model."), + ("LOCKED FORMAT", "Exports nothing else can open."), + ("NO STORAGE ANSWER", "Can't say where notes live."), +] + + +def build(): + out = {} + + rows = "".join( + f'
' + f'{i + 1:02d}' + f'{t}
' + for i, (t, _) in enumerate(FLAGS)) + art = box(760, 84, 380, 494, f'
CHECK BEFORE YOU TRUST
{rows}', + cls="sigshadow", style="padding:18px 22px") + out["01-banner"] = banner( + "PRIVACY · CHECKLIST · 2026", + '9 PRIVACY RED FLAGS', + "Nine signs a notes app knows, keeps, or shares more than it tells you.", + art, title_size=100, text_width=640) + + # The nine flags at a glance. + checks = ["Open the app before signing in.", "Look for Export in settings.", "Find who holds the key.", + "Read the permission list.", "Scan the store page for ads.", "Run an Exodus Privacy scan.", + "Look for an AI off switch.", "Open an export in a text editor.", "Ask: device, cloud, or both?"] + body = "" + for i, ((t, d), c) in enumerate(zip(FLAGS, checks)): + col, row = i % 3, i // 3 + x, y = 56 + col * 370, 194 + row * 186 + body += box(x, y, 340, 158, + f'
{i + 1:02d}' + f'CHECK
' + f'
{t}
{c}
', + style="padding:12px 18px") + out["02-nine-red-flags"] = figure("FIG 1 · THE CHECKLIST", 'NINE FLAGS. ONE MINUTE EACH.', body, + "One flag is a question. Three or more is a pattern.", h=790) + + # Where a note's text and data can travel. + body = box(76, 330, 250, 150, + '
YOUR NOTE
ON YOUR PHONE
', cls="signal sigshadow") + dests = [ + ("APP SERVER", "Plain text unless end-to-end encrypted.", 182), + ("ANALYTICS SDK", "Screens, taps, sessions, device.", 300), + ("AD NETWORK", "An interest profile for targeting.", 418), + ("AI PROVIDER", "Note text sent for processing.", 536), + ("BACKUPS", "Copies that outlive a delete.", 654), + ] + lines = "" + for t, d, y in dests: + body += box(640, y - 8, 504, 96, + f'
{t}' + f'CAN LEAK
{d}
', + cls="flat", style="padding:12px 18px") + lines += arrow(334, 405, 630, y + 40, color="err", dash=True, width=3) + out["03-where-notes-travel"] = figure("FIG 2 · EXPOSURE MAP", 'WHERE A NOTE CAN TRAVEL', body, + "Nobody has to read a note for it to expose you. Each path is a flag on the checklist.", lines=lines, h=800) + + # Three layers of evidence, strongest on top. + layers = [ + ("PUBLIC CODE + TRACKER SCAN", "Anyone can check the libraries an app ships and the trackers inside its APK.", "STRONGEST", "signal"), + ("PRIVACY POLICY", "Legally binding, but often written in broad phrases like \"trusted partners\".", "BETTER", "surface"), + ("STORE PRIVACY LABEL", "Self-declared. Google says developers alone are responsible for its accuracy.", "WEAKEST", ""), + ] + body = "" + for i, (t, d, tag, cls) in enumerate(layers): + y = 196 + i * 150 + body += box(170, y, 974, 124, + f'
{t}' + f'{tag}
' + f'
{d}
', + cls=cls + (" sigshadow" if cls == "signal" else ""), style="padding:16px 22px") + body += ('
MORE
PROOF
' + '
LESS
PROOF
') + lines = arrow(101, 584, 101, 250, color="sig", width=4) + out["04-evidence-ladder"] = figure("FIG 3 · TRUST, BUT VERIFY", 'THREE LAYERS OF EVIDENCE', body, + "A label is a claim. A policy is a promise. Code and scans are evidence.", lines=lines, h=720) + + # Permissions a notes app needs, and ones it doesn't. + perms = [ + ("INTERNET", "Sync and sign-in", "needed", "Requested"), + ("ACCESS_NETWORK_STATE", "Knowing when to sync", "needed", "Requested"), + ("USE_BIOMETRIC", "Fingerprint app lock", "needed", "Requested"), + ("RECORD_AUDIO", "Only for voice notes", "maybe", "Not requested"), + ("READ_CONTACTS", "Nothing a notes app needs", "no", "Not requested"), + ("ACCESS_FINE_LOCATION", "Rarely justified", "no", "Not requested"), + ("READ_PHONE_STATE", "Device identity", "no", "Not requested"), + ] + mark = {"needed": 'NEEDED', "maybe": 'DEPENDS', "no": 'RED FLAG'} + trs = "".join(f'{p}{why}{mark[k]}{a}' + for p, why, k, a in perms) + body = (f'' + f'{trs}
ANDROID PERMISSIONWHAT IT IS FORVERDICTATOMIC NOTES
') + out["05-permissions-check"] = figure("FIG 4 · PERMISSIONS", 'WHAT A NOTES APP SHOULD ASK FOR', body, + "Atomic Notes 2.03.5 requests three permissions. Check any app in Settings, Apps, Permissions.", h=760) + + # Atomic Notes, checked against its own list. + status = [("PARTIAL", "A Google account is required. Notes still save on the phone first."), + ("PARTIAL", "No export button yet. Synced notes are readable JSON in your Drive."), + ("CLEAR", "Optional vault with a key only you hold. Off by default."), + ("CLEAR", "Three permissions: internet, network state, biometric lock."), + ("CLEAR", "No ads and no ad SDKs."), + ("CLEAR", "No analytics, crash, or tracker SDKs."), + ("CLEAR", "No AI features."), + ("CLEAR", "One JSON file per note."), + ("CLEAR", "Phone first, then your own Google Drive.")] + body = "" + for i, ((t, _), (st, d)) in enumerate(zip(FLAGS, status)): + col, row = i % 3, i // 3 + x, y = 56 + col * 370, 194 + row * 196 + clear = st == "CLEAR" + body += box(x, y, 340, 170, + f'
{i + 1:02d} · {t}
' + f'{st}' + f'
{d}
', + cls="" if clear else "surface flat", style="padding:14px 18px") + out["06-atomic-notes-scorecard"] = figure("FIG 5 · OUR OWN SCORE", 'ATOMIC NOTES, CHECKED HONESTLY', body, + "Seven clear, two partial. Email sign-in and an export button are on the roadmap.", h=830) + return out diff --git a/Atomic-Blog-Generation-Pipeline/visuals/posts/what_is_a_local_first_notes_app.py b/Atomic-Blog-Generation-Pipeline/visuals/posts/what_is_a_local_first_notes_app.py new file mode 100644 index 0000000..a006678 --- /dev/null +++ b/Atomic-Blog-Generation-Pipeline/visuals/posts/what_is_a_local_first_notes_app.py @@ -0,0 +1,116 @@ +"""Figures for L1: What Is a Local First Notes App and Why Does It Matter?""" +from atomic_viz import figure, banner, box, phone, arrow + + +def build(): + out = {} + + art = (phone(3, 820, 92, 300, crop=560) + + box(690, 452, 300, 118, + '
SAVED ON DEVICE
0 MS · OFFLINE
', + cls="sigshadow", style="padding:16px 20px")) + out["01-banner"] = banner( + "LOCAL FIRST · THE GUIDE", + 'WHAT IS A LOCAL FIRST NOTES APP?', + "Your phone holds the real copy. The cloud holds a backup. Here is why that order matters.", + art, title_size=96, text_width=620) + + # Two ways to save one note. + def lane(y, label, steps, note, sig): + html = f'
{label}
' + x = 56 + for i, (t, d, cls) in enumerate(steps): + html += box(x, y, 238, 132, f'
{t}
{d}
', + cls=cls, style="padding:16px 18px") + x += 278 + html += f'

{note}

' + return html + + body = lane(214, "CLOUD-FIRST APP", [ + ("You type", "A new line in a note.", ""), + ("Network", "The app sends it out.", "surface flat"), + ("Server saves", "The real copy lives there.", "surface flat"), + ("Phone shows saved", "Only after the reply.", ""), + ], "Every save waits on a round trip. No network means no save, or a cache that may not survive.", False) + body += lane(496, "LOCAL-FIRST APP", [ + ("You type", "A new line in a note.", ""), + ("Phone saves", "Written to device storage.", "signal flat"), + ("Sync later", "Queued until a network shows up.", "surface flat"), + ("Cloud copy", "A backup that follows you.", ""), + ], "The save finishes on the device. Sync is a separate job that can wait.", True) + lines = "" + for y, color, dash in ((280, "ink", False), (562, "sig", False)): + for i in range(3): + x1 = 56 + 238 + i * 278 + 6 + lines += arrow(x1, y, x1 + 26, y, color=color, dash=(dash or (y == 562 and i == 1)), width=3) + out["02-two-ways-to-save"] = figure("FIG 1 · THE WRITE PATH", 'TWO WAYS TO SAVE A NOTE', body, lines=lines, h=712) + + # The spectrum of where the source of truth lives. + stops = [ + ("CLOUD ONLY", "The server holds the only real copy. Your screen is a window onto it.", "Most web notes tools", ""), + ("OFFLINE CACHE", "Recent notes are cached for reading. New writes may fail or wait.", "Many sync apps", ""), + ("OFFLINE FIRST", "Writes queue on the device, but the server's copy wins any argument.", "Apps built to survive bad signal", "surface"), + ("LOCAL FIRST", "The device owns the data. Sync copies it to other devices and the cloud.", "Atomic Notes, Obsidian, Logseq", "signal"), + ] + body = "" + for i, (t, d, ex, cls) in enumerate(stops): + x = 56 + i * 280 + body += box(x, 256, 248, 330, + f'
{"0" + str(i + 1)}
{t}
{d}
' + f'
{ex}
', + cls=cls + (" sigshadow" if cls == "signal" else ""), style="padding:18px 20px") + body += ('
' + 'THE SERVER OWNS ITYOU OWN IT
') + lines = arrow(56, 228, 1136, 228, color="sig", width=4) + out["03-source-of-truth-spectrum"] = figure("FIG 2 · THE SPECTRUM", 'WHERE DOES THE REAL COPY LIVE?', body, + "Offline support is a feature. Local first is an architecture.", lines=lines, h=680) + + # Seven ideals, Ink & Switch 2019. + ideals = [("NO SPINNERS", "Instant, because the data is already on the device."), + ("MULTI-DEVICE", "Your work is not trapped on one device."), + ("NETWORK OPTIONAL", "Full reading and writing with no connection."), + ("COLLABORATION", "Real-time work with other people."), + ("THE LONG NOW", "Data that still opens years from now."), + ("PRIVACY BY DEFAULT", "Security and privacy are not an upgrade."), + ("YOU OWN IT", "Ultimate ownership and control stay with you.")] + body = "" + for i, (t, d) in enumerate(ideals): + col, row = (i % 4, i // 4) + x = 56 + col * 276 + (138 if row == 1 else 0) + y = 196 + row * 244 + body += box(x, y, 246, 214, + f'
{i + 1:02d}
{t}
{d}
', + cls="signal sigshadow" if i == 6 else "", style="padding:16px 18px") + out["04-seven-ideals"] = figure("FIG 3 · INK & SWITCH, 2019", 'THE SEVEN IDEALS OF LOCAL-FIRST SOFTWARE', body, + "Source: \"Local-first software\", Ink & Switch, 2019.", h=740) + + # How Atomic Notes applies it. + body = phone(7, 64, 196, 250, crop=470, shadow="ink") + body += box(390, 190, 360, 196, + '
1 · YOUR PHONE
THE REAL COPY
Hive storage on the device. Every note saves as you type, online or not.
', + cls="sigshadow") + body += box(390, 430, 360, 196, + '
2 · SYNC SERVER
METADATA ONLY
IDs, timestamps, flags, and versions. Never your titles or note text.
', + cls="surface") + body += box(800, 190, 344, 436, + '
3 · YOUR GOOGLE DRIVE
MY-ATOMIC-NOTES
' + '
3f2a9c41….atomic
8b17e0d2….atomic
c04d5b9e….atomic
' + '
One file per note, in a folder you own. The app can only see files it created.
') + body += ('
' + 'VAULT · OPTIONALTurn it on and the server and Drive only ever hold ciphertext.
') + lines = arrow(752, 288, 796, 288, color="sig") + arrow(570, 388, 570, 426, color="ink", dash=True) + arrow(752, 528, 796, 528, color="ink", dash=True) + out["05-atomic-notes-layers"] = figure("FIG 4 · ATOMIC NOTES", 'PHONE FIRST. DRIVE SECOND.', body, lines=lines, h=760) + + # The honest tradeoffs. + rows = [ + ("CONFLICTS", "Two devices can edit the same note offline.", "A stale edit is refused and saved as a conflict copy. Nothing is overwritten."), + ("LOST PHONE", "Edits that never synced live on one device.", "Auto sync runs a few seconds after you stop typing, on resume, and on reconnect."), + ("HARDER TO BUILD", "Retries, queues, and versions are real engineering.", "Replay-safe requests and one transaction per push."), + ("COLLABORATION", "Real-time co-editing needs CRDTs or similar.", "Not supported. Atomic Notes is a personal notes app."), + ] + trs = "".join(f"{a}{b}{c}" for a, b, c in rows) + body = (f'' + f'{trs}
TRADEOFFWHAT IT MEANSHOW ATOMIC NOTES HANDLES IT
') + out["06-local-first-tradeoffs"] = figure("FIG 5 · THE COST", 'WHAT LOCAL FIRST ASKS OF YOU', body, + "Local first moves the hard work from the server into the sync engine.", h=740) + return out diff --git a/Atomic-Blog-Generation-Pipeline/visuals/render.py b/Atomic-Blog-Generation-Pipeline/visuals/render.py new file mode 100644 index 0000000..7b951ff --- /dev/null +++ b/Atomic-Blog-Generation-Pipeline/visuals/render.py @@ -0,0 +1,33 @@ +"""Renders every figure for one blog post into public/blog//. + + python visuals/render.py [figure-name ...] + +Each post has a module in visuals/posts/ named after its slug (dashes become underscores) +with a build() function that returns {figure_name: page}. +""" +import importlib +import os +import shutil +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, HERE) +import atomic_viz as viz # noqa: E402 + + +def main(): + if len(sys.argv) < 2: + sys.exit(__doc__) + slug, only = sys.argv[1], set(sys.argv[2:]) + mod = importlib.import_module("posts." + slug.replace("-", "_")) + out = os.path.join(viz.PUBLIC, "blog", slug) + for name, page in mod.build().items(): + if only and name not in only: + continue + print("rendered", os.path.relpath(viz.render(name, page, out), viz.COMMUNITY)) + # The HTML sources are for review only. Keep them out of public/. + shutil.rmtree(os.path.join(out, "_src"), ignore_errors=True) + + +if __name__ == "__main__": + main() diff --git a/content/blog/notes-app-privacy-red-flags.md b/content/blog/notes-app-privacy-red-flags.md new file mode 100644 index 0000000..f3d314d --- /dev/null +++ b/content/blog/notes-app-privacy-red-flags.md @@ -0,0 +1,211 @@ +--- +title: "Notes App Privacy in 2026: 9 Red Flags to Check First" +slug: "notes-app-privacy-red-flags" +description: "Is your notes app private? Check these 9 notes app privacy red flags, from vague encryption to hidden trackers, with a free checker and a policy decoder." +excerpt: "Nine signs a notes app knows, keeps, or shares more than it tells you, and how to check each one in about a minute." +author: "ashutosh-sharma" +publishedAt: "2026-10-07" +updatedAt: "2026-10-07" +category: "privacy" +tags: ["privacy", "security", "trackers", "permissions", "encryption"] +featured: false +draft: false +coverImage: "/blog/notes-app-privacy-red-flags/01-banner.png" +coverAlt: "Cover reading 9 Privacy Red Flags, beside a checklist card of nine flags from account first to no storage answer" +canonical: "https://atomic-notes.devbehindyou.com/blog/notes-app-privacy-red-flags" +keywords: "notes app privacy, is notes app private, privacy focused notes app, third party trackers, data safety section, notes app encryption, privacy policy, data export options" +readingTime: "11 min read" +--- + +
+

Quick answer

+

Notes app privacy comes down to three questions: where your notes are stored, who holds the encryption key, and who pays for the app. Nine red flags answer those questions for you, from forced accounts and vague encryption to ad SDKs, trackers, and AI that reads everything. Each one takes about a minute to check.

+
+ +Evernote users got a surprise in December 2016. A privacy policy update said some employees could read notes to oversee the company's machine learning features. The backlash was so loud that Evernote reversed course two days later and made the program opt-in. + +Nobody had been hacked. A notes app had simply written down what it was allowed to do with the most personal text people own. + +That's the uncomfortable truth about notes app privacy. The risk usually isn't a breach. It's the fine print, the libraries inside the app, and the business model behind it. I build Atomic Notes, a privacy focused notes app for Android, and these nine red flags are the checklist I use on other apps, and on my own. + +## Is your notes app private? + +**Probably less than it feels.** Notes app privacy matters more than most app privacy, because notes hold passwords people shouldn't keep there, health worries, money plans, and half-finished ideas. That makes them more revealing than almost any other app on your phone. + +Notes app privacy also isn't only about whether someone reads your notes. A note can expose you without anyone opening it, through the server that stores it, the analytics code that watches you write, the ad network that profiles you, the AI provider that processes the text, and the backups that keep it after you delete it. + +![A note on your phone with dashed red paths to five places it can leak: the app server, an analytics SDK, an ad network, an AI provider, and backups.](/blog/notes-app-privacy-red-flags/03-where-notes-travel.png "Fig 1. Where a note can travel. Each path maps to a flag on the checklist below.") + +So asking "is my notes app private?" means asking three plainer questions: + +1. **Where are my notes stored?** On my phone, on the company's servers, or in storage I control? +2. **Who holds the key?** If the company can decrypt my notes, it can read them, share them, or hand them over. +3. **Who pays?** If I'm not paying, someone else is, and they usually pay for data. + +The nine red flags turn those questions into things you can check today. + +## The 9 notes app privacy red flags + +![Nine numbered cards, one per red flag, each with a one-minute check: account first, no export, vague encryption, greedy permissions, ad SDKs, trackers, forced AI, locked format, and no storage answer.](/blog/notes-app-privacy-red-flags/02-nine-red-flags.png "Fig 2. Nine flags, one minute each. One flag is a question. Three or more is a pattern.") + +### 1. You can't write a note without an account + +An account ties every note to your identity and to someone else's servers. If the account gets locked, your notes can lock with it. + +**Check:** install the app and try to write a note before signing up. **Better:** the app lets you write first, and the account only switches on sync. + +### 2. There's no way out + +If you can't export, you don't own your notes. You rent them. Good data export options cover every note, include attachments, and use open formats. + +**Check:** look for Export in settings, then open the file you get. **Better:** one-tap export to Markdown, plain text, or JSON, or notes stored as readable files you can copy anytime. + +### 3. "Encrypted" with no word on the key + +"Your data is encrypted in transit and at rest" sounds reassuring. It usually means HTTPS on the wire and encrypted disks on the server, while the company keeps the keys. Good notes app encryption says plainly whether it's end-to-end, and who can decrypt. + +**Check:** find the security page and look for the words "end-to-end" and "only you hold the key." **Better:** a one-page explanation in plain English, including what happens if you lose your key. This one flag tells you more about notes app privacy than any marketing page. + +### 4. Permissions that have nothing to do with notes + +Permissions are the fastest notes app privacy check you can run, because they show what an app can reach. A notes app asking for contacts, precise location, or call logs is collecting more than notes. + +![A table of Android permissions. Internet, network state, and biometric lock are needed. Record audio depends on voice notes. Contacts, fine location, and phone state are red flags. Atomic Notes requests only the first three.](/blog/notes-app-privacy-red-flags/05-permissions-check.png "Fig 3. What a notes app should ask for. Atomic Notes 2.03.5 requests three permissions.") + +**Check:** on Android, open Settings, then Apps, then the app, then Permissions. If you're comfortable with developer tools, you can list everything an app requested, not just what it was granted: + +

Terminal · Android platform tools

+ +```bash +# Every permission an installed app requested (USB debugging on) +adb shell dumpsys package com.notes.atomic | grep "permission" + +# Or inspect an APK file before you install it +aapt2 dump permissions atomic-notes.apk +``` + +Swap in any app's package name, which appears in its Play Store URL after `id=`. **Better:** a short list, each with a reason you can guess. + +### 5. Ads, or advertising SDKs + +Every ad slot runs on a profile of the person looking at it. The FTC's 2023 case against GoodRx shows how this goes wrong: the company shared people's health information with ad platforms through tracking code. Nobody broke in. The tracking did the damage. + +**Check:** look for ads in the app and a "Contains ads" label on its store page. **Better:** an app funded by its users, with no ad slots to fill. + +### 6. Third party trackers and analytics + +Third party trackers send your behavior to companies you've never heard of: which screens you open, when you write, how long you stay. Even with encrypted notes, that pattern reveals your routine, which is why trackers matter so much for notes app privacy. + +**Check:** search the app on Exodus Privacy, a free service that lists the trackers and permissions inside Android apps. **Better:** zero trackers and a privacy policy that says so in one line. + +### 7. AI that reads everything, with no off switch + +AI features send note content to a model, often run by another company. If you can't turn them off, the app has decided who reads your notes for you. That's a notes app privacy decision made without you. + +**Check:** find the AI settings, the named AI provider, and whether your text trains anything. **Better:** AI that's off by default and clearly scoped, or no AI at all. + +### 8. Exports in a format nothing else opens + +An export button doesn't help if the file only works in the app you're leaving. A proprietary format is a soft lock-in. + +**Check:** open an export in a plain text editor. **Better:** Markdown, plain text, or JSON, one note per file. + +### 9. No straight answer to "where are my notes?" + +Your notes live somewhere specific: your device, the company's cloud, or storage you own, in a particular country, under a particular key. A good app says which in one sentence. + +**Check:** search the privacy policy for "store," "server," and "location," and find out whether account deletion also deletes your notes and backups. **Better:** an app that names every place a copy exists, including cloud sync and backups. A [local first notes app](/blog/what-is-a-local-first-notes-app) makes this easy, because the answer starts with "on your phone." + +## Check your own notes app + +Open your current app's store page, settings, and privacy policy side by side. Tick every flag it raises. + +:::widget red-flag-checker + +Your score is a fast read on your notes app privacy today. One flag is a question worth asking the developer. Three or more is a pattern, and it's a sign your most private writing belongs somewhere else. + +## How to verify notes app privacy instead of trusting it + +**Treat every privacy claim as one of three kinds of evidence: a label, a policy, or something you can check yourself.** They are not equally strong. + +![Three stacked layers of evidence. A store privacy label is weakest, a privacy policy is better, and public code with a tracker scan is strongest.](/blog/notes-app-privacy-red-flags/04-evidence-ladder.png "Fig 4. A label is a claim. A policy is a promise. Code and scans are evidence.") + +**Store privacy labels** are the quickest read on notes app privacy and the weakest proof. Google Play's data safety section is filled in by the developer, and Google's own help page says developers alone are responsible for making it complete and accurate. Treat it as a claim, not a verdict. + +**The privacy policy** is legally binding, which makes it stronger. It's also where vague wording hides. Phrases like "trusted partners" and "improve our services" can allow far more than they seem to. Try the decoder below on phrases you've seen. + +:::widget policy-decoder + +**Code and scans** are the strongest proof, because you don't have to take anyone's word. A tracker scan shows what an app actually ships. Source code transparency goes further: when an app's code is public, anyone can check the libraries it uses and the network calls it makes. Atomic Notes is source-available for exactly this reason. You can read the code, but the license doesn't allow reuse, so I won't call it open source. + +## How does Atomic Notes score? + +I'd be a hypocrite to publish this list and skip my own app. Here's Atomic Notes 2.03.5, checked against all nine notes app privacy flags: + +![A grid of nine cards scoring Atomic Notes. Account first and no export are partial. The other seven flags are clear.](/blog/notes-app-privacy-red-flags/06-atomic-notes-scorecard.png "Fig 5. Atomic Notes, checked honestly: seven clear, two partial.") + +| Flag | Result | Detail | +|---|---|---| +| 1. Account first | Partial | A Google account is required to sign in. Notes still save on the phone first and work offline. | +| 2. No export | Partial | No export button yet. Synced notes are readable JSON files in your own Google Drive. | +| 3. Vague encryption | Clear | An optional vault with a key only you hold. It's off by default, and the app says so. | +| 4. Greedy permissions | Clear | Three permissions: internet, network state, and biometric lock. | +| 5. Ad SDKs | Clear | No ads and no advertising SDKs. | +| 6. Trackers | Clear | No analytics, crash-reporting, or tracker SDKs. | +| 7. Forced AI | Clear | No AI features at all. | +| 8. Locked format | Clear | One JSON file per note. Vault notes are ciphertext by design. | +| 9. No storage answer | Clear | Your phone first, then a folder in your own Google Drive. The server keeps metadata only. | + +Two flags are partial, and I won't pretend otherwise. Email sign-in and a full export feature are both on the roadmap. Until then, your Drive copy is the way out. + +## Why do so many notes apps raise these flags? + +**Look at who pays.** Advertising runs on tracking. Growth teams live on analytics dashboards. AI features run on your text. Each of those is a business decision before it's a notes app privacy one. + +That's why the third question, who pays, matters as much as encryption. A privacy focused notes app has to earn money without touching your data. Otherwise the flags return with the next funding round. Atomic Notes has no ads, no data deals, and no AI. Writing notes is free, and the people who use it fund the cloud sync that costs real money to run. + +## FAQ + +
+
+Is my notes app private? +

Check three things: where notes are stored, who holds the encryption key, and how the app makes money. If the company holds the key, can't name where your notes live, or earns from ads, your notes app privacy depends on its goodwill, not on its design.

+
+
+What is the biggest notes app privacy red flag? +

Vague encryption. "Encrypted" on its own can mean the company holds the keys and can read everything you write. Look for a plain statement that encryption is end-to-end, that only you hold the key, and what recovery looks like if that key is lost.

+
+
+How can I check if a notes app has trackers? +

Paste the app's name into Exodus Privacy, which reports the trackers and permissions found inside Android apps. Then search its privacy policy for the words analytics, advertising, and third parties. A privacy focused notes app comes back clean on both checks.

+
+
+Is a secure notes app the same as a private one? +

Not quite. Security keeps attackers out of your notes. Privacy is about what the company itself gathers, shares, and does with your data. A good app needs both: strong encryption, as little collection as possible, no trackers, and a way of making money that doesn't run on your notes.

+
+
+Can a notes app with cloud sync still be private? +

Yes. Privacy depends on what the server can see and who holds the key. Cloud sync can be private with end-to-end encryption, or when notes go to storage you own, like your own Google Drive. Atomic Notes does both, with the vault turned on.

+
+
+ +## Keep reading + +- [What is a local first notes app, and why does it matter?](/blog/what-is-a-local-first-notes-app) +- [The Atomic Notes privacy policy](/privacy), in plain English +- [Read the Atomic Notes code on GitHub](https://github.com/DevBehindYou/Atomic-Notes-App-V0.2) and check every claim on this page + +
+

Atomic Notes by DevBehindYou

+

Atomic Notes keeps every note on your phone, copies it to a folder in your Google Drive, and never shows ads, runs trackers, or sends your text to an AI model. Turn on the vault and even those copies are encrypted. Take the tour.

+
+ +## Sources + +- [Evernote's new privacy policy allows employees to read your notes. TechCrunch, December 14, 2016](https://techcrunch.com/2016/12/14/evernotes-new-privacy-policy-allows-employees-to-read-your-notes/) +- [Evernote reverses its privacy policy change. TechCrunch, December 16, 2016](https://techcrunch.com/2016/12/16/evernote-u-turn) +- [FTC enforcement action to bar GoodRx from sharing consumers' sensitive health info for advertising. FTC, February 2023](https://www.ftc.gov/news-events/news/press-releases/2023/02/ftc-enforcement-action-bar-goodrx-sharing-consumers-sensitive-health-info-advertising) +- [Provide information for Google Play's data safety section. Play Console Help](https://support.google.com/googleplay/android-developer/answer/10787469?hl=en) +- [Permissions on Android. Android Developers](https://developer.android.com/guide/topics/permissions/overview) +- [What Exodus Privacy does. Exodus Privacy](https://exodus-privacy.eu.org/en/page/what/) +- [Atomic Notes privacy policy](https://atomic-notes.devbehindyou.com/privacy) diff --git a/content/blog/what-is-a-local-first-notes-app.md b/content/blog/what-is-a-local-first-notes-app.md new file mode 100644 index 0000000..547d6e4 --- /dev/null +++ b/content/blog/what-is-a-local-first-notes-app.md @@ -0,0 +1,201 @@ +--- +title: "What Is a Local First Notes App and Why Does It Matter?" +slug: "what-is-a-local-first-notes-app" +description: "A local first notes app saves every note on your device first and treats the cloud as a copy. Here is what local-first software means, how it works, and its costs." +excerpt: "Your phone holds the real copy. The cloud holds a backup. What local-first software means, how it works, and the tradeoffs nobody mentions." +author: "ashutosh-sharma" +publishedAt: "2026-10-07" +updatedAt: "2026-10-07" +category: "local-first" +tags: ["local-first", "offline", "sync", "data-ownership", "architecture"] +featured: true +draft: false +coverImage: "/blog/what-is-a-local-first-notes-app/01-banner.png" +coverAlt: "Cover reading What Is a Local First Notes App, beside a phone showing the Atomic Notes notes grid and a card that says saved on device, 0 ms, offline" +canonical: "https://atomic-notes.devbehindyou.com/blog/what-is-a-local-first-notes-app" +keywords: "local first notes app, local-first software, local first meaning, Ink & Switch, offline notes, source of truth, sync engine, CRDT, data ownership" +readingTime: "10 min read" +--- + +
+

Quick answer

+

A local first notes app saves every note to your own device first and treats the cloud as a copy. Writing never waits for a server, works with no signal, and keeps your notes readable if the company behind the app disappears. Sync still exists. It just stops being the boss.

+
+ +Picture a train heading into a tunnel. You open your notes app, the signal drops, and a spinner sits where your idea should go. By the next stop, the thought is gone. That tiny failure is the best argument for a local first notes app. + +Most notes apps treat your phone as a window onto someone else's server. A local first notes app flips that order. The device holds the real copy, and the cloud follows behind. + +I build Atomic Notes, a local first notes app for Android, so I live with these details every day. This guide covers what the term means, how it differs from "works offline", what it costs, and how to test any app in five minutes. + +## What does local first mean? + +**The local first meaning, in one sentence: the copy of your data that matters most lives on your device, and every other copy is a replica.** The app reads and writes locally, then syncs when it can. + +The phrase comes from a 2019 essay by the research lab Ink & Switch, written by Martin Kleppmann, Adam Wiggins, Peter van Hardenberg, and Mark McGranaghan. They called the idea local-first software and set out seven ideals for it. Their argument was simple. Cloud apps made collaboration easy, but they quietly took ownership away from the people doing the work. + +For notes, the local first meaning gets very concrete: + +- The save button, or the autosave, finishes on your phone. It never waits on a round trip. +- Everything you do daily works in airplane mode, writing included. +- Your notes exist in a place you control, so the app can't hold them hostage. + +![Two lanes of four steps. A cloud-first app types, sends over the network, waits for the server to save, then shows saved. A local-first app types, saves on the phone, syncs later, and keeps a cloud copy.](/blog/what-is-a-local-first-notes-app/02-two-ways-to-save.png "Fig 1. A cloud-first save is a request. A local-first save is a device write, with sync as a separate job.") + +The difference looks small on a diagram. In daily use, it decides whether your notes app ever makes you wait. + +## Is a local first notes app just an offline mode? + +**No. Offline mode is a feature. Local first is an architecture.** An offline mode usually means a cache: recent notes kept around so you can read them when the network drops. A local first notes app keeps the authoritative copy on the device, so offline is simply the normal state with nothing to sync. Offline capability falls out of the design instead of being bolted on. That's the local first meaning in practice. + +That distinction shows up the moment something goes wrong. In a cache-based app, a new note written offline often sits in a temporary queue. If the app crashes or the cache clears, that note can vanish. In a local first notes app, the note is already saved. Sync is the only thing waiting. + +There's a middle ground called offline first. Writes queue on the device, but the server's copy still wins any disagreement when you reconnect. It's a big step up from a plain cache, yet the server remains the source of truth. + +![A four-step spectrum from the server owning your data to you owning it: cloud only, offline cache, offline first, and local first.](/blog/what-is-a-local-first-notes-app/03-source-of-truth-spectrum.png "Fig 2. Where the real copy lives decides who owns the experience.") + +Here's how the four models compare on the things you'll actually notice: + +| Model | Where the real copy lives | Write with no signal | If the company shuts down | +|---|---|---|---| +| Cloud only | The server | No | Notes may vanish with it | +| Offline cache | The server | Sometimes, if the cache holds | Notes may vanish with it | +| Offline first | The server, with a local queue | Yes, queued | Whatever was synced or exported | +| Local first | Your device | Yes, saved | Everything on your device stays | + +Try the difference yourself. Save a note, switch the network off, and save another. + +:::widget offline-simulator + +## Why does a local first notes app matter? + +**Because your notes are personal, long-lived, and written at awkward moments.** Those three facts favor a local first notes app over a cloud-first one every time. + +### Your notes stay fast + +A local first notes app writes in milliseconds. A cloud write takes a round trip, and round trips stretch on weak signal. The Ink & Switch essay calls this ideal "no spinners." Once you've used an app that never shows one, the others feel broken. + +### Your notes survive other people's outages + +On October 20, 2025, a failure in Amazon Web Services' US-East-1 region took down DynamoDB and cascaded into many of the apps built on top of it. Recovery took most of a day. Cloud dependency doesn't just mean your own connection. It means every server between you and your notes. A local first notes app keeps working through all of it, because nothing it needs to show you lives far away. + +### Your notes outlive the app + +Companies pivot, get acquired, and shut down. When a cloud-only notes service closes, its users get an export window, if they're lucky. Notes stored on your device and in storage you control don't depend on anyone's roadmap. That's user control in its plainest form. The essay calls this longevity ideal "the long now," and it matters more for notes than for almost any other kind of data, because notes are where you keep the things you meant to remember for years. + +### Privacy gets easier + +When the device holds the real copy, the server doesn't need to read your notes to show them to you. That makes end-to-end encryption a natural fit instead of a bolt-on. It also shrinks what a breach can expose. Local-first software doesn't guarantee privacy on its own, but it removes the main excuse for collecting your content. + +## The seven ideals of local-first software + +The essay's seven ideals are the best checklist I know for judging any local first notes app. Use them as questions, not marketing badges. + +![Seven numbered cards: no spinners, multi-device, network optional, collaboration, the long now, privacy by default, and you own it.](/blog/what-is-a-local-first-notes-app/04-seven-ideals.png "Fig 3. The seven ideals from Ink & Switch's 2019 essay \"Local-first software\".") + +No app scores a perfect seven, including mine. Collaboration is the hardest ideal for a small team, because real-time co-editing needs a CRDT or a similar conflict-free data structure. Atomic Notes is a personal notes app, so it skips that ideal on purpose and says so. + +Score the app you use today against what a local first notes app should do. You can reveal how Atomic Notes answers, gaps included. + +:::widget local-first-scorecard + +## How a local first notes app actually works + +**Three parts do the work: on-device storage, a sync engine, and a cloud copy.** The interesting engineering is in how they talk to each other when the network is unreliable. + +Here's how Atomic Notes splits those jobs: + +1. **Your phone holds the real copy.** Every note saves to on-device storage (Hive) as you type, online or not. Search, edit, and delete all run against that local copy. +2. **A sync server coordinates, and keeps metadata only.** It tracks note IDs, timestamps, flags, and versions so your devices agree. It never stores your note titles or text. +3. **Your own Google Drive holds the cloud copy.** Every note lands in a My-Atomic-Notes folder as its own `.atomic` file. The app uses Google's narrow `drive.file` scope, so it can only see files it created. + +![A phone showing the Atomic Notes storage report, beside three cards: your phone holds the real copy, the sync server keeps metadata only, and your Google Drive holds one file per note. An optional vault keeps cloud copies as ciphertext.](/blog/what-is-a-local-first-notes-app/05-atomic-notes-layers.png "Fig 4. Atomic Notes: phone first, Google Drive second, a metadata-only server in between.") + +One honest detail. Unless you turn on the optional vault, note text is plain JSON in your Drive and passes through the sync server on its way there, without being stored. Turn the vault on and your phone encrypts every note with AES-256-GCM before upload, so the server and Google only ever hold ciphertext. + +Here's what "the save is the device write" looks like in real code. This is the save path from the Atomic Notes source, trimmed, with comments added: + +

lib/database/notes_repository.dart · Atomic Notes 2.03.5

+ +```dart +@override +Future save(Note note) async { + note.touch(); // stamp the edit time and mark the note "dirty" + note.settleDirty(); // an edit undone back to the synced copy needs no upload + _notes[note.id] = note; + await _persist(note.id); // the save finishes here, on the device + notifyListeners(); // the screen updates right away + if (note.dirty) _scheduleAutoSync(); // the network comes later, if at all +} +``` + +Notice what isn't there: no network call, no spinner, no error path for "server unreachable." The sync engine picks up dirty notes about eight seconds after you stop typing, on app resume, and when the network comes back. If two devices edit the same note while offline, the server rejects the out-of-date edit, and your phone saves it as a separate conflict copy. Nothing gets overwritten. + +## What does local-first software cost you? + +**Local first moves the hard work from the server into the sync engine.** That trade is worth it for notes, but it isn't free, and an honest local first notes app should tell you where it bends. + +![A table of four tradeoffs: conflicts, a lost phone, harder engineering, and collaboration, with how Atomic Notes handles each.](/blog/what-is-a-local-first-notes-app/06-local-first-tradeoffs.png "Fig 5. The tradeoffs of local first, and how one app handles each.") + +- **Conflicts.** Two devices can change the same note offline. The app needs a rule for that moment, and "last write wins" quietly throws work away. +- **A lost phone.** Edits that never synced exist on one device only. Frequent automatic sync shrinks that window, but it can't close it entirely. +- **Harder engineering.** Retries, queues, versions, and replay-safe requests take real work. Some apps call themselves local-first without doing it. +- **Collaboration.** Real-time shared editing is still the frontier of local-first software, and most personal notes apps, mine included, don't offer it yet. + +My opinion after building one: for personal notes, these costs are small next to a notes app that freezes on a train. For a team wiki, the math is different. + +## How to tell if your notes app is really local first + +You don't need to read code. Give any app five minutes and these four checks: + +1. **The airplane test.** Turn on airplane mode, force-close the app, and reopen it. Create a note, edit an old one, search, and delete. Everything should work, with no error and no spinner. +2. **The restart test.** Still offline, restart your phone. Your offline edits should all still be there. +3. **The reconnect test.** Turn the network back on. Your offline changes should sync on their own, without a manual retry. +4. **The exit test.** Find out where your notes physically live and in what format. If the only answer is "our servers," the app is cloud first, whatever its marketing says. + +If an app passes all four, you're using a local first notes app. If it fails the first one, you're using a web page with an icon. + +Local first is also one of the strongest privacy signals a notes app can send. For the rest of that picture, read [notes app privacy: 9 red flags to check](/blog/notes-app-privacy-red-flags). + +## FAQ + +
+
+Is a local first notes app the same as an offline notes app? +

Not exactly. An offline notes app may only cache recent notes and queue new ones for the server. A local first notes app keeps the real copy on your device, so offline isn't a special mode. It's the normal state, and sync catches up later.

+
+
+Can a local first notes app still sync between devices? +

Yes. Local first doesn't mean local only. The device holds the authoritative copy, and a sync engine copies changes to the cloud and your other devices. Atomic Notes syncs each note to a folder in your own Google Drive.

+
+
+What happens to my notes if a local first app shuts down? +

Your notes stay on your device, and any synced copies stay wherever they were stored. If that storage is yours, like your own Google Drive, the files remain readable. A cloud-only app can take your notes with it when it closes.

+
+
+Is local-first software more private? +

It makes privacy easier, not automatic. When the device holds the real copy, the server doesn't need to read your notes, so end-to-end encryption fits naturally. You still need to check for trackers, ads, AI processing, and what the sync server keeps.

+
+
+Does Atomic Notes work without internet? +

Yes. Writing, editing, searching, and deleting all work offline, because every note saves to on-device storage first. Sync to your Google Drive runs when a connection returns. Signing in for the first time does need a connection.

+
+
+ +## Keep reading + +- [Notes app privacy: 9 red flags to check before you trust one](/blog/notes-app-privacy-red-flags) +- [How Atomic Notes works, on the home page](/) +- [The Atomic Notes source code on GitHub](https://github.com/DevBehindYou/Atomic-Notes-App-V0.2), public to read and verify + +
+

Atomic Notes by DevBehindYou

+

A source-available, local first notes app for Android. Notes save on your phone first and sync to your own Google Drive. No ads, no trackers, no AI, and an optional end-to-end vault. See how it works.

+
+ +## Sources + +- [Local-first software: you own your data, in spite of the cloud. Ink & Switch, 2019](https://www.inkandswitch.com/essay/local-first/) +- [Summary of the Amazon DynamoDB service disruption in the US-East-1 Region. Amazon Web Services, October 2025](https://aws.amazon.com/message/101925) +- [Atomic Notes source code and README. GitHub](https://github.com/DevBehindYou/Atomic-Notes-App-V0.2) +- [Atomic Notes privacy policy](https://atomic-notes.devbehindyou.com/privacy) diff --git a/package-lock.json b/package-lock.json index 904e4df..5af2162 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,9 +63,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", - "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", "cpu": [ "arm64" ], @@ -81,13 +81,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.3.3" + "@img/sharp-libvips-darwin-arm64": "1.3.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", - "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", "cpu": [ "x64" ], @@ -103,20 +103,20 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.3.3" + "@img/sharp-libvips-darwin-x64": "1.3.4" } }, "node_modules/@img/sharp-freebsd-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", - "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", "license": "Apache-2.0", "optional": true, "os": [ "freebsd" ], "dependencies": { - "@img/sharp-wasm32": "0.35.4" + "@img/sharp-wasm32": "0.35.5" }, "engines": { "node": ">=20.9.0" @@ -126,9 +126,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", - "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", "cpu": [ "arm64" ], @@ -142,9 +142,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", - "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", "cpu": [ "x64" ], @@ -158,9 +158,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", - "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", "cpu": [ "arm" ], @@ -177,9 +177,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", - "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", "cpu": [ "arm64" ], @@ -196,9 +196,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", - "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", "cpu": [ "ppc64" ], @@ -215,9 +215,9 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", - "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", "cpu": [ "riscv64" ], @@ -234,9 +234,9 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", - "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", "cpu": [ "s390x" ], @@ -253,9 +253,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", - "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", "cpu": [ "x64" ], @@ -272,9 +272,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", - "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", "cpu": [ "arm64" ], @@ -291,9 +291,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", - "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", "cpu": [ "x64" ], @@ -310,9 +310,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", - "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", "cpu": [ "arm" ], @@ -331,13 +331,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.3.3" + "@img/sharp-libvips-linux-arm": "1.3.4" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", - "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", "cpu": [ "arm64" ], @@ -356,13 +356,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.3.3" + "@img/sharp-libvips-linux-arm64": "1.3.4" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", - "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", "cpu": [ "ppc64" ], @@ -381,13 +381,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.3.3" + "@img/sharp-libvips-linux-ppc64": "1.3.4" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", - "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", "cpu": [ "riscv64" ], @@ -406,13 +406,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.3.3" + "@img/sharp-libvips-linux-riscv64": "1.3.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", - "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", "cpu": [ "s390x" ], @@ -431,13 +431,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.3.3" + "@img/sharp-libvips-linux-s390x": "1.3.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", - "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", "cpu": [ "x64" ], @@ -456,13 +456,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.3.3" + "@img/sharp-libvips-linux-x64": "1.3.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", - "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", "cpu": [ "arm64" ], @@ -481,13 +481,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", - "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", "cpu": [ "x64" ], @@ -506,13 +506,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.3.3" + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", - "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { @@ -526,16 +526,16 @@ } }, "node_modules/@img/sharp-webcontainers-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", - "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", "cpu": [ "wasm32" ], "license": "Apache-2.0", "optional": true, "dependencies": { - "@img/sharp-wasm32": "0.35.4" + "@img/sharp-wasm32": "0.35.5" }, "engines": { "node": ">=20.9.0" @@ -545,9 +545,9 @@ } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", - "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", "cpu": [ "arm64" ], @@ -564,9 +564,9 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", - "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", "cpu": [ "ia32" ], @@ -583,9 +583,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", - "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", "cpu": [ "x64" ], @@ -1834,9 +1834,9 @@ } }, "node_modules/sharp": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", - "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", "license": "Apache-2.0", "optional": true, "dependencies": { @@ -1851,31 +1851,31 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.35.4", - "@img/sharp-darwin-x64": "0.35.4", - "@img/sharp-freebsd-wasm32": "0.35.4", - "@img/sharp-libvips-darwin-arm64": "1.3.3", - "@img/sharp-libvips-darwin-x64": "1.3.3", - "@img/sharp-libvips-linux-arm": "1.3.3", - "@img/sharp-libvips-linux-arm64": "1.3.3", - "@img/sharp-libvips-linux-ppc64": "1.3.3", - "@img/sharp-libvips-linux-riscv64": "1.3.3", - "@img/sharp-libvips-linux-s390x": "1.3.3", - "@img/sharp-libvips-linux-x64": "1.3.3", - "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", - "@img/sharp-libvips-linuxmusl-x64": "1.3.3", - "@img/sharp-linux-arm": "0.35.4", - "@img/sharp-linux-arm64": "0.35.4", - "@img/sharp-linux-ppc64": "0.35.4", - "@img/sharp-linux-riscv64": "0.35.4", - "@img/sharp-linux-s390x": "0.35.4", - "@img/sharp-linux-x64": "0.35.4", - "@img/sharp-linuxmusl-arm64": "0.35.4", - "@img/sharp-linuxmusl-x64": "0.35.4", - "@img/sharp-webcontainers-wasm32": "0.35.4", - "@img/sharp-win32-arm64": "0.35.4", - "@img/sharp-win32-ia32": "0.35.4", - "@img/sharp-win32-x64": "0.35.4" + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" }, "peerDependenciesMeta": { "@types/node": { diff --git a/public/blog/notes-app-privacy-red-flags/01-banner.png b/public/blog/notes-app-privacy-red-flags/01-banner.png new file mode 100644 index 0000000..c251a2e Binary files /dev/null and b/public/blog/notes-app-privacy-red-flags/01-banner.png differ diff --git a/public/blog/notes-app-privacy-red-flags/02-nine-red-flags.png b/public/blog/notes-app-privacy-red-flags/02-nine-red-flags.png new file mode 100644 index 0000000..d3deea1 Binary files /dev/null and b/public/blog/notes-app-privacy-red-flags/02-nine-red-flags.png differ diff --git a/public/blog/notes-app-privacy-red-flags/03-where-notes-travel.png b/public/blog/notes-app-privacy-red-flags/03-where-notes-travel.png new file mode 100644 index 0000000..85e4fc4 Binary files /dev/null and b/public/blog/notes-app-privacy-red-flags/03-where-notes-travel.png differ diff --git a/public/blog/notes-app-privacy-red-flags/04-evidence-ladder.png b/public/blog/notes-app-privacy-red-flags/04-evidence-ladder.png new file mode 100644 index 0000000..747f04a Binary files /dev/null and b/public/blog/notes-app-privacy-red-flags/04-evidence-ladder.png differ diff --git a/public/blog/notes-app-privacy-red-flags/05-permissions-check.png b/public/blog/notes-app-privacy-red-flags/05-permissions-check.png new file mode 100644 index 0000000..6c5a9f6 Binary files /dev/null and b/public/blog/notes-app-privacy-red-flags/05-permissions-check.png differ diff --git a/public/blog/notes-app-privacy-red-flags/06-atomic-notes-scorecard.png b/public/blog/notes-app-privacy-red-flags/06-atomic-notes-scorecard.png new file mode 100644 index 0000000..48b92ce Binary files /dev/null and b/public/blog/notes-app-privacy-red-flags/06-atomic-notes-scorecard.png differ diff --git a/public/blog/what-is-a-local-first-notes-app/01-banner.png b/public/blog/what-is-a-local-first-notes-app/01-banner.png new file mode 100644 index 0000000..28a3904 Binary files /dev/null and b/public/blog/what-is-a-local-first-notes-app/01-banner.png differ diff --git a/public/blog/what-is-a-local-first-notes-app/02-two-ways-to-save.png b/public/blog/what-is-a-local-first-notes-app/02-two-ways-to-save.png new file mode 100644 index 0000000..f7f2633 Binary files /dev/null and b/public/blog/what-is-a-local-first-notes-app/02-two-ways-to-save.png differ diff --git a/public/blog/what-is-a-local-first-notes-app/03-source-of-truth-spectrum.png b/public/blog/what-is-a-local-first-notes-app/03-source-of-truth-spectrum.png new file mode 100644 index 0000000..f9b0680 Binary files /dev/null and b/public/blog/what-is-a-local-first-notes-app/03-source-of-truth-spectrum.png differ diff --git a/public/blog/what-is-a-local-first-notes-app/04-seven-ideals.png b/public/blog/what-is-a-local-first-notes-app/04-seven-ideals.png new file mode 100644 index 0000000..4c5eb24 Binary files /dev/null and b/public/blog/what-is-a-local-first-notes-app/04-seven-ideals.png differ diff --git a/public/blog/what-is-a-local-first-notes-app/05-atomic-notes-layers.png b/public/blog/what-is-a-local-first-notes-app/05-atomic-notes-layers.png new file mode 100644 index 0000000..e1e149a Binary files /dev/null and b/public/blog/what-is-a-local-first-notes-app/05-atomic-notes-layers.png differ diff --git a/public/blog/what-is-a-local-first-notes-app/06-local-first-tradeoffs.png b/public/blog/what-is-a-local-first-notes-app/06-local-first-tradeoffs.png new file mode 100644 index 0000000..70eb1c6 Binary files /dev/null and b/public/blog/what-is-a-local-first-notes-app/06-local-first-tradeoffs.png differ diff --git a/public/llms.txt b/public/llms.txt index 9f1f3fb..1879c30 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -22,6 +22,8 @@ Key facts: ## Blog posts +- [What Is a Local First Notes App and Why Does It Matter?](https://atomic-notes.devbehindyou.com/blog/what-is-a-local-first-notes-app): local-first software defined, offline first vs local first, the seven Ink & Switch ideals, how Atomic Notes syncs, and the tradeoffs +- [Notes App Privacy in 2026: 9 Red Flags to Check First](https://atomic-notes.devbehindyou.com/blog/notes-app-privacy-red-flags): nine checks for any notes app (accounts, export, encryption keys, permissions, ads, trackers, AI, formats, storage) and how Atomic Notes scores - [Atomic Notes v1.18.2 Demo Build](https://atomic-notes.devbehindyou.com/blog/atomic-notes-v1-18-2): Atomic Energy, in-app email flows, notification center, offline launch fix - [RSS feed](https://atomic-notes.devbehindyou.com/feed.xml) and [sitemap](https://atomic-notes.devbehindyou.com/sitemap.xml): every published page and post diff --git a/src/app/blog/[slug]/page.tsx b/src/app/blog/[slug]/page.tsx index 2c7fb52..fa1fce6 100644 --- a/src/app/blog/[slug]/page.tsx +++ b/src/app/blog/[slug]/page.tsx @@ -6,6 +6,8 @@ import { DEVELOPER_URL, SITE_URL } from "@/lib/site"; import { FEED_ALTERNATE, SITE_NAME, TWITTER_HANDLE, absoluteUrl, canonicalUrl } from "@/lib/seo"; import { SiteNav } from "@/components/SiteNav"; import { SiteFooter } from "@/components/SiteFooter"; +import { BlogWidget } from "@/components/blog/BlogWidget"; +import { ReadingProgress } from "@/components/blog/ReadingProgress"; export const dynamicParams = false; // unknown slugs -> 404 @@ -112,6 +114,7 @@ export default async function ArticlePage({ params }: { params: Promise<{ slug: return (
+