<?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: Vladimir Letiagin</title>
    <description>The latest articles on DEV Community by Vladimir Letiagin (@jorrygo_dev).</description>
    <link>https://dev.to/jorrygo_dev</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1938665%2F7788418f-fcca-4050-b8ab-871a7f432c88.png</url>
      <title>DEV Community: Vladimir Letiagin</title>
      <link>https://dev.to/jorrygo_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jorrygo_dev"/>
    <language>en</language>
    <item>
      <title>The best Jira time trackers for engineering teams in 2026</title>
      <dc:creator>Vladimir Letiagin</dc:creator>
      <pubDate>Tue, 12 May 2026 19:48:53 +0000</pubDate>
      <link>https://dev.to/jorrygo_dev/the-best-jira-time-trackers-for-engineering-teams-in-2026-n94</link>
      <guid>https://dev.to/jorrygo_dev/the-best-jira-time-trackers-for-engineering-teams-in-2026-n94</guid>
      <description>&lt;p&gt;I co-founded &lt;a href="https://time.planim.app/jira?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=best-jira-trackers" rel="noopener noreferrer"&gt;Planim Time&lt;/a&gt;, one of the trackers in this list, so factor that in. The flip side of building one of these for a year is that I know which of the other four would beat us in your situation, and where. So instead of a "best overall" ranking (a question with no honest answer), what follows is my actual shortlist, organised by the use case each tool wins on.&lt;/p&gt;

&lt;p&gt;Prices in this category move. Re-verify before quoting any number internally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Criteria
&lt;/h2&gt;

&lt;p&gt;What I'm not weighing, and what I am.&lt;/p&gt;

&lt;p&gt;I don't weigh &lt;strong&gt;mobile apps&lt;/strong&gt; highly for this audience. Phone-first tracking matters for field service, agencies billing from cafés, consultants on the move; Clockify below earns its spot partly on that. For an engineer in a sprint at a two-monitor desk, it's almost never the deciding factor.&lt;/p&gt;

&lt;p&gt;I don't weigh &lt;strong&gt;AI weekly summaries&lt;/strong&gt; highly. They're the headline feature on every tracker's pricing page right now, and the engineers I talk to read them once and turn them off. The hour you logged on Tuesday is whatever the worklog says.&lt;/p&gt;

&lt;p&gt;I do weigh:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Survival of a Jira outage.&lt;/strong&gt; Atlassian's &lt;a href="https://status.atlassian.com/" rel="noopener noreferrer"&gt;status page&lt;/a&gt; shows a couple of degraded periods most months. If your tracker goes blank during them, that's a failure mode you keep paying for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where the API token lives.&lt;/strong&gt; OS keychain means it shares a vault with the saved passwords in your browser. SaaS account means you inherit that vendor's threat model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Round-trip sync, not just push.&lt;/strong&gt; When a teammate fixes a worklog directly in Jira, your tracker either picks that change up or silently overwrites it on the next push. Most overwrite. I've written about &lt;a href="https://time.planim.app/blog/two-way-jira-worklog-sync" rel="noopener noreferrer"&gt;why&lt;/a&gt; separately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whether you need a Jira admin to install it.&lt;/strong&gt; Marketplace apps need one; desktop apps don't. For a single engineer trying to evaluate something, the difference between five minutes and three weeks of internal tickets is decisive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free tier that isn't a teaser.&lt;/strong&gt; An indefinite free tier that does the job, or a long trial. "Fourteen days then it nags you" doesn't count.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tempo Timesheets: finance and PMO that doesn't sleep
&lt;/h2&gt;

&lt;p&gt;If someone at your company has "Director" in their title and reads timesheets monthly, &lt;a href="https://marketplace.atlassian.com/apps/6572/tempo-timesheets-time-tracking-report" rel="noopener noreferrer"&gt;Tempo Timesheets&lt;/a&gt; is probably already the answer. It's been the #1 time-management product in the Atlassian ecosystem since 2009, and the feature stack shows it: approvals, capacity planning, budgets, invoice-ready timesheets, audit-friendly reports. None of which is easy to retrofit.&lt;/p&gt;

&lt;p&gt;Tempo Timesheets on Cloud starts at &lt;strong&gt;$10 per user per month&lt;/strong&gt; and scales per Jira user from there. List prices have crept up roughly once a year, so before quoting any number, re-check the &lt;a href="https://marketplace.atlassian.com/apps/6572/tempo-timesheets-time-tracking-report?hosting=cloud&amp;amp;tab=pricing" rel="noopener noreferrer"&gt;Marketplace listing&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Trade-off: you're paying per Jira user for a workflow that mostly serves the finance side of the room. A four-engineer squad paying $40/month so two of them log hours and a non-engineer runs an approval flow stops liking enterprise features pretty quickly. That's the gap I see most when teams leave Tempo. Tempo isn't bad; the engineers were paying for a finance product they didn't need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick Tempo if&lt;/strong&gt; approvals, capacity planning, or client invoicing inside the tracker are load-bearing for your team. We have a &lt;a href="https://time.planim.app/jira/vs/tempo?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=best-jira-trackers" rel="noopener noreferrer"&gt;longer head-to-head&lt;/a&gt; if you want the row-by-row.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clockwork: free for ten, Jira-internal for everyone else
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://marketplace.atlassian.com/apps/1219213/clockwork-pro-for-jira-time-tracking-and-timesheets" rel="noopener noreferrer"&gt;Clockwork&lt;/a&gt; (HeroCoders) is the lightest option in the Marketplace. Two SKUs: &lt;strong&gt;Pro&lt;/strong&gt;, free for ≤10 Cloud users and paid above, and &lt;strong&gt;Lite&lt;/strong&gt;, which HeroCoders split off in January 2025 for teams that want something smaller and cheaper than Pro.&lt;/p&gt;

&lt;p&gt;Under ten Cloud users, this is hard to beat: a real timesheet UI, embedded inside Jira, at zero cost. The catch is the same as Tempo's at smaller scale. The timer is a Jira widget that lives or dies with the page. No offline mode, no menu-bar surface.&lt;/p&gt;

&lt;p&gt;Watch the upgrade cliff. Above ten users the per-Jira-user model bills every Jira user, including ones who'll never log an hour. A 50-person org where 12 actively track ends up paying for 38 people who don't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick Clockwork if&lt;/strong&gt; you want a free timesheet that lives inside the Jira tab itself (no separate app to install) and your team is ≤10 users on Cloud, or your policy is "everything stays in Jira's database". &lt;a href="https://time.planim.app/jira/vs/clockwork?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=best-jira-trackers" rel="noopener noreferrer"&gt;Head-to-head&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Everhour: when the timer also has to be the invoice
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://everhour.com/pricing" rel="noopener noreferrer"&gt;Everhour&lt;/a&gt; is the right pick when "track an hour" and "bill a client" are two faces of the same job. Pricing as of April 2026 is &lt;strong&gt;$8.50 per user per month billed annually&lt;/strong&gt;, with a 15% premium for monthly billing and a five-seat minimum on the paid plan. The free tier covers up to five users.&lt;/p&gt;

&lt;p&gt;The Jira integration is a Marketplace add-on that replaces Jira's native time-tracking UI with Everhour's controls (with an optional browser extension for list and board views). Projects sync into Everhour's web dashboard, and that's where the centre of gravity is: budgets, billable rates, custom reports, QuickBooks/Xero/FreshBooks integrations, capacity planning. If a single hour has to end up on a client invoice through the same tool that started the timer, Everhour has built around that opinion for years.&lt;/p&gt;

&lt;p&gt;The engineering trade-off: you pay for an invoicing engine even if you never invoice, your token lives in Everhour's cloud (not your OS keychain), and logging forty minutes against &lt;code&gt;PROJ-1247&lt;/code&gt; takes more clicks than it should.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick Everhour if&lt;/strong&gt; you bill clients hourly and the tracker also has to be the billing system. &lt;a href="https://time.planim.app/jira/vs/everhour?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=best-jira-trackers" rel="noopener noreferrer"&gt;Head-to-head&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clockify: generalist with a phone in its pocket
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://clockify.me/pricing" rel="noopener noreferrer"&gt;Clockify&lt;/a&gt; is the breadth play. The free tier covers &lt;strong&gt;up to five users&lt;/strong&gt; and includes a working timer, timesheets, idle detection, calendar view, mobile + desktop apps, Pomodoro, kiosk mode, and 80+ integrations. Paid tiers run &lt;strong&gt;Basic $4.99 / Standard $6.99 / Pro $9.99 / Enterprise $14.99&lt;/strong&gt; per user per month, with ~20% off on annual. The Jira integration pushes worklogs through the browser extension.&lt;/p&gt;

&lt;p&gt;If your tracking life really does span Jira, GitHub, Trello, Asana, Basecamp, ClickUp plus a phone, that breadth is paid for and used. If 90% of your hours are on Jira, you're paying for surface you'll never touch. The Jira integration is also one-way (extension → Jira), not round-trip, so the moment a teammate fixes a worklog in Jira, the tracker goes quietly out of sync.&lt;/p&gt;

&lt;p&gt;There's also the second-cloud problem: time entries live primarily inside Clockify and get pushed to Jira on demand. For an engineering team that already pays Atlassian for Jira, a parallel store of the same data is something a security review will eventually flag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick Clockify if&lt;/strong&gt; you track across many tools or you need a mobile app as a first-class surface. &lt;a href="https://time.planim.app/jira/vs/clockify?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=best-jira-trackers" rel="noopener noreferrer"&gt;Head-to-head&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Planim Time: full disclosure, this is mine
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://time.planim.app/jira?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=best-jira-trackers" rel="noopener noreferrer"&gt;Planim Time for Jira&lt;/a&gt; is what I've been working on for the last year. It's a native desktop app, a menu-bar or system-tray icon on macOS, Windows, and Linux. It talks to your Jira instance through your personal API token, stores the token in the OS keychain, and runs entirely offline. Worklogs sync both ways: edit one in Jira's UI and it shows up in the tracker within about a minute, no webhook server in the middle. Issue selection is JQL-driven, so whatever filter you already use in Jira, the tracker can use too.&lt;/p&gt;

&lt;p&gt;On a normal workday the surface most people live in is the calendar (Pro). It's a multi-week grid where worklogs are draggable blocks you can resize to extend or shorten. Closer to how Google Calendar feels than a row-by-row spreadsheet. Team stats roll up every member who logged time to the workspace's Jira issues, not only the paid seats, so the boards include everyone's hours without you having to buy licences for the people who won't use Pro. Both come with a 14-day trial on first launch, no card.&lt;/p&gt;

&lt;p&gt;Where Planim Time loses to the four above: no approval workflows, no capacity planning, no budgets, no full invoicing (CSV export is as close as we get), no mobile app, no kiosk mode. We track worked hours and push them to Jira; everything else is somebody else's job. If you need any of that, one of the four above will earn its seat. We wrote separately about &lt;a href="https://time.planim.app/blog/why-native-desktop-time-trackers" rel="noopener noreferrer"&gt;why we shipped a desktop binary instead of a Marketplace plugin&lt;/a&gt; if you want the architectural side of that trade-off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick Planim Time if&lt;/strong&gt; Jira is your source of truth, you want a timer that survives both Jira outages and laptop tab-evictions, and you'd rather your API token live in the OS keychain than in another vendor's cloud.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Finance reads your timesheets monthly?&lt;/strong&gt; Tempo Timesheets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want the timer to live inside the Jira tab itself, ≤10 users on Cloud?&lt;/strong&gt; Clockwork (Pro Free or Lite).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You bill clients hourly through the same tool that runs the timer?&lt;/strong&gt; Everhour.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your tracking life spans many tools and you need a real mobile app?&lt;/strong&gt; Clockify (free up to five users; $4.99–$14.99 per seat above that).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jira is the source of truth and you want a timer that survives Atlassian's bad afternoons?&lt;/strong&gt; Planim Time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Five buckets, five different winners. There isn't a single "best Jira time tracker", and the mistake every other roundup makes is pretending there is one.&lt;/p&gt;

&lt;p&gt;If you've got Jira and an hour you need to remember on Tuesday, &lt;a href="https://time.planim.app/jira/download?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=best-jira-trackers" rel="noopener noreferrer"&gt;download Planim Time&lt;/a&gt; and point it at your real instance for a sprint. If it doesn't earn its keep in the first week, one of the four above will, and now you know which one.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>tooling</category>
      <category>agile</category>
      <category>jira</category>
    </item>
    <item>
      <title>Building a Jira Time Tracker with Tauri: How I Stored API Tokens Securely</title>
      <dc:creator>Vladimir Letiagin</dc:creator>
      <pubDate>Wed, 29 Apr 2026 18:49:08 +0000</pubDate>
      <link>https://dev.to/jorrygo_dev/building-a-jira-time-tracker-with-tauri-how-i-stored-api-tokens-securely-46aj</link>
      <guid>https://dev.to/jorrygo_dev/building-a-jira-time-tracker-with-tauri-how-i-stored-api-tokens-securely-46aj</guid>
      <description>&lt;p&gt;I've been building a small menu-bar app for tracking time on Jira issues. Mostly it's boring CRUD: a timer, a list of issues, push worklogs back to Jira. Except for one thing I had to figure out on day one. The user logs in with a Jira API token, and that token has to live somewhere on their machine.&lt;/p&gt;

&lt;p&gt;Where, exactly?&lt;/p&gt;

&lt;p&gt;Every Tauri tutorial skips this part. Below is what I tried, what broke, and what I shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  My first attempt (don't do this)
&lt;/h2&gt;

&lt;p&gt;I already had a local SQLite database in the app for caching Jira issues, worklogs, and user prefs. So my first move was the obvious one — stick the API token in a &lt;code&gt;settings&lt;/code&gt; table next to everything else. One database, one place to look, simple.&lt;/p&gt;

&lt;p&gt;Don't do this.&lt;/p&gt;

&lt;p&gt;SQLite stores everything in a plain &lt;code&gt;.db&lt;/code&gt; file on disk. No encryption by default. Anyone with file access on the machine reads your token in two seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sqlite3 app.db &lt;span class="s2"&gt;"SELECT value FROM settings WHERE key='api_token'"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Backups, iCloud sync, Dropbox, malware scanners — all see it. Same problem with &lt;code&gt;localStorage&lt;/code&gt; in the Tauri webview, with a JSON file in the app data dir, or with convenience plugins like &lt;code&gt;tauri-plugin-store&lt;/code&gt;. Plain text on disk is plain text on disk. And if your frontend has any XSS-shaped bug, the token is right there for a script to grab.&lt;/p&gt;

&lt;p&gt;What you actually want is the OS credential store. Every desktop OS ships with one. They exist specifically to keep secrets off plain disk and away from other users on the machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Tauri suggests
&lt;/h2&gt;

&lt;p&gt;Tauri's official docs point you at the Stronghold plugin. It uses IOTA's Stronghold engine, encrypts everything with a password, and writes to a snapshot file.&lt;/p&gt;

&lt;p&gt;Stronghold solves a different problem — a full vault with key derivation, multiple records, the whole deal. For a single API token it's the wrong shape. It also needs the user to enter a password to unlock the vault, or you stash that password somewhere, and now you've moved the problem one level up. Where do you store the password that unlocks Stronghold?&lt;/p&gt;

&lt;p&gt;I wanted the OS to handle the trust. That means the system keychain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually used: the keyring crate
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://crates.io/crates/keyring" rel="noopener noreferrer"&gt;&lt;code&gt;keyring&lt;/code&gt;&lt;/a&gt; crate is a Rust wrapper that gives you one API on top of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;macOS&lt;/strong&gt;: Keychain Services&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows&lt;/strong&gt;: Credential Manager&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linux&lt;/strong&gt;: Secret Service API (gnome-keyring, KWallet, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You write the code once, and on each platform it talks to the native store. Users get the same security guarantees they expect from Safari saving a password or git saving a credential.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Heads up: &lt;code&gt;keyring&lt;/code&gt; 3.x is a major bump from 2.x with breaking API changes. Half the tutorials you'll find on Google are still on the old version, so check the version in their &lt;code&gt;Cargo.toml&lt;/code&gt; before copy-pasting.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;Cargo.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[dependencies]&lt;/span&gt;
&lt;span class="py"&gt;keyring&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;features&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s"&gt;"apple-native"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"windows-native"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"sync-secret-service"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"crypto-rust"&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;Those features matter. &lt;code&gt;apple-native&lt;/code&gt; and &lt;code&gt;windows-native&lt;/code&gt; use the platform APIs directly. &lt;code&gt;sync-secret-service&lt;/code&gt; is the Linux backend. &lt;code&gt;crypto-rust&lt;/code&gt; handles the D-Bus transport encryption in pure Rust so you don't drag in an OpenSSL dependency.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole storage module
&lt;/h2&gt;

&lt;p&gt;This is the entire &lt;code&gt;secure_store.rs&lt;/code&gt; from my app. There's nothing more to it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;keyring&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Entry&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;SERVICE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"planim-time-tracker"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;set_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nn"&gt;Entry&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&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="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Keyring entry error: {e}"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="nf"&gt;.set_password&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Failed to save to keyring: {e}"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;get_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="nn"&gt;Entry&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&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="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Keyring entry error: {e}"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="nf"&gt;.get_password&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;keyring&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;NoEntry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Failed to read from keyring: {e}"&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="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;delete_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="nn"&gt;Entry&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&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="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Keyring entry error: {e}"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="nf"&gt;.delete_credential&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;keyring&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;NoEntry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(()),&lt;/span&gt;
        &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Failed to delete from keyring: {e}"&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;Three functions, no state, no init. The &lt;code&gt;SERVICE&lt;/code&gt; constant is what shows up as the entry name in Keychain Access on macOS or Credential Manager on Windows. Pick a stable name and don't change it later — if you do, every existing user loses their saved credentials and has to re-auth.&lt;/p&gt;

&lt;p&gt;One thing worth being explicit about: this is for &lt;strong&gt;secrets only&lt;/strong&gt;. Tokens, refresh tokens, passwords. User preferences, base URLs, UI state — that all goes in SQLite or wherever else makes sense. Keychain entries are slow-ish to read and have size limits, so don't dump everything in there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it into a Tauri command
&lt;/h2&gt;

&lt;p&gt;I never touch &lt;code&gt;secure_store&lt;/code&gt; from the frontend. The frontend asks for an action, the backend handles the secret. The Jira API token never leaves the Rust side once it's saved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[tauri::command]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;settings_save_jira_config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AppState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// non-secret fields go in SQLite&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="nn"&gt;secure_store&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;set_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"jira_api_token"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;api_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&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 pattern: secrets in keyring, everything else (Jira base URL, email, preferences) in SQLite. The frontend never holds the token in JS state for longer than a single form submission.&lt;/p&gt;

&lt;h2&gt;
  
  
  Per-OS gotchas you'll only find out about in production
&lt;/h2&gt;

&lt;p&gt;This is the part nobody mentions until it bites you.&lt;/p&gt;

&lt;h3&gt;
  
  
  macOS
&lt;/h3&gt;

&lt;p&gt;The first time another binary tries to read a Keychain item, the user gets an "allow access" prompt — three buttons: Always Allow, Allow, Deny. No password input. A lot of devs avoid Keychain because they think it nags users for their macOS password every time. It doesn't, as long as you put items in the &lt;em&gt;login&lt;/em&gt; keychain (which is what &lt;code&gt;keyring&lt;/code&gt; does by default). The login keychain is unlocked automatically when the user signs into the Mac, and stays unlocked.&lt;/p&gt;

&lt;p&gt;The catch: Keychain identifies your app by its code signature. If the signature changes, Keychain treats it as a new app and prompts again. In development, every &lt;code&gt;cargo build&lt;/code&gt; produces a new ad-hoc signature, so you see the "allow access" prompt after every rebuild. Annoying, but it goes away in production as long as you sign with a stable Developer ID. The signature stays consistent across app updates, and Always Allow sticks.&lt;/p&gt;

&lt;p&gt;If you ship unsigned, you have a bigger problem than Keychain prompts: macOS 15+ makes it deeply painful for users to launch your app. The right-click → Open trick is gone in Sequoia — users now have to dig into System Settings → Privacy &amp;amp; Security and explicitly allow the binary every time. Sign your app. The Apple Developer Program is $99/year and worth it for any real desktop app.&lt;/p&gt;

&lt;p&gt;Notarization, by the way, doesn't affect Keychain at all. That's only for Gatekeeper letting users open downloaded &lt;code&gt;.dmg&lt;/code&gt; files.&lt;/p&gt;

&lt;p&gt;There's also a quirk where if you change your bundle identifier or developer team, Keychain treats the new build as a different app. Pre-existing entries from old builds stay in the user's Keychain forever unless you clean them up. On uninstall, run &lt;code&gt;delete_credential&lt;/code&gt; for every key your app stores.&lt;/p&gt;

&lt;h3&gt;
  
  
  Windows
&lt;/h3&gt;

&lt;p&gt;Credential Manager is per-user. No prompt at all, ever. Items are protected by the user's Windows login. Easy mode.&lt;/p&gt;

&lt;p&gt;The catch: if a user logs into Windows with a different account, they don't see those credentials. Which is what you want, but it occasionally confuses users with multiple Windows accounts on the same machine.&lt;/p&gt;

&lt;h3&gt;
  
  
  Linux
&lt;/h3&gt;

&lt;p&gt;This is where it gets messy. The Secret Service API needs an active D-Bus session and a running provider, usually &lt;code&gt;gnome-keyring-daemon&lt;/code&gt; or &lt;code&gt;kwalletmanager&lt;/code&gt;. On a clean GNOME or KDE desktop, no problem. On a minimal i3 or sway setup, or in a CI container, there's no daemon running, and &lt;code&gt;keyring&lt;/code&gt; returns an error.&lt;/p&gt;

&lt;p&gt;I handle that case explicitly. If the keyring lookup fails on Linux, I show the user a message asking them to install gnome-keyring. Most desktop users won't hit it, but power users on bare WMs will, and silently failing is the worst possible UX.&lt;/p&gt;

&lt;p&gt;For headless or server use, the &lt;code&gt;keyring&lt;/code&gt; crate has a &lt;code&gt;mock&lt;/code&gt; feature for tests, and there are file-based fallbacks. I didn't ship those because the app is desktop-only.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd skip if I started over
&lt;/h2&gt;

&lt;p&gt;A few things I tried first and threw away:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Encrypting a JSON file with a hardcoded key in the binary.&lt;/strong&gt; Anyone with a hex editor pulls that key out. Pointless.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stronghold.&lt;/strong&gt; Not because it's bad — it's a serious piece of crypto engineering. But for a single API token, it added a password-unlock flow my users didn't want. They expected the OS to remember them, the way every other app does.&lt;/p&gt;

&lt;h2&gt;
  
  
  See it in production
&lt;/h2&gt;

&lt;p&gt;The app I built on top of all this is &lt;a href="https://time.planim.app/jira" rel="noopener noreferrer"&gt;time.planim.app&lt;/a&gt;. One-click timers on Jira issues, automatic worklog sync, calendar view, custom JQL filters — all without leaving the menu bar.&lt;/p&gt;

&lt;p&gt;If you went the Stronghold route, or built some hybrid setup, or solved this differently on Linux — drop your approach in the comments. Curious what trade-offs other people made.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;P.S.&lt;/strong&gt; — if you're shopping around for a Jira time tracker rather than building one, I followed up with &lt;a href="https://time.planim.app/blog/where-jira-time-trackers-store-your-api-token" rel="noopener noreferrer"&gt;a comparison of how five different&lt;br&gt;
  products handle this same problem&lt;/a&gt; — Tempo, Clockwork, Everhour,&lt;br&gt;
  Clockify, and Planim Time. Same threat-model lens, applied to the rest of the category, with the actual auth model each one uses.&lt;/p&gt;

</description>
      <category>tauri</category>
      <category>rust</category>
      <category>security</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
