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.

Lifetime PRO billing & the road to beta

In this release, we officially retired recurring subscriptions in favor of a straightforward $19.99 Lifetime PRO license ("Pay once, own forever").

Under the hood, we hardened the purchase verification and entitlement pipeline. On both initial in-app purchase and restore flows via Google Play, SubscriptionServiceImpl now extracts and parses the store transaction timestamp (purchaseDetails.transactionDate) into purchasedAt. This timestamp is HMAC-SHA256 cryptographically signed using a device-bound secret key and persisted securely in FlutterSecureStorage (Android Keystore). Capturing purchase timestamps from day one guarantees historical provenance and preserves future architectural flexibility for time-boxed update models.

With everything now finalized, v0.8.0 marks our final foundational release before Beta. Our engineering focus now pivots entirely to shipping the closed and open Beta tracks on Google Play: polishing first-run onboarding, finalizing crash telemetry with strict privacy guardrails, and rolling out access to our early waitlist community.

v0.8.0
// What shipped
  • Replaced recurring subscription model with a single $19.99 one-time Lifetime PRO purchase
  • Day-one store transaction timestamp (purchasedAt) capture across purchase and restore flows
  • HMAC-SHA256 cryptographic signing for tamper-proof local license verification in Android Keystore
  • Streamlined ProUpgradeSheet with clear one-tap lifetime purchase button and value propositions
  • Settings screen updated with "Lifetime License Active" status badge and one-tap restore
  • Finalized core milestone checklist and initiated Google Play Beta release preparation

Comprehensive UI overhaul & Material 3 polish

As we approach our public Beta release, we took a step back to elevate the entire visual and interactive feel of GitScribe. We completed an extensive design sprint across the dashboard, settings architecture, and editor workspace, delivering a refined, modern Material 3 aesthetic with crisp micro-interactions and high-contrast typography.

The repository list received a complete visual overhaul. Active repositories now render with prominent "Hero" styling, including distinct avatar treatments and smooth AnimatedCrossFade commit snippets displaying the latest commit message alongside monospace Git SHA badges. Sync status indicators have been upgraded to semantic pill badges featuring dedicated cloud status icons (cloud_done, cloud_upload, and warning_amber), giving users unambiguous clarity on their notebook state at a single glance.

We also reimagined the settings experience. The subscription section now features a premium gold-accented status card with lifetime license indicators and frictionless one-tap upgrade affordances. All settings sections (About, Git, Diagnostics) were unified under structured SettingsGroupCard layouts with clear external link indicators and dynamic priority support email routing ([email protected]) for PRO users.

v0.7.0
// What shipped
  • Hero repository card design with animated commit snippet expansion and Git SHA badges
  • Upgraded sync status pill badges with semantic Material cloud status icons
  • Redesigned gold-accented SubscriptionSettingsSection with lifetime license badges
  • Unified SettingsGroupCard architecture across About, Git, and Diagnostics views
  • Dynamic priority support routing ([email protected]) with prefilled diagnostic headers
  • Ergonomic editor formatting toolbar adjustments and enhanced accessibility semantics

Demand-driven auto-sync & reactive background pipeline

On mobile devices, background synchronization cannot rely on naive periodic timers. Firing network requests and waking up the CPU on a rigid interval wastes battery, burns cellular data, and triggers noisy empty sync cycles when notes haven't changed. In this release, we rebuilt our entire background synchronization architecture into a demand-driven, reactive auto-sync engine.

Instead of polling continuously, GitScribe now tracks dirty state at the repository boundary (_dirtySince). The synchronization timer only arms when uncommitted edits or unpushed commits are actively present (_hasWorkToSync). When notebooks are clean, the sync scheduler enters complete quiescence with zero CPU wakeups. When network connectivity drops, pending changes are staged locally; the exact moment an Internet connection is restored (via connectivityProvider), the engine dispatches a high-priority push immediately without waiting for the remaining timer interval.

For PRO users utilizing on-device AI commit messages, the engine now features foreground prefetching. As files are edited in the document editor, on-device Gemini Nano analyzes working tree diffs and prepares conventional commit summaries in the background, eliminating latency the moment background auto-sync triggers. Finally, app suspension and resume lifecycles now automatically enforce local atomic commits to ensure zero data loss under any operating system constraint.

v0.6.9
// What shipped
  • Demand-driven auto-sync engine scheduling sync timers only when unpushed changes exist
  • Instant network recovery auto-sync automatically flushing pending changes when reconnected
  • Foreground AI commit message prefetching via on-device Gemini Nano eliminating sync latency
  • Background lifecycle safety ensuring dirty notes are atomically committed prior to app suspension
  • Reactive repository watchers adjusting sync schedules upon repository switching or cloning
  • Automated test suites covering demand-driven schedules and multi-file auto-sync conflict matrices

Hardening smart merge & robust 3-way conflict resolution

While we previously introduced 3-way smart merge to automate Git conflict resolution, extensive real-world dogfooding across multi-device setups surfaced several subtle edge cases. Cross-platform line ending mismatches (CRLF vs LF), desynchronized Git index stages during rapid sync loops, and simultaneous checklist edits could occasionally trigger false collisions or imperfect list joins. In this release, we completed a thorough bugfix and hardening overhaul of the entire 3-way Smart Merge Engine and its underlying Rust FFI bridge.

We rebuilt our low-level conflict extraction pipeline in Rust FFI via git_extract_conflict_content. The function now reliably pulls pristine, uncorrupted buffers for Stage 1 (Ancestor), Stage 2 (Ours), and Stage 3 (Theirs) directly from the libgit2 index. On the Dart side, SmartMergeEngine received major algorithmic fixes: paragraph and heading reconciliation now accurately tracks block boundaries, cross-platform newlines are normalized transparently, and Markdown list union has been hardened so that concurrent additions to checklists and bullet points merge cleanly without duplicate items or ordering glitches.

To guarantee absolute stability and prevent regressions, we created an extensive automated test suite with over 1,500 lines of rigorous test cases (smart_merge_engine_test.dart and smart_merge_conflict_scenarios_test.dart). These tests exhaustively simulate complex multi-file conflict matrices, concurrent offline-to-online edits, network dropouts, disk-full constraints, and expired authentication handshakes, ensuring GitScribe's background sync resolves cleanly and quietly under any condition.

v0.6.8
// What shipped
  • Overhauled SmartMergeEngine reconciliation logic with hardened block boundary detection
  • Fixed Rust FFI git_extract_conflict_content for resilient stage 1/2/3 Git index buffer retrieval
  • Hardened Markdown list union resolving ordering glitches on concurrent checklist additions
  • Fixed cross-platform CRLF/LF newline normalization eliminating false merge collisions
  • Robust SyncOrchestrator automated resolution flow with deterministic fallback safeguards
  • 1,500+ lines of comprehensive automated test scenarios covering concurrency and network fault matrices

Decoupled editor architecture & pure domain interfaces

As an offline-first Markdown editor, the writing experience is the heartbeat of GitScribe. As features like live formatting, interactive task lists, and background sync expanded, managing editor state inside a monolithic widget became unsustainable. In this release, we completed a dedicated architectural overhaul that encapsulates document state, extracts pure domain interfaces, and decomposes our remaining heavy views.

At the center of this update is EditorController, a standalone, testable controller that encapsulates document lifecycle, text mutations, preview/write mode toggling, and dirty tracking. The controller guarantees safe file flushes with synchronous unmount handlers (saveFileSync), ensuring in-flight edits are never lost when navigating away or backgrounding the app. Interactive Markdown checkboxes can now also be toggled directly in preview mode with localized in-place state updates rather than full-document reloads.

To reinforce Clean Architecture across the codebase, we extracted pure domain interfaces for all platform services (including ISubscriptionService, ISyncNotificationService, and auth providers). We also modularized HistoryScreen and RepositoriesTab into isolated, low-overhead widgets, slashing rebuild costs and guaranteeing smooth 120 FPS scrolling. Finally, we added native branch management with fast checkout and branch switching directly from the repository drawer.

v0.6.7
// What shipped
  • Dedicated EditorController managing document lifecycle, dirty tracking, and synchronous disposal flush
  • Interactive Markdown checkbox toggling in preview mode with localized state sync
  • Pure domain interfaces and Riverpod DI for subscription, notification, and auth services
  • Decomposed History and Repository views into isolated, high-performance micro-widgets
  • Native Git branch management with listBranches, checkoutBranch, and interactive switch sheet
  • Granular accessibility semantics for interactive preview checkboxes and editor state changes

Strict queue concurrency & offline sync resilience

Writing directly to local Git repositories on mobile requires flawless concurrency primitives and an unwavering native engine. In this milestone update, we overhauled our thread safety architecture, upgraded our native Rust toolchain, and decomposed our heaviest views into modular, high-performance components.

We re-engineered GitOperationQueue to enforce strict FIFO queue serialization across both read and write operations. By queuing read tasks behind active and pending writes, we guarantee that native FFI invocations to the underlying libgit2 engine never observe transient states or race against in-flight commits. Simultaneously, we upgraded our native dependencies to git2 0.21 and sha2 0.11, accompanied by position-independent OpenSSL static compilation (-fPIC / no-asm) for the Android NDK to eliminate linker text relocations on modern platforms.

On the UI layer, we decomposed SettingsScreen and FilesTab into focused, single-responsibility widgets, modernized async loaders using Dart 3 typed record destructuring, and replaced static configuration states with clean Riverpod dependency injection. We also added offline commit fallback protection to prevent note loss during network dropouts, non-blocking streaming full-text search with automatic .git exclusion, and a built-in Diagnostics suite for sanitized log export.

v0.6.6
// What shipped
  • GitOperationQueue strict FIFO serialization preventing libgit2 thread races
  • Upgraded native Rust dependencies to git2 0.21 and sha2 0.11
  • Position-independent OpenSSL static compilation (-fPIC / no-asm) for Android NDK
  • Local commit fallback ensuring notes are never lost during failed remote push attempts
  • Modular decomposition of SettingsScreen and FilesTab into focused sub-widgets
  • Non-blocking streaming full-text search engine with automatic .git exclusion
  • Built-in Diagnostics suite with exportable sanitized logs and environment metadata
  • Offline-first typography fallback chain ensuring resilient rendering without network dependencies

Zero-tolerance CI/CD & production signing gates

Shipping a cross-platform mobile app powered by a native Rust engine requires an industrial-grade build pipeline. Today we completed our GitHub Actions workflow matrix for both PR verification (ci.yml) and Play Store packaging (release.yml).

The CI pipeline operates on a zero-tolerance policy: flutter analyze --fatal-infos, mandatory dart format --set-exit-if-changed, and a worktree check that regenerates all Riverpod and Freezed sources via build_runner to ensure committed code never disagrees with generators. The Rust job runs a parallel matrix of cargo fmt --check, cargo clippy -D warnings, and all unit tests.

For Android builds, we wired automated non-interactive license validation for Android SDK Platform 37 and NDK 30.0.14904198. Our Gradle configuration now includes an unbypassable task-graph signing gate: it immediately halts if a release bundle would be signed with the public Android debug keystore. When building signed App Bundles (AAB), native Rust/libgit2 debug symbols are embedded under BUNDLE-METADATA for effortless symbolication in Google Play vitals.

v0.6.5
// What shipped
  • GitHub Actions CI/CD workflows covering Dart, Rust, and Android compilation gates
  • Automated worktree drift enforcement for code-generated providers and models
  • Android SDK 37 & NDK 30 installation with automated license acceptance
  • Gradle release signing gate blocking accidental public debug-key artifacts
  • Production AAB bundle optimization with embedded native libgit2 debug symbols

Pro feature architecture & unified dialog UX

As we refined our monetization and configuration flows, we completely overhauled how premium features are gated, how dynamic entitlements resolve, and how secrets are injected into local and production builds.

We instituted a strict UX gating rule: never gate a Pro feature by disabling its control. Disabled buttons give users no affordance to discover what the feature does or how to upgrade. Instead, all premium features now funnel through ProAccess.guard(), ProListTile, and ProBadge. Behind the scenes, the sync engine resolves features through dynamic strategy resolvers (effectiveCommitMessage and effectiveConflictResolution), granting Free users safe defaults while enabling custom formatting and advanced conflict resolution for Pro users without code branching.

We also standardized our modal presentation layer with AppDialogs, providing consistent Material 3 bottom sheets for link insertion, confirmation prompts, and branch management with high-contrast dark mode styling and accessible screen-reader semantics. Build-time credentials were decoupled from application source into templated .env.example files loaded via Flutter's native --dart-define-from-file.

v0.6.4
// What shipped
  • Unified pro_gate.dart architecture with ProAccess.guard and ProBadge
  • Non-disabled control policy ensuring transparent Pro discovery across all settings
  • Dynamic effectiveCommitMessage and effectiveConflictResolution strategy resolvers
  • Standardized Material 3 dialog and modal bottom sheet UX system via AppDialogs
  • Decoupled .env.example configuration loaded via compile-time constants
  • Updated developer iteration script (dev_run.ps1) supporting live and release modes
  • Fine-tuned default security settings for lock grace periods and background sync intervals

Privacy-first diagnostics & screen shields

Debugging distributed Git sync issues on mobile devices usually requires logs, but raw logs are notorious for capturing sensitive secrets like access tokens or note titles. We engineered a hardened diagnostic logging pipeline to solve this.

At the core is LogRedactor, an automated scrubber that intercepts every log entry before it enters our in-memory DiagnosticLog ring buffer. It sanitizes recognized credential patterns (including GitHub and GitLab PATs), Bearer authorization headers, private key blocks, and sensitive query parameters. A dedicated Diagnostics & System Info screen lets users inspect recent events and safely export redacted logs when asking for support.

We also added operating-system-level screen privacy. On Android, the app sets FLAG_SECURE to prevent screenshots and obscure app content in the recent apps switcher. On iOS, SceneDelegate layers a privacy blur over the view hierarchy the moment the app moves to the background.

v0.6.3
// What shipped
  • LogRedactor engine sanitizing OAuth tokens, PATs, Bearer headers, and private keys
  • In-memory DiagnosticLog circular ring buffer capturing the last 500 system events
  • Diagnostics UI screen with copy/export actions for safe, privacy-preserving bug reports
  • Android FLAG_SECURE and iOS background blur privacy shields

Strict URL policies & SSRF defense

When building a Git client that talks directly to user-supplied remotes, URL parsing isn't just about string formatting; it's a critical security boundary. Today we shipped a dedicated UrlPolicy engine to audit and sanitize every repository endpoint before libgit2 or our HTTP transports ever see it.

The policy enforces strict HTTPS and SSH transports by default, immediately rejecting insecure plain HTTP unless explicitly configured for local development. It strictly parses RFC 3986 hostnames and ports, strips embedded userinfo (like https://user:token@host) to ensure credentials never linger in Git remote state or logs, and implements comprehensive SSRF (Server-Side Request Forgery) protection.

Any attempts to point a repository remote to loopback interfaces (127.0.0.1, localhost), private RFC 1918 subnets, or cloud provider metadata endpoints (such as AWS/GCP's 169.254.169.254) are trapped and blocked at the UI boundary with clear, informative errors.

v0.6.2
// What shipped
  • Dedicated UrlPolicy security validator for all Git remotes and OAuth endpoints
  • Automatic stripping of embedded credentials from clone URLs prior to storage
  • SSRF protection blocking loopbacks, private CIDR blocks, and cloud metadata services
  • Explicit transport validation rejecting unencrypted HTTP endpoints

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, preventing multi-GB downloads from 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.