<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ayesha Iftikhar</title>
    <description>The latest articles on DEV Community by Ayesha Iftikhar (@ayeshaiftikhar).</description>
    <link>https://dev.to/ayeshaiftikhar</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3407569%2F81cfd268-140a-435e-88ea-73398ecf6eea.jpg</url>
      <title>DEV Community: Ayesha Iftikhar</title>
      <link>https://dev.to/ayeshaiftikhar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ayeshaiftikhar"/>
    <language>en</language>
    <item>
      <title>How Uber Computes Estimated Time of Arrival (ETA)</title>
      <dc:creator>Ayesha Iftikhar</dc:creator>
      <pubDate>Sat, 26 Sep 2026 17:05:43 +0000</pubDate>
      <link>https://dev.to/ayeshaiftikhar/how-uber-computes-estimated-time-of-arrival-eta-7eh</link>
      <guid>https://dev.to/ayeshaiftikhar/how-uber-computes-estimated-time-of-arrival-eta-7eh</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Short answer: how does Uber compute ETA?&lt;/strong&gt;&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Uber snaps noisy GPS pings to the road network (map matching), finds candidate routes on a road graph, and predicts travel time for each road segment with machine-learning models fed by real-time traffic. It then sums and corrects the route estimate, models its uncertainty, and keeps refreshing the Uber ETA as the trip progresses.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For a ride-hailing platform like Uber, Estimated Time of Arrival (ETA) is not a cosmetic feature. It is a core real-time decisioning system that directly influences conversion, matching quality, pricing behavior, and user trust.&lt;/p&gt;

&lt;p&gt;An inaccurate ETA increases cancellations, reduces marketplace liquidity, and weakens reliability perception. At scale, even small percentage errors can have measurable business impact.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Problem Definition
&lt;/h2&gt;

&lt;p&gt;Uber must estimate two different time horizons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pickup ETA:&lt;/strong&gt; Time for the driver to reach the rider&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trip ETA:&lt;/strong&gt; Time from pickup to destination&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This happens under hard constraints: real-time computation, global traffic variability, sparse-data regions, continuous driver movement, and noisy GPS streams.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. System Architecture Overview
&lt;/h2&gt;

&lt;p&gt;Uber's ETA stack is a multi-layer system where each layer solves a specific sub-problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Data ingestion for live GPS and traffic signals&lt;/li&gt;
&lt;li&gt;Map matching to align noisy locations to roads&lt;/li&gt;
&lt;li&gt;Routing to generate path candidates&lt;/li&gt;
&lt;li&gt;Feature engineering for temporal and contextual signals&lt;/li&gt;
&lt;li&gt;Machine learning for segment-level travel-time prediction&lt;/li&gt;
&lt;li&gt;Online serving for low-latency ETA responses&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Data Inputs that Drive Accuracy
&lt;/h2&gt;

&lt;p&gt;ETA quality depends on combining real-time, historical, and context-rich features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;**Real-time: **driver GPS, speed vectors, congestion, live traffic feeds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Historical:&lt;/strong&gt; time-of-day trends, weekday patterns, road speed distributions, weather and holiday effects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contextual:&lt;/strong&gt; city density, special events, roadwork, and construction zones&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Map Matching and Graph Routing
&lt;/h2&gt;

&lt;p&gt;Raw GPS is inherently noisy. Before route estimation, positions are snapped to likely road segments using probabilistic map-matching methods, often including Hidden Markov Model-style inference.&lt;/p&gt;

&lt;p&gt;The road network is then represented as a weighted directed graph where intersections are nodes and road segments are edges. Edge weights represent traversal time, not just distance.&lt;/p&gt;

&lt;p&gt;Routing engines apply optimized shortest-path methods such as Dijkstra, A*, and contraction hierarchies with real-time weight updates from live traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Travel Time Estimation via Machine Learning
&lt;/h2&gt;

&lt;p&gt;Uber does not rely purely on static shortest-path output. It predicts travel time at the road-segment level using machine learning.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Current speed and congestion&lt;/li&gt;
&lt;li&gt;Historical average segment speed&lt;/li&gt;
&lt;li&gt;Time bucket and day effects&lt;/li&gt;
&lt;li&gt;Weather condition&lt;/li&gt;
&lt;li&gt;Road type and intersection complexity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Model evolution typically progresses from linear models to gradient boosting and then to deep temporal architectures that better capture time-series traffic dynamics.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Pickup ETA Computation Logic
&lt;/h2&gt;

&lt;p&gt;Pickup ETA is often harder than trip ETA because drivers may be moving, finishing another ride, or deviating in dense traffic.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Pickup ETA ≈ Remaining current segment time + optimal route time + intersection delay adjustments.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The system also computes uncertainty margins so user-facing predictions are robust under variance.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Real-Time Recalculation
&lt;/h2&gt;

&lt;p&gt;ETA is continuously refreshed every few seconds using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live GPS deltas&lt;/li&gt;
&lt;li&gt;Congestion updates&lt;/li&gt;
&lt;li&gt;Route deviation detection&lt;/li&gt;
&lt;li&gt;Dynamic re-routing logic
If traffic spikes or route conditions shift, ETA is recomputed immediately.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  8. Offline Training and Online Serving
&lt;/h2&gt;

&lt;p&gt;The architecture is split into two stages for scale and latency:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Offline:&lt;/strong&gt; train on billions of trips, build region-specific models, retrain continuously&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Online:&lt;/strong&gt; low-latency inference with feature store lookups, caching, and approximate serving optimizations
Production latency targets are strict, commonly sub-100 ms for inference pathways.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  9. Uncertainty and Confidence Modeling
&lt;/h2&gt;

&lt;p&gt;ETA systems must model variance explicitly rather than output a single deterministic number.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Confidence intervals&lt;/li&gt;
&lt;li&gt;Percentile ETAs (for example P50 and P90)&lt;/li&gt;
&lt;li&gt;Error calibration curves&lt;/li&gt;
&lt;li&gt;This enables messaging like &lt;strong&gt;"Arriving in 5–7 minutes"&lt;/strong&gt; and improves perceived reliability.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  10. Global Scaling Challenges
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;City diversity:&lt;/strong&gt; each city has distinct traffic behavior requiring regionalized models&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sparse regions:&lt;/strong&gt; cold-start geographies rely more on map priors, traffic APIs, and transfer learning&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPS distortion:&lt;/strong&gt; urban canyons require smoothing with Kalman filters and trajectory correction&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  11. Continuous Improvement Loop
&lt;/h2&gt;

&lt;p&gt;Uber improves ETA with a feedback cycle that combines A/B testing, prediction error pipelines, cancellation correlation analysis, and operational feedback from drivers and riders.&lt;/p&gt;

&lt;p&gt;When ETA error patterns correlate with cancellation spikes, retraining and feature adjustments are prioritized.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. Business Impact
&lt;/h2&gt;

&lt;p&gt;Accurate ETA improves ride acceptance, reduces cancellations, raises driver utilization, and increases marketplace efficiency. Even small gains in ETA precision create significant revenue and trust impact at global scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  13. Lessons for Engineers
&lt;/h2&gt;

&lt;p&gt;Do not rely only on shortest-path algorithms&lt;br&gt;
Predict at segment level instead of full-route level&lt;br&gt;
Separate heavy offline ML from low-latency online serving&lt;br&gt;
Model uncertainty explicitly, not optionally&lt;br&gt;
Retrain continuously as traffic behavior shifts&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaway
&lt;/h2&gt;

&lt;p&gt;Uber's ETA engine is far more than distance divided by speed. It is a distributed, real-time intelligence system combining graph theory, probabilistic modeling, deep learning, and infrastructure engineering under strict latency and reliability constraints.&lt;/p&gt;

</description>
      <category>algorithms</category>
      <category>machinelearning</category>
      <category>scalability</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>How I Built a Local-First Journaling App with Flutter, Drift and Firebase</title>
      <dc:creator>Ayesha Iftikhar</dc:creator>
      <pubDate>Wed, 23 Sep 2026 12:33:13 +0000</pubDate>
      <link>https://dev.to/ayeshaiftikhar/how-i-built-a-local-first-journaling-app-with-flutter-drift-and-firebase-1i34</link>
      <guid>https://dev.to/ayeshaiftikhar/how-i-built-a-local-first-journaling-app-with-flutter-drift-and-firebase-1i34</guid>
      <description>&lt;p&gt;I build &lt;a href="https://mindleau.web.app" rel="noopener noreferrer"&gt;Mindleau&lt;/a&gt;, a free brain-dump journal and mood tracker for iOS and Android. From day one, it had one hard requirement: &lt;strong&gt;you should be able to open the app and start writing without creating an account or having an internet connection.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Journal entries are some of the most private things people type, and the moment someone needs to empty their head is probably the worst possible time to show them a sign-up form.&lt;/p&gt;

&lt;p&gt;That requirement shaped the whole architecture. Here's how it's built, including the tradeoffs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flutter&lt;/strong&gt; for iOS and Android from one codebase&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drift&lt;/strong&gt; (typed SQLite) as the source of truth&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firebase Auth + Cloud Firestore&lt;/strong&gt; for optional background sync&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;provider&lt;/strong&gt; + &lt;code&gt;ChangeNotifier&lt;/code&gt; / &lt;code&gt;ValueNotifier&lt;/code&gt; for state. No Riverpod, no Bloc. The app is small enough that it didn't need them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The central rule: &lt;strong&gt;the UI never talks to Firestore.&lt;/strong&gt; Every screen reads and writes through a single repository, and the repository only writes to SQLite. Sync happens afterwards, in the background.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Screens ──► SanctuaryRepository ──► Drift (SQLite)   ◄── source of truth
                      │
                      └──(background)──► SyncService ──► Firestore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  1. Every syncable table carries its own sync state
&lt;/h2&gt;

&lt;p&gt;Instead of a separate outbox table, each syncable row records whether it has been pushed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BrainDumpRows&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;Table&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;IntColumn&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;autoIncrement&lt;/span&gt;&lt;span class="p"&gt;()();&lt;/span&gt;
  &lt;span class="n"&gt;TextColumn&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;remoteId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;()();&lt;/span&gt;
  &lt;span class="n"&gt;TextColumn&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;()();&lt;/span&gt;
  &lt;span class="n"&gt;DateTimeColumn&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;createdAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;dateTime&lt;/span&gt;&lt;span class="p"&gt;()();&lt;/span&gt;
  &lt;span class="n"&gt;DateTimeColumn&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;updatedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;dateTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;clientDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)();&lt;/span&gt;
  &lt;span class="n"&gt;DateTimeColumn&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;deletedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;dateTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;()();&lt;/span&gt;   &lt;span class="c1"&gt;// soft delete&lt;/span&gt;
  &lt;span class="n"&gt;TextColumn&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;mood&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;()();&lt;/span&gt;
  &lt;span class="n"&gt;TextColumn&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;focusTagsCsv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;()();&lt;/span&gt;
  &lt;span class="n"&gt;TextColumn&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;syncStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Constant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'synced'&lt;/span&gt;&lt;span class="p"&gt;))();&lt;/span&gt;       &lt;span class="c1"&gt;// pending_create | pending_update | synced&lt;/span&gt;
  &lt;span class="n"&gt;DateTimeColumn&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;lastSyncedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;dateTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;()();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four columns carry the sync logic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;remoteId&lt;/code&gt; maps the local autoincrement ID to the Firestore document ID.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;updatedAt&lt;/code&gt; drives conflict resolution (more on that below).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;deletedAt&lt;/code&gt; is a tombstone. Hard-deleting a row locally would leave nothing to tell the server it's gone.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;syncStatus&lt;/code&gt; makes "what still needs pushing?" a single indexed query.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Writes are local and synchronous; sync is fire-and-forget
&lt;/h2&gt;

&lt;p&gt;Saving a brain dump is a plain SQLite insert, marked as pending, followed by a sync attempt the UI doesn't wait for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;saveBrainDump&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;focusTags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_database&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;into&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_database&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;brainDumpRows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;BrainDumpRowsCompanion&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;content:&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;createdAt:&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;updatedAt:&lt;/span&gt; &lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="nl"&gt;mood:&lt;/span&gt; &lt;span class="n"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;focusTagsCsv:&lt;/span&gt; &lt;span class="n"&gt;_encodeList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;focusTags&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="nl"&gt;syncStatus:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'pending_create'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;unawaited&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;syncNow&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// never blocks the save&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user sees their entry immediately. Whether sync then succeeds, fails, or can't run because there's no signal changes nothing on screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Sync is pull-then-push, last-write-wins
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;syncNow()&lt;/code&gt; runs the same steps every time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;syncNow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_canAttemptCloudSync&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ensureSignedIn&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_pullRemoteChanges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_pushPreferences&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_pushBrainDumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_pushMoodCheckIns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_pushLearnProgress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stackTrace&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Sync failed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;error:&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;stackTrace:&lt;/span&gt; &lt;span class="n"&gt;stackTrace&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pulling first means remote changes (from another device) are merged before local pending rows are pushed. The merge rule is plain &lt;strong&gt;last-write-wins on &lt;code&gt;updatedAt&lt;/code&gt;&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_database&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_database&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;brainDumpRows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;remoteId&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remoteId&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSingleOrNull&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Local copy is newer: keep it; it'll be pushed next.&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;updatedAt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isAfter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remoteUpdatedAt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Tradeoff:&lt;/strong&gt; LWW can lose an edit if the same entry is changed on two offline devices. For a personal journal, where one person is almost always on one device at a time, that's acceptable. A CRDT would add a lot of complexity to prevent a conflict that almost never happens here.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Anonymous auth first, email later
&lt;/h2&gt;

&lt;p&gt;"No account required" and "sync across devices" pull in opposite directions. Firebase anonymous auth solves it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;On first sync, the app signs in anonymously. The user never sees a form, but their data has a stable &lt;code&gt;users/{uid}&lt;/code&gt; path in Firestore.&lt;/li&gt;
&lt;li&gt;If they later add an email, the anonymous account is &lt;strong&gt;linked&lt;/strong&gt; to an email-link credential, so the same UID and data carry over.&lt;/li&gt;
&lt;li&gt;On a new phone, the email link restores that account.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If anonymous auth is disabled or unreachable, &lt;code&gt;ensureSignedIn()&lt;/code&gt; returns &lt;code&gt;false&lt;/code&gt; and sync is skipped. The app keeps working from SQLite either way.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Bound every network call
&lt;/h2&gt;

&lt;p&gt;This bug took me a while to understand. Firestore and Firebase Auth calls &lt;strong&gt;have no built-in timeout&lt;/strong&gt;. On a flaky connection that isn't fully offline, an &lt;code&gt;await&lt;/code&gt; can hang forever, leaving a spinner with no way out. Every network call now goes through one wrapper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;_networkTimeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;seconds:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_withTimeout&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;_networkTimeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;onTimeout:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;NetworkTimeoutException&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the UI never awaits sync, a timeout just means "try again later". When connectivity comes back, a listener calls &lt;code&gt;syncNow()&lt;/code&gt; and every row that isn't &lt;code&gt;synced&lt;/code&gt; goes up.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Migrations are part of the product
&lt;/h2&gt;

&lt;p&gt;Moving from "local only" to "local-first with sync" meant adding sync columns to tables that already held real user data. Drift's &lt;code&gt;MigrationStrategy&lt;/code&gt; makes this explicit and versioned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nd"&gt;@override&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;schemaVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@override&lt;/span&gt;
&lt;span class="n"&gt;MigrationStrategy&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;migration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MigrationStrategy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;onUpgrade:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;brainDumpRows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;brainDumpRows&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;remoteId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;brainDumpRows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;brainDumpRows&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;syncStatus&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="c1"&gt;// ...&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stillnessSessionRows&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One lesson: SQLite won't add a &lt;code&gt;NOT NULL&lt;/code&gt; column without a SQL-level default, and Drift's &lt;code&gt;clientDefault&lt;/code&gt; only exists in Dart. So &lt;code&gt;updated_at&lt;/code&gt; needed a raw &lt;code&gt;ALTER TABLE ... ADD COLUMN updated_at INTEGER NOT NULL DEFAULT 0&lt;/code&gt; for the rows already on users' phones.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Not everything has to sync
&lt;/h2&gt;

&lt;p&gt;Guided stillness sessions (a timer-based breathing pause) are &lt;strong&gt;local-only&lt;/strong&gt; on purpose. They only feed streaks and the 30-day heatmap, so syncing them wasn't worth the extra code and the extra data leaving the device. Deciding what &lt;em&gt;not&lt;/em&gt; to sync is part of the privacy design.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Start with the sync columns.&lt;/strong&gt; Adding them in migration v2 was avoidable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store tags as a JSON column, not CSV.&lt;/strong&gt; CSV was quick at first and has been awkward to work with since.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the timeout wrapper on day one.&lt;/strong&gt; Hanging awaits are the worst kind of offline bug because they only show up on bad networks, never at your desk.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;If you're building anything personal (journals, habit trackers, notes), local-first is less work than it sounds:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Make SQLite the source of truth.&lt;/li&gt;
&lt;li&gt;Put a &lt;code&gt;syncStatus&lt;/code&gt; and &lt;code&gt;updatedAt&lt;/code&gt; on every row.&lt;/li&gt;
&lt;li&gt;Never let the UI wait for the network.&lt;/li&gt;
&lt;li&gt;Use anonymous auth so "no account" and "sync" can coexist.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can see the result in &lt;a href="https://mindleau.web.app" rel="noopener noreferrer"&gt;Mindleau&lt;/a&gt;. It's free on iOS and Android, and you can start writing without signing up. Questions about the sync design are welcome in the comments.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>firebase</category>
      <category>architecture</category>
      <category>mobile</category>
    </item>
    <item>
      <title>How to Generate a Secure JWT Secret Key in Node.js</title>
      <dc:creator>Ayesha Iftikhar</dc:creator>
      <pubDate>Wed, 13 Aug 2025 10:35:52 +0000</pubDate>
      <link>https://dev.to/ayeshaiftikhar/how-to-generate-a-secure-jwt-secret-key-in-nodejs-335f</link>
      <guid>https://dev.to/ayeshaiftikhar/how-to-generate-a-secure-jwt-secret-key-in-nodejs-335f</guid>
      <description>&lt;p&gt;When working with JWT (JSON Web Tokens) in a Node.js application, your secret key plays a critical role in signing and verifying tokens.&lt;/p&gt;

&lt;p&gt;A weak or predictable key can compromise your application’s security, so it’s best to generate it randomly.&lt;/p&gt;

&lt;p&gt;Here’s how you can easily create a strong JWT secret key using Node.js:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add Script to &lt;code&gt;package.json&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Open your package.json and inside the "scripts" section, add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"scripts": {
  "jwt-key": "node -e \"console.log(require('crypto').randomBytes(32).toString('hex'))\""
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Run the script&lt;/strong&gt;&lt;br&gt;
In your terminal run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm run jwt-key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;a47f7c20e4f9a87c6d0af6e6c8c4bc25d2a8e7c4a9f5c30f72db7d9a48f1c3d2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy that string into your &lt;code&gt;.env&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you use this often, you can even combine it with a direct write to .env so it replaces the JWT secret automatically:&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"scripts": {
  "jwt-key": "echo JWT_SECRET=$(node -e \"console.log(require('crypto').randomBytes(32).toString('hex'))\") &amp;gt; .env"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will overwrite &lt;code&gt;.env&lt;/code&gt; with the new secret each time.&lt;/p&gt;

</description>
      <category>node</category>
      <category>jwt</category>
      <category>npm</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
