Building in public, one commit at a time.

GitScribe is an offline-first Markdown note editor with Git at its core. Follow along as we build it - every feature, every fix, and every lesson we learn along the way.

Priority support & release workflows

We spent this cycle preparing GitScribe for its upcoming beta launch by setting up dynamic support desks and strict publishing checklists. PRO users now see a premium "Priority" badge in the settings page. Composed emails are pre-populated with system diagnostic headers using percent-encoding so that spaces display correctly on all native mail clients.

We also consolidated and automated our release hygiene. A single terminal command now bumps the version and synchronizes configurations across both the Flutter app and the native Rust engine Cargo files concurrently. We merged all scattered setup, code signing, and testing guidelines into a master publishing manual with a strict developer checklist to make every future store release seamless and bug-free.

v0.6.1
// What shipped
  • Dynamic "Feedback & Priority Support" settings tile with custom visual badge for PRO accounts
  • Prefilled system diagnostics (App Version, Platform, and Support Tier) in support emails
  • Single-command version bumping script (bump_version.dart) syncing Flutter and Rust engine crates
  • Added a Master Release & Publishing Handbook with pre-release checklist

Pressure testing

This cycle is all about pressure-testing the foundation with three separate audits - performance, security, and accessibility. It's rounded out with a top-to-bottom rewrite of the Rust FFI layer for the modern flutter_rust_bridge v2 idioms.

On the Flutter side, the Riverpod 3.x graph had quietly grown bigger than we were tracking, and a lot of widgets were watching whole notifiers when they only cared about one field. We swept through with fine-grained .select(...) selectors wherever it mattered - sync indicators, the editor's status badges, the lock screen - and the difference in profiler frames is honest. The pending-changes provider and the current-branch provider now also ref.watch the active repo path, so they reset automatically when users switch repos instead of leaking stale data into the new session.

On the Rust side, we threw out every catch_unwind in favor of typed Result<T, GitError> errors via thiserror, with a scrub_credentials pass that strips tokens out of any error message before it crosses the FFI boundary. The SSL init path is now guarded by OnceLock so the unsafe { set_var } calls only ever run on a single thread, exactly once per process. Clone progress events are throttled to ~10 Hz and abort libgit2 immediately if the Dart stream closes - no more multi-GB downloads continuing in the background after users navigate away. And git_checkout_branch now refuses to clobber a dirty working tree unless the caller explicitly passes force = true.

Security got the biggest list. We pulled git service client IDs out of source in favor of String.fromEnvironment defines, swapped OAuth from the older plain-PKCE method to S256 with full state validation, and migrated subscription state out of SharedPreferences into hardened FlutterSecureStorage. Subscription receipts are now HMAC-signed in the local cache so a tampered file fails closed instead of silently unlocking Pro subscription. We also closed a path-traversal hole in the file repository: any path containing .. or pointing outside the repo root is rejected before it ever reaches the filesystem.

Accessibility was less dramatic but probably the most important for real users. Every GestureDetector and bare IconButton now lives inside a Semantics widget with a real label, the lock screen wraps everything beneath it in ExcludeSemantics so screen readers can't read through the lock, and the editor's font size respects the system text scaler with a sensible 2.0× cap. Status transitions like "Saved" and "Synced" get announced exactly once via SemanticsService.announce instead of being shouted by a noisy liveRegion on every animation tick.

The biggest under-the-hood change is a process-wide repository cache. Every Git operation used to re-open the libgit2 Repository from disk, which on Android meant walking .git/, parsing config, and mapping the index on every single call. The cache keeps one Arc<Mutex<Repository>> per canonicalized repo path for the lifetime of the process. Same repo serializes (which is libgit2's contract anyway), different repos run fully in parallel. The repository list loads visibly faster - back-to-back status + log + remoteHeadSha calls per repo now share a single handle instead of paying the open cost three times. We also exposed git_show_file_bytes returning a Vec<u8> that maps zero-copy onto Dart's Uint8List, so binary previews (PNG, PDF, anything non-text) can land in the next release without a second FFI round-trip.

v0.6.0
// What shipped
  • Process-wide libgit2 repository cache - 20-30% faster status / log / branch reads
  • Typed Rust errors (thiserror) with credential scrubbing across the FFI boundary
  • git_show_file_bytes for upcoming binary file preview (zero-copy Vec<u8>Uint8List)
  • git_checkout_branch refuses to overwrite a dirty working tree by default
  • Clone progress throttled to 10 Hz and aborts libgit2 the moment the Dart stream closes
  • OnceLock-guarded SSL init makes the unavoidable unsafe blocks provably one-shot
  • GitLab OAuth migrated to PKCE S256 with full state (CSRF) validation
  • Subscription state moved to hardened FlutterSecureStorage with HMAC-signed cache
  • OAuth client IDs moved out of source into --dart-define build-time injection
  • Path-traversal guard in the file repository - .. and absolute paths rejected
  • Fine-grained Riverpod .select(...) selectors across editor, sync, lock, and home screens
  • Search debounce + cooperative yielding - no more UI freezes on large repos
  • Semantics labels on every interactive widget; ExcludeSemantics under the lock overlay
  • Editor font size respects system text scaler (capped at 2.0x) for accessibility

Performance optimizations

We spent this cycle obsessed with optimizations. The app was working well, but as repositories got larger, we started seeing some jitter. We went through the entire application to isolate UI rebuilds, replacing heavy layouts with streamlined custom painters, and swapping out eagerly loaded columns for lazy list builders. The result is a buttery smooth experience even with hundreds of branches or files.

GitScribe now has a "Bottom Sheet First" design policy. Instead of disruptive modal dialogs or pushing new screens for simple settings, all contextual inputs now slide up elegantly from the bottom. Destructive operations (like discarding changes or deleting files) now use non-blocking undo snackbars instead of scary confirmation dialogues.

We also shipped an AI-generated commit messages feature using Gemini Nano (on-device AI). It analyzes the exact diff of the pending changes and proposes a clean, conventional commit message with one tap. And since everything is processed locally, nothing ever leaves users' devices.

v0.5.0
// What shipped
  • Complete UI audit enforcing a "Bottom Sheet First" mobile design policy
  • Major performance boost for the commit timeline using custom painters
  • Lazy loading implemented across the entire application
  • External file system changes are now reliably detected on app resume
  • Destructive actions replaced with safe, non-blocking undo snackbars
  • AI commit messages (Gemini Nano), analyzing diffs instantly

Per-file sync & connectivity

The sync tab got a big upgrade. Each file now shows its individual sync status - synced, modified, or pending sync. That last one was tricky: "pending sync" means a user has committed locally but hasn't pushed yet, so the file blinks amber to remind them there's work waiting to go upstream.

The sync state tracker now knows about lastSavedAt, lastSyncedAt, and hasUnpushedCommits separately, so the UI can show users exactly where their data is in the pipeline. The sync tab surfaces those last-saved and last-synced timestamps right at the top.

Connectivity got more visible too. The offline banner drops in from the top the moment the network goes away, and pull-to-refresh is wired up across the file browser and sync screens so users can always nudge things along by hand.

v0.4.4
// What shipped
  • Per-file sync status: synced, modified, pending sync (blinking amber)
  • Sync state tracks lastSavedAt, lastSyncedAt, and hasUnpushedCommits
  • Sync tab shows "Last Saved" and "Last Synced" timestamps
  • Pull-to-refresh across file browser and sync screens
  • Offline banner drops in on connectivity changes
  • History screen no longer shows loading skeleton on pull-to-refresh

Multi-repo management

GitScribe now supports multiple repositories. Users can add as many repos as they want, switch between them, and each one tracks its own sync state independently. The repo list shows each one's last sync time and status, and switching is instant because we keep the file trees cached.

Setting up a new repo is a lot less friction too. A user's Git profile is read once at auth time and pre-fills the Git author name and email, so users are not staring at an empty form trying to remember what email is on their commits.

The bundled sample vault used to commit with empty author strings if users poked at it before configuring anything - it now uses a sensible default author so those first commits are actually attributable.

v0.4.3
// What shipped
  • Add, switch, list, and delete repositories from settings
  • Each repo tracks its own sync state independently
  • Cached file trees for instant repo switching
  • Per-repo last-sync timestamps in the repo list
  • GitHub profile auto-populates Git author name and email
  • Sample vault uses a sensible default author (no empty-author commits)

Files, Search & Editor

Three areas got attention this week: the file browser, search, and the editor itself.

On the files side, the old single FAB grew up into a speed dial; tap the + and it fans out into options for new file and new folder. File selection now has long-press to enter selection mode, with the app bar transforming into a contextual action bar for rename and delete.

Search is a proper feature now. A Material 3 search bar gives instant results across the content of every file in user's repo, and tapping it drops the user straight into the editor at the right spot.

The editor now features keyboard shortcuts - Ctrl+B for bold, Ctrl+I for italic, Ctrl+K for links - plus YAML frontmatter support and inline rendering of local images. We switched the Markdown preview to flutter_markdown_plus with Material 3 styling after the original package was discontinued. External file changes (like editing from a computer and syncing) get detected and trigger a reload prompt.

v0.4.2
// What shipped
  • Speed dial FAB with animated + → x rotation and scale transitions
  • Contextual app bar with long-press multi-select
  • Shared widgets: EmptyStateWidget, InlineErrorBanner, AppDialogs
  • Material 3 SearchBar at /search with instant full-text results
  • Full-text search across the content of all files in the repository
  • Keyboard shortcuts: Ctrl+B (bold), Ctrl+I (italic), Ctrl+K (link)
  • YAML frontmatter support in editor and preview
  • Local image preview renders inline in Markdown
  • External change detection with reload prompt

Adaptive navigation & biometric lock

Restructured the entire navigation model. GitScribe now has four proper tabs: Files, Repository, Sync, and Settings. The navigation adapts to screen size; phones get a Material 3 bottom navigation bar, and tablets or foldables get a Material 3 navigation rail on the side.

We also added biometric lock, with a configurable grace period so it doesn't nag users if they just switched apps for a second. Both enabling and disabling the option requires a biometric confirmation, keeping users' notes and repositories safe.

Privacy policy, along with Terms of Use can now be accessed in Settings. A couple other configurable options, such as Auto-Sync intervals, and Hidden Files toggles, have also been added.

v0.4.1
// What shipped
  • 4-tab navigation: Files, Repository, Sync, Settings
  • Adaptive layout: NavigationBar (compact) + NavigationRail (medium+)
  • Biometric lock with configurable grace period (immediate to 30 minutes)
  • Settings now includes Privacy Policy and Terms of Use links
  • Auto-sync with configurable intervals (5 min to 1 hour)
  • Offline-aware sync: commits locally when offline, full sync when back online

Phase 2 - Motion & Material 3 overhaul

Phase 2 was supposed to be "just polish" but it ended up being bigger than Phase 1. Turns out the gap between "it works" and "it feels good" is enormous so we decided to split it into multiple sprints instead.

For this sprint, we completely reworked how GitScribe feels. Every animation now uses spring physics instead of linear easing - which sounds like a nerdy distinction but the difference is night and day. Things feel alive now. Bouncy where they should be, crisp where they shouldn't.

The whole motion system lives in a single AppMotion class with three spring profiles: snappy for taps and toggles, standard for most transitions, and gentle for ambient elements. Everything respects the system's reduce-motion preference too.

Also did a full Material 3 shape and color audit. Replaced every hardcoded Colors.black and Colors.grey with proper theme tokens. Dynamic color is now enabled on Android 12+ so it picks up user's wallpaper's palette. Turns out that feature was disabled this whole time.

v0.4.0
// What shipped
  • Spring-based animation system with snappy, standard, and gentle profiles
  • Staggered file list animations with slide, scale, and fade per item
  • Long-press micro-interaction with haptic feedback
  • Dynamic color enabled on Android 12+
  • Full Material 3 shape token audit - AppRadius aligned to MD3 spec
  • All hardcoded colors replaced with semantic colorScheme tokens
  • Accessibility: reduce-motion support across all animated widgets
  • Semantics labels added to status dots, file cards, sync indicator, and nav bar

File history & version restore

One of the things that makes Git-backed notes genuinely useful: the ability to restore any version. We've built a file history screen that shows every commit that touched a given file as a timeline, and whether it's been synced to the remote or is still local-only.

Tapping a commit opens a diff view in a bottom sheet with a line-by-line comparison using an LCS diff algorithm implemented in Dart. Green for additions, red for deletions. There's a "Restore" button that reverts the file to that exact version and pops the user back into the editor.

The Git operations are all in Rust: git_log, git_show_file, and git_remote_head_sha to determine the synced/local badge. Having the heavy lifting in native code means the history screen loads instantly even for files with hundreds of commits.

v0.3.0
// What shipped
  • File history timeline with synced/local badges per commit
  • Diff bottom sheet with LCS-based line comparison
  • One-tap restore to any previous version
  • Rust-powered git_log, git_show_file, git_remote_head_sha
  • Editor reloads after history restore

Phase 1 - the core loop

The core loop is complete: authenticate → clone a repo → browse files → edit Markdown → sync back to remote. We've been dogfooding it for our own project notes and it's already changed how we take notes. Having real version history on our phones without having to think about it is genuinely useful.

Authentication supports GitHub Device Flow and a manual PAT option for any Git provider. GitLab OAuth with PKCE is implemented but the deep link callback needs App Links verification on a real device.

The sync engine is something we're particularly proud of. It runs through a SyncOrchestrator that handles the full pull → merge → commit → push cycle. When offline, it gracefully degrades to commit-only mode and syncs when connectivity returns. All Git operations are serialized through a queue to prevent corruption.

v0.2.0
// What shipped
  • GitHub Device Flow authentication
  • Clone with real-time progress streaming from Rust FFI
  • File browser with folder navigation, breadcrumbs, and hidden directory filtering
  • Markdown editor with syntax highlighting, auto-save (500ms debounce), and atomic writes
  • Sync engine: pull → merge → commit → push with offline fallback
  • Git operations serialized through a single-writer queue
  • Token refresh for GitLab, re-auth detection for GitHub

The foundation

Before building any features, we decided to spend two weeks on infrastructure. We decided on Flutter with Rust via flutter_rust_bridge for the Git operations. Clean architecture with core, domain, data, and presentation layers. Riverpod 3.x for state management. go_router for navigation. The whole thing compiles to a single APK with native Rust code embedded.

The Git service wraps git2 with catch_unwind on every FFI export so a panic in Rust doesn't take down the Flutter app. Ten functions exposed to Dart: clone, pull, push, commit, status, add, log, show file, remote head SHA, and branch operations. All HTTPS-only - SSH support would double the complexity for a use case most phone users don't need.

v0.1.0
// What shipped
  • Flutter + Rust (flutter_rust_bridge 2.12.0) project scaffolding
  • Clean Architecture: core / domain / data / presentation layers
  • Riverpod 3.x state management with code generation
  • 10 Rust FFI functions with catch_unwind safety
  • Git operation queue with single-writer serialization and cancellation
  • Migration service with version-based runners
  • Material 3 theme scaffolding with dark/light/system support
  • Edge-to-edge display, ProGuard config, crash zone error boundary

The idea

There are note apps that sync via Git but they either treat Git as an afterthought, don't work offline properly, or bury the version history so deep users forget it's there. We wanted something that puts Git front and center: every file in a real repo, every change is a real commit, and users can see the full history of any file with one tap.

And that's how GitScribe was born. It's a Markdown editor that clones a user's repo, lets users browse and edit files, and syncs everything back. Offline-first, Material 3 design, the Git operations run in Rust because life's too short for slow clones on mobile. Is this a good idea? We have absolutely no idea. But we're going to build it anyway and write about the process here.