<?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: Abdul Rafay</title>
    <description>The latest articles on DEV Community by Abdul Rafay (@rafay99).</description>
    <link>https://dev.to/rafay99</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%2F1094214%2F6168fb39-6bee-446c-a765-69acf77b7577.jpeg</url>
      <title>DEV Community: Abdul Rafay</title>
      <link>https://dev.to/rafay99</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rafay99"/>
    <language>en</language>
    <item>
      <title>Building an Android Studio Plugin is not Easy..But fun after a long time</title>
      <dc:creator>Abdul Rafay</dc:creator>
      <pubDate>Sun, 30 Aug 2026 21:11:16 +0000</pubDate>
      <link>https://dev.to/rafay99/building-an-android-studio-plugin-is-not-easybut-fun-after-a-long-time-4ga3</link>
      <guid>https://dev.to/rafay99/building-an-android-studio-plugin-is-not-easybut-fun-after-a-long-time-4ga3</guid>
      <description>&lt;p&gt;So, I have been working toward building an application called envpilot.dev for a very long time. I have VS Code extension support, Cursor support, GitHub Actions, Docker, a CLI, and a lot more, along with a web application. The codebase is getting much bigger and more complex, and yet I’ve somehow managed to make everything work in a mostly monolithic way, which is honestly pretty fascinating.&lt;/p&gt;

&lt;p&gt;But here’s the thing: I never added support for Android Studio or any of the IntelliJ products because, first of all, I don’t use an IDE, and second, I had absolutely no idea how any of it worked.&lt;/p&gt;

&lt;p&gt;I’ve been in the TypeScript and JavaScript ecosystem for so long that I genuinely had no idea how to develop an IntelliJ plugin from scratch.&lt;/p&gt;

&lt;p&gt;So, this is the rabbit hole we’re going down: how do we build a full-on IntelliJ plugin from scratch?&lt;/p&gt;

&lt;p&gt;And by “from scratch,” I mean dealing with the nightmare of setting up Gradle, downloading different versions, figuring out compatibility issues between older and newer versions of IntelliJ, dealing with preview versions, understanding the uploading and publishing process, getting through approval and security scanning, making sure I’m not using deprecated APIs, and, of course, all the other fun stuff that comes with building an IntelliJ plugin.&lt;/p&gt;

&lt;p&gt;Basically, welcome to the rabbit hole.&lt;/p&gt;

&lt;p&gt;First of all, historically, IntelliJ plugins and, by extension, plugins for Android Studio were primarily built in Java. And I haven’t touched Java since I was in college.&lt;/p&gt;

&lt;p&gt;Then Google officially announced that Kotlin would become the preferred language for building Android applications. Kotlin is a modern, strongly typed language with a lot of functional programming features, and honestly, I love that style of programming because it makes my life much easier when I’m writing code.&lt;/p&gt;

&lt;p&gt;But coming from the TypeScript and JavaScript ecosystem, getting back into the JVM world was a bit of a reality check. I’ve spent years working with TypeScript, where functional patterns are everywhere, and suddenly I had to deal with all those object-oriented concepts, classes, interfaces, inheritance, and the general structure of a JVM-based project again.&lt;/p&gt;

&lt;p&gt;It was a strangely nostalgic and memorable moment.&lt;/p&gt;

&lt;p&gt;I actually had to go back to some of my old college lectures and notes just to remind myself how things were supposed to be structured. How should I architect this application? Where does each piece of logic belong? How does the plugin lifecycle work? And, most importantly, how do I make all of this actually work together?&lt;/p&gt;

&lt;p&gt;That was essentially the first hurdle.&lt;/p&gt;

&lt;p&gt;The very first step is getting the Kotlin project initialized correctly. Thankfully, there’s a small utility provided by the IntelliJ ecosystem that can help you bootstrap a plugin project and get the initial project structure in place.&lt;/p&gt;

&lt;p&gt;And that’s where the journey really begins. Taking a completely unfamiliar ecosystem, getting the project initialized, understanding what all these generated files actually do, and slowly figuring out how an IntelliJ plugin is supposed to be built.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs6skmcreff06ph674yvb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs6skmcreff06ph674yvb.png" alt="Project Strcuture" width="556" height="806"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So, as you can see in the image above, this was the structure I got when I initially initialized the project.&lt;/p&gt;

&lt;p&gt;One important detail here is that I did not use Android Studio or IntelliJ to create this project. Honestly, I was probably being a little stupid. I did not even think about using the IDE to do the initial setup.&lt;/p&gt;

&lt;p&gt;Instead, I opened VS Code, installed the Kotlin and Android extensions, and started putting the project structure together manually. I used the CLI to figure out what needed to be installed and how everything needed to be configured. I honestly do not even remember which exact commands I used at the time to install Gradle and set everything up.&lt;/p&gt;

&lt;p&gt;A lot of this process was basically me asking the internet increasingly specific questions.&lt;/p&gt;

&lt;p&gt;How should I initialize an IntelliJ plugin project? How do I configure the SDK? How do I make the terminal recognize the SDK and the project directory I have open? Which Gradle version should I use? Which IntelliJ platform version should I target?&lt;/p&gt;

&lt;p&gt;I had Android Studio installed on my machine, but I had barely ever used it. So opening this kind of project and trying to understand what was going on was actually a really fun experience.&lt;/p&gt;

&lt;p&gt;Once you look at the project structure, though, it starts to feel surprisingly familiar if you have worked with Android projects before. You have the build directory, the Gradle directory, the source directory, and then your main Kotlin source set. Inside that, you have your packages and application code.&lt;/p&gt;

&lt;p&gt;I started organizing the project into several different areas as the codebase grew. Things like actions, authentication, configuration, Convex, additions, errors, guards, UI, sync, and so on.&lt;/p&gt;

&lt;p&gt;This was actually a significant change from the very first version of the plugin.&lt;/p&gt;

&lt;p&gt;The first version I wrote was basically me dumping everything into a few massive files. I did not really understand how an IntelliJ plugin should be structured, so I was just looking things up, integrating APIs one by one, and trying to make the application work.&lt;/p&gt;

&lt;p&gt;And, somehow, it did.&lt;/p&gt;

&lt;p&gt;But it was not good.&lt;/p&gt;

&lt;p&gt;Once I started understanding the ecosystem better, I realized that I needed to rethink the architecture instead of continuing to pile more code onto what I already had.&lt;/p&gt;

&lt;p&gt;This is where Convex became particularly interesting.&lt;/p&gt;

&lt;p&gt;My main backend service for &lt;a href="//envpilot.dev"&gt;Envpilot&lt;/a&gt; is Convex. I really like Convex because it gives me the backend primitives I need without forcing me to build and maintain a traditional REST API layer for every client. The web application, CLI, extensions, and other parts of &lt;a href="//envpilot.dev"&gt;Envpilot&lt;/a&gt; already communicate with the same backend.&lt;/p&gt;

&lt;p&gt;So I started looking into how Convex handled Android and Kotlin.&lt;/p&gt;

&lt;p&gt;And then I found out that Convex already had support for Android applications.&lt;/p&gt;

&lt;p&gt;That immediately made me think: if they already support Kotlin and Java based applications, there is a very good chance that I can use the same underlying infrastructure for an IntelliJ plugin.&lt;/p&gt;

&lt;p&gt;That was the moment when I decided to stop trying to build a completely separate backend integration for the plugin.&lt;/p&gt;

&lt;p&gt;Instead, I rewrote the entire integration around the same backend primitives that the rest of &lt;a href="//envpilot.dev"&gt;Envpilot&lt;/a&gt; uses.&lt;/p&gt;

&lt;p&gt;That meant rewriting the authentication layer, the API layer, the WebSocket communication, the real-time synchronization, and a bunch of the supporting infrastructure around them.&lt;/p&gt;

&lt;p&gt;The code I’m going to show you is open source as well. I’ll link the repository and the pull request where I worked on this implementation so you can go through the actual codebase yourself.&lt;/p&gt;

&lt;p&gt;And before we go any further, a small disclaimer.&lt;/p&gt;

&lt;p&gt;The code is not perfect.&lt;/p&gt;

&lt;p&gt;I tried my best to understand Kotlin and write what I considered to be reasonably elegant code, but I had not used Kotlin or Java seriously in a very long time. So if you are a Kotlin expert reading this and wondering why I made a particular decision, please forgive me.&lt;/p&gt;

&lt;p&gt;And if something is especially questionable, I’m going to blame AI.&lt;/p&gt;

&lt;p&gt;With that out of the way, let’s look at the three pieces that make this integration work: the Convex API layer, the Convex WebSocket implementation, and the synchronization service.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Convex API Layer
&lt;/h3&gt;

&lt;p&gt;The first thing I wanted was a small abstraction around the backend.&lt;/p&gt;

&lt;p&gt;The rest of &lt;a href="//envpilot.dev"&gt;Envpilot&lt;/a&gt; already has a set of backend operations for organizations, projects, variables, files, authentication metadata, and device linking. I did not want the IntelliJ plugin to invent an entirely new API surface just for itself.&lt;/p&gt;

&lt;p&gt;Instead, &lt;code&gt;ConvexApi&lt;/code&gt; acts as the data-plane client for the plugin.&lt;/p&gt;

&lt;p&gt;For example, fetching organizations is essentially a call to the same Convex query that other &lt;a href="//envpilot.dev"&gt;Envpilot&lt;/a&gt; clients use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;orgs&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Org&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"features/organizations/queries:listForUser"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nf"&gt;emptyMap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;parseArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="nc"&gt;Org&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"_id"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"slug"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"role"&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;The interesting part here is that the plugin is not talking to a separate REST endpoint. It is communicating directly with the Convex backend over the same socket infrastructure.&lt;/p&gt;

&lt;p&gt;The API layer then converts the raw JSON responses into Kotlin data models such as &lt;code&gt;Org&lt;/code&gt;, &lt;code&gt;Project&lt;/code&gt;, &lt;code&gt;PullMeta&lt;/code&gt;, &lt;code&gt;PullResult&lt;/code&gt;, and &lt;code&gt;SecretFileMeta&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For example, pulling variables from a project is handled through a Convex action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;pullValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt;
    &lt;span class="n"&gt;metadataOnly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;PullResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"features/variables/values:pullValues"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nf"&gt;buildMap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"projectId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;environment&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"environment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"metadataOnly"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metadataOnly&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="c1"&gt;// Parse the response and convert it into Kotlin models...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same abstraction also handles secret files, creating variable requests, linking a device, and unlinking a device.&lt;/p&gt;

&lt;p&gt;The important architectural decision here is that the plugin is not trying to recreate &lt;a href="//envpilot.dev"&gt;Envpilot's&lt;/a&gt; backend logic. It is simply another client of the existing backend.&lt;/p&gt;

&lt;p&gt;That is something I really wanted to preserve.&lt;/p&gt;

&lt;h3&gt;
  
  
  The WebSocket Layer
&lt;/h3&gt;

&lt;p&gt;The more interesting part is the WebSocket implementation.&lt;/p&gt;

&lt;p&gt;The IntelliJ plugin needs real-time synchronization. If a variable changes somewhere else, I want the IDE to know about it without requiring the user to manually refresh everything.&lt;/p&gt;

&lt;p&gt;So I implemented a &lt;code&gt;ConvexSocket&lt;/code&gt; that manages the entire WebSocket lifecycle.&lt;/p&gt;

&lt;p&gt;The lifecycle is essentially:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connect → Authenticate → Subscribe → Receive updates → Reconnect when necessary&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The socket keeps track of things like the current connection, connection count, identity version, query set version, query IDs, active subscriptions, pending actions, pending queries, and the last time a message was received from the server.&lt;/p&gt;

&lt;p&gt;One of the things I particularly wanted was for subscriptions to survive reconnects.&lt;/p&gt;

&lt;p&gt;When the socket connects, it restores the query set and resubscribes to everything that was previously active. That means the rest of the plugin does not need to constantly worry about whether the underlying connection has disappeared and come back.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;subscribe&lt;/code&gt; method is intentionally small. It creates a query ID, stores the subscription, stores its arguments, and sends the query change over the socket.&lt;/p&gt;

&lt;p&gt;For one-shot operations, I use the same socket differently.&lt;/p&gt;

&lt;p&gt;Actions and mutations are sent through the socket and then paired with a &lt;code&gt;CompletableDeferred&lt;/code&gt;. The request waits for the corresponding response, with a 30-second timeout so that the caller does not hang forever if something goes wrong.&lt;/p&gt;

&lt;p&gt;Queries work similarly, except they temporarily create a subscription, wait for the first result, and then unsubscribe.&lt;/p&gt;

&lt;p&gt;That gives me a fairly clean API from the rest of the plugin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;projects&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ConvexApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;organizationId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;variables&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ConvexApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pullValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The complicated WebSocket mechanics stay underneath the abstraction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Speaking Convex's Wire Protocol
&lt;/h3&gt;

&lt;p&gt;The final piece is &lt;code&gt;ConvexWire&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is where things get particularly interesting because I am not simply using some generic WebSocket API and hoping everything works.&lt;/p&gt;

&lt;p&gt;The plugin needs to understand the wire format that Convex uses for its synchronization protocol.&lt;/p&gt;

&lt;p&gt;So I created a small Kotlin representation of the messages that matter to the plugin.&lt;/p&gt;

&lt;p&gt;There are server messages for things like &lt;code&gt;Ping&lt;/code&gt;, &lt;code&gt;Transition&lt;/code&gt;, &lt;code&gt;AuthError&lt;/code&gt;, and &lt;code&gt;FatalError&lt;/code&gt;. There are also messages for adding and removing query subscriptions, authenticating, executing actions, and executing mutations.&lt;/p&gt;

&lt;p&gt;For example, the connection message contains the session ID, connection count, and client timestamp:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;connectMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;connectionCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;obj&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JsonObject&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;addProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Connect"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;addProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sessionId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;addProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"connectionCount"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;connectionCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"lastCloseReason"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;JsonNull&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;INSTANCE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;addProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"clientTs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;currentTimeMillis&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;gson&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&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 query subscription protocol is handled through &lt;code&gt;ModifyQuerySet&lt;/code&gt;, which lets the client add and remove subscriptions while maintaining a query set version.&lt;/p&gt;

&lt;p&gt;That matters because the plugin can have multiple projects being watched at the same time.&lt;/p&gt;

&lt;p&gt;The WebSocket layer also has to deal with authentication. When a connection is established, the plugin sends the &lt;code&gt;Connect&lt;/code&gt; message first and then authenticates before restoring the subscriptions. The implementation explicitly does this because queries sent before authentication can otherwise come back as unauthenticated failures on some deployments.&lt;/p&gt;

&lt;p&gt;And then there is the part that I really did not want to get wrong: reconnection.&lt;/p&gt;

&lt;p&gt;Connections fail. Networks disappear. Laptops sleep. VPNs change. Servers restart.&lt;/p&gt;

&lt;p&gt;So the socket implements reconnect logic with exponential backoff, capped at 30 seconds. It also has a watchdog that considers the connection unhealthy if the server has been silent for more than 60 seconds and forces a reconnect.&lt;/p&gt;

&lt;p&gt;When a socket disconnects, I also make sure that any callers currently waiting for an action or query do not remain suspended forever. Pending actions are completed with an error, and pending queries are completed exceptionally.&lt;/p&gt;

&lt;p&gt;Finally, when a &lt;code&gt;Transition&lt;/code&gt; message arrives, the wire layer determines which queries were updated and passes those events back up to the synchronization service. That is what ultimately tells Envpilot that something changed and that a sync cycle needs to run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Putting Everything Together
&lt;/h3&gt;

&lt;p&gt;The interesting part is that these three pieces have very different responsibilities.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ConvexWire&lt;/code&gt; understands the protocol.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ConvexSocket&lt;/code&gt; manages the connection and turns that protocol into something the rest of the application can use.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ConvexApi&lt;/code&gt; exposes actual &lt;a href="//envpilot.dev"&gt;Envpilot&lt;/a&gt; operations such as fetching projects, pulling variables, reading secret files, and linking devices.&lt;/p&gt;

&lt;p&gt;And above all of that sits the synchronization layer, which decides what those real-time updates actually mean for an IntelliJ project.&lt;/p&gt;

&lt;p&gt;This separation is something I absolutely did not have in the first version.&lt;/p&gt;

&lt;p&gt;The first version was essentially one giant pile of code that happened to work.&lt;/p&gt;

&lt;p&gt;This version is much closer to how I would normally structure a TypeScript application. There are clear boundaries, responsibilities are separated, and the transport layer does not need to know what a project sync actually means.&lt;/p&gt;

&lt;p&gt;And that brings me to the next rabbit hole: Kotlin itself.&lt;/p&gt;

&lt;p&gt;Because after spending years writing TypeScript, coming back to a JVM language and actually having to think in Kotlin was probably the most interesting part of this entire experiment.&lt;/p&gt;

&lt;p&gt;I had to relearn a lot of things, discover how Kotlin approaches problems that I would normally solve differently in TypeScript, and figure out where my existing programming habits translated well and where they absolutely did not.&lt;/p&gt;

&lt;p&gt;And that is what I want to dive into next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting the Plugin to Actually Feel Native
&lt;/h2&gt;

&lt;p&gt;Once the backend, authentication, synchronization, and everything else were properly configured, the next thing I focused on was the actual experience inside the IDE.&lt;/p&gt;

&lt;p&gt;The UI and UX were already working surprisingly well. Project linking was working, pulling files from the development server was working, pulling metadata and variables was working, and all the major pieces were finally coming together.&lt;/p&gt;

&lt;p&gt;At that point, I was pretty happy with the first real state of the application.&lt;/p&gt;

&lt;p&gt;Sure, there were still some missing icons and a few visual details that needed work, but those were relatively easy to solve. I already had the SVGs from the other &lt;a href="//envpilot.dev"&gt;Envpilot&lt;/a&gt; clients, so I just had to redefine them as components that could be used inside the Kotlin application. After making a few small adjustments to sizing and spacing, everything started rendering properly.&lt;/p&gt;

&lt;p&gt;And honestly, it went much better than I expected.&lt;/p&gt;

&lt;p&gt;The plugin started blending into the IntelliJ interface instead of looking like something that had been bolted onto the side of the IDE.&lt;/p&gt;

&lt;p&gt;That was very important to me.&lt;/p&gt;

&lt;p&gt;I did not want the IntelliJ plugin to feel like a completely different product. My goal has always been to have one-to-one feature parity between the VS Code extension, the Cursor extension, and now the IntelliJ plugin, while still making each integration feel native to its respective environment.&lt;/p&gt;

&lt;p&gt;There are obviously some differences between the platforms, but the core experience should remain the same.&lt;/p&gt;

&lt;h3&gt;
  
  
  Kotlin Was Surprisingly Nice
&lt;/h3&gt;

&lt;p&gt;One of the things that surprised me during this process was how much I started enjoying Kotlin.&lt;/p&gt;

&lt;p&gt;Coming from TypeScript, I initially expected Kotlin to feel much more foreign than it actually did. There are a lot of concepts that felt familiar, especially when working with functions, collections, nullable values, and the general ability to express fairly complex operations without writing a huge amount of code.&lt;/p&gt;

&lt;p&gt;The IntelliJ ecosystem also makes it surprisingly easy to work with the platform APIs.&lt;/p&gt;

&lt;p&gt;I can write a relatively small amount of code, import the APIs I need, and then build fairly sophisticated editor behavior on top of them.&lt;/p&gt;

&lt;p&gt;For example, one of the features I wanted to bring over from the VS Code and Cursor extensions was something I call &lt;strong&gt;Env Cloak&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The idea is simple.&lt;/p&gt;

&lt;p&gt;When you open an &lt;code&gt;.env&lt;/code&gt; file inside the editor, &lt;a href="//envpilot.dev"&gt;Envpilot&lt;/a&gt; can detect which values are managed secrets and hide those values directly in the editor. Instead of displaying the actual secret, the editor renders a placeholder.&lt;/p&gt;

&lt;p&gt;This is not just a cosmetic change where the text is recolored or made transparent. The plugin actually creates folding regions at the editor level, so the secret value is not rendered normally at all.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;.env&lt;/code&gt; files, managed values are replaced with a placeholder such as:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;For other managed secret files, such as JSON, PEM, or keystore files, the plugin can fold the entire document and display a locked placeholder instead.&lt;/p&gt;

&lt;p&gt;The implementation is fairly straightforward once you understand the IntelliJ editor APIs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;editor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foldingModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addFoldRegion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;range&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;range&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;ENV_PLACEHOLDER&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;isExpanded&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The plugin identifies the values that belong to the linked &lt;a href="//envpilot.dev"&gt;Envpilot&lt;/a&gt; project and then creates fold regions around those values.&lt;/p&gt;

&lt;p&gt;There is also a document listener attached to the editor, so when the document changes, the cloak can be refreshed automatically.&lt;/p&gt;

&lt;p&gt;This is one of those features that sounds complicated when you describe it, but once you understand the platform API, the implementation becomes surprisingly clean.&lt;/p&gt;

&lt;p&gt;And this is something I really like about Kotlin and the IntelliJ platform.&lt;/p&gt;

&lt;p&gt;The APIs are expressive enough that I can describe what I want the editor to do without having to build an enormous abstraction layer around it.&lt;/p&gt;

&lt;h3&gt;
  
  
  There Is Still Work to Do
&lt;/h3&gt;

&lt;p&gt;Of course, the IntelliJ plugin is not at complete feature parity with the VS Code and Cursor extensions yet.&lt;/p&gt;

&lt;p&gt;There are still a few things missing.&lt;/p&gt;

&lt;p&gt;For example, the webview-based application experience is not currently available in the Kotlin plugin. The inline autocomplete experience for importing environment variables is also not implemented yet.&lt;/p&gt;

&lt;p&gt;Those are things I plan to work on in the coming iterations.&lt;/p&gt;

&lt;p&gt;But the core functionality is already there.&lt;/p&gt;

&lt;p&gt;Authentication works. Project linking works. Variable synchronization works. File pulling works. Secret protection works. The error handling infrastructure works. And the platform gives me a lot of the pieces I need to continue building the remaining features.&lt;/p&gt;

&lt;p&gt;That brings me to another thing I really appreciate about the IntelliJ ecosystem: the platform itself takes a lot of responsibility for things that I would otherwise have to build manually.&lt;/p&gt;

&lt;h3&gt;
  
  
  Error Handling and Developer Experience
&lt;/h3&gt;

&lt;p&gt;Some of the error handling and global error tracking that I already use throughout &lt;a href="//envpilot.dev"&gt;Envpilot&lt;/a&gt; translated surprisingly well into the plugin.&lt;/p&gt;

&lt;p&gt;The platform has solid support for handling errors, tracking application state, managing services, and integrating those things into the IDE lifecycle.&lt;/p&gt;

&lt;p&gt;I had honestly underestimated how useful that would be.&lt;/p&gt;

&lt;p&gt;When you are building a JavaScript or TypeScript application, especially one with a large and increasingly complicated codebase, maintaining consistent error handling and application-wide tracking can become a pain.&lt;/p&gt;

&lt;p&gt;There are a lot of different execution paths, environments, integrations, and edge cases.&lt;/p&gt;

&lt;p&gt;The IntelliJ platform provides a lot of structure around these things.&lt;/p&gt;

&lt;p&gt;That means I can focus more on the actual &lt;a href="//envpilot.dev"&gt;Envpilot&lt;/a&gt; functionality instead of constantly worrying about how every single piece of infrastructure needs to be wired together.&lt;/p&gt;

&lt;h3&gt;
  
  
  And Then Came Compatibility
&lt;/h3&gt;

&lt;p&gt;The next challenge was something I had not really considered when I started this project.&lt;/p&gt;

&lt;p&gt;IntelliJ is not just one version.&lt;/p&gt;

&lt;p&gt;There are different IntelliJ platform versions, different IDE versions, preview releases, and changes to the APIs over time. So even after getting the plugin working locally, I still had to make sure that it actually worked across the versions I wanted to support.&lt;/p&gt;

&lt;p&gt;This is where the JetBrains plugin marketplace tooling became really interesting.&lt;/p&gt;

&lt;p&gt;As I started preparing the plugin for publishing, I could run compatibility verification against different IntelliJ IDEA versions.&lt;/p&gt;

&lt;p&gt;And this is where the screenshot above comes in.&lt;/p&gt;

&lt;p&gt;You can see several versions of the plugin listed, with compatibility verification results for different IntelliJ IDEA versions. The current version was successfully verified against multiple IDE versions, and the results were all green.&lt;/p&gt;

&lt;p&gt;That was not always the case.&lt;/p&gt;

&lt;p&gt;During the earlier versions of the plugin, I ran into compatibility issues and deprecated APIs. I had to go through the codebase, figure out which APIs were no longer recommended, replace them, and then run the verification again.&lt;/p&gt;

&lt;p&gt;Eventually, I got to the point where the compatibility checks were passing cleanly.&lt;/p&gt;

&lt;p&gt;And I was genuinely impressed by this.&lt;/p&gt;

&lt;p&gt;The JetBrains ecosystem does a really good job of telling you which APIs are deprecated, which APIs are compatible with specific platform versions, and whether your plugin is actually behaving correctly when it runs inside a particular IDE version.&lt;/p&gt;

&lt;p&gt;The compatibility verification process essentially gives you another layer of confidence before you publish the plugin.&lt;/p&gt;

&lt;p&gt;The current plugin version was going through verification against multiple IntelliJ versions, and seeing those green "Compatible" results was one of those small moments that made the whole process feel real.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxmvozn09oiks5oi5puvr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxmvozn09oiks5oi5puvr.png" alt="Plugin Compatibility  " width="800" height="502"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This was no longer just a plugin running on my machine.&lt;/p&gt;

&lt;p&gt;I was actually preparing it for the IntelliJ Marketplace.&lt;/p&gt;

&lt;p&gt;And that introduced an entirely new set of problems to solve.&lt;/p&gt;

&lt;p&gt;I now had to think about plugin signing, marketplace publishing, compatibility verification, security scanning, deprecated APIs, version ranges, review, and everything else that comes with distributing a plugin to other developers.&lt;/p&gt;

&lt;p&gt;That entire publishing process deserves its own section, because this is where building an IntelliJ plugin went from being a fun experiment into something that actually felt like shipping a real product.&lt;/p&gt;

&lt;p&gt;That is it from my side today. If you are really interested into the application that I'm building, then check out envpilot.dev and if you  want to reach out then email me at &lt;a href="mailto:99marafay@gmail.com"&gt;99marafay@gmail.com&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Until them I am fixing more bugs... &lt;/p&gt;

</description>
      <category>coding</category>
      <category>kotlin</category>
      <category>typescript</category>
      <category>convex</category>
    </item>
    <item>
      <title>dotenv-vault Is Deprecated — Here's Your Migration Path</title>
      <dc:creator>Abdul Rafay</dc:creator>
      <pubDate>Mon, 20 Jul 2026 09:39:11 +0000</pubDate>
      <link>https://dev.to/rafay99/dotenv-vault-is-deprecated-heres-your-migration-path-21ke</link>
      <guid>https://dev.to/rafay99/dotenv-vault-is-deprecated-heres-your-migration-path-21ke</guid>
      <description>&lt;p&gt;If your team syncs environment variables with &lt;code&gt;dotenv-vault&lt;/code&gt;, you've probably seen the notices by now: the project is deprecated. The &lt;code&gt;.env.vault&lt;/code&gt; file format is &lt;a href="https://dotenvx.com/docs/env-vault-file" rel="noopener noreferrer"&gt;officially marked deprecated&lt;/a&gt;, &lt;code&gt;dotenv-vault-core&lt;/code&gt; has pointed users elsewhere for a while, and active development has moved to &lt;a href="https://dotenvx.com/" rel="noopener noreferrer"&gt;dotenvx&lt;/a&gt; — built by Mot, the same developer who created the original &lt;code&gt;dotenv&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Nothing is going to break tomorrow. But building on a deprecated secrets format is a slow leak: no fixes, no new integrations, and every new teammate asking "wait, why are we using this?" Here's an honest map of where to go.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you actually lose
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;dotenv-vault&lt;/code&gt; did three jobs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Encrypted your .env files&lt;/strong&gt; into a &lt;code&gt;.env.vault&lt;/code&gt; file you could commit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Synced them&lt;/strong&gt; across the team through dotenv.org&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separated environments&lt;/strong&gt; (development, staging, production) with one key each&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Any replacement needs to cover those three jobs — and ideally fix the things that were always awkward: key distribution still happened out-of-band, there was no per-person access control, and no record of who read what.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 1: dotenvx (the official successor)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://dotenvx.com/" rel="noopener noreferrer"&gt;dotenvx&lt;/a&gt; is the path the dotenv creator recommends, and there's an &lt;a href="https://github.com/dotenvx/dotenvx-ext-vault" rel="noopener noreferrer"&gt;official migration extension&lt;/a&gt; for converting &lt;code&gt;.env.vault&lt;/code&gt; files.&lt;/p&gt;

&lt;p&gt;Its model: encrypted &lt;code&gt;.env&lt;/code&gt; files live in your repo, decrypted at runtime with a private key. It's free, open source, works with any language, and requires no account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it fits:&lt;/strong&gt; solo developers and teams comfortable with encrypted-files-in-git. &lt;strong&gt;Where it doesn't:&lt;/strong&gt; the private key still has to reach every teammate and every CI job through some other channel — which for most small teams means it gets pasted into Slack, the exact problem you were trying to solve. There's also no per-person access control and no audit trail; whoever holds the key holds everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 2: hosted secrets managers (Doppler, Infisical)
&lt;/h2&gt;

&lt;p&gt;Both are mature, both solve team sync properly, both price &lt;strong&gt;per seat&lt;/strong&gt; — Doppler's Team plan runs &lt;a href="https://www.doppler.com/pricing" rel="noopener noreferrer"&gt;$21/user/month&lt;/a&gt;, Infisical's Pro is &lt;a href="https://infisical.com/pricing" rel="noopener noreferrer"&gt;$18/identity/month&lt;/a&gt; where machines count as identities too. For a six-person team that's $1,300–1,500 a year, and machine identities push it higher. We keep honest comparisons of both, including where they beat us: &lt;a href="https://www.envpilot.dev/vs/doppler" rel="noopener noreferrer"&gt;Envpilot vs Doppler&lt;/a&gt; and &lt;a href="https://www.envpilot.dev/vs/infisical" rel="noopener noreferrer"&gt;Envpilot vs Infisical&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 3: Envpilot (what we built)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.envpilot.dev" rel="noopener noreferrer"&gt;Envpilot&lt;/a&gt; is an open-source (MIT) environment variable manager built for exactly the team that used dotenv-vault: small, moving fast, no dedicated ops person.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No files in the repo at all.&lt;/strong&gt; &lt;code&gt;envpilot run -- npm run dev&lt;/code&gt; injects variables straight into the process; nothing secret touches disk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team sync with actual access control&lt;/strong&gt; — role-based, per-variable permissions, and an audit log of who read what, when, from where.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-environment values&lt;/strong&gt; with enforced key uniqueness — &lt;code&gt;DATABASE_URL&lt;/code&gt; for development and production are separate variables that can never silently collide.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flat pricing.&lt;/strong&gt; Free for 3 teammates and 3 projects; the paid tier is one flat price per organization, not per seat. Adding your seventh developer costs nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encrypted in a dedicated vault&lt;/strong&gt; — our database stores only reference IDs, never plaintext secret values. &lt;a href="https://github.com/rafay99-epic/envpilot.dev" rel="noopener noreferrer"&gt;The whole platform is MIT-licensed on GitHub&lt;/a&gt;, so you can read exactly how that works.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The 10-minute migration
&lt;/h2&gt;

&lt;p&gt;You don't need to convert your &lt;code&gt;.env.vault&lt;/code&gt; file — you need the decrypted values you already have locally.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Install and sign in (browser SSO, no API keys in shell history)&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @envpilot/cli
envpilot login

&lt;span class="c"&gt;# 2. Link the project folder&lt;/span&gt;
envpilot init

&lt;span class="c"&gt;# 3. Push your existing .env into an environment&lt;/span&gt;
envpilot push &lt;span class="nt"&gt;--env&lt;/span&gt; development
envpilot push &lt;span class="nt"&gt;--env&lt;/span&gt; production &lt;span class="nt"&gt;--file&lt;/span&gt; .env.production

&lt;span class="c"&gt;# 4. Verify, then delete the files&lt;/span&gt;
envpilot run &lt;span class="nt"&gt;--&lt;/span&gt; npm run dev
&lt;span class="nb"&gt;rm&lt;/span&gt; .env .env.production .env.vault .env.keys
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Teammates run steps 1–2 and they're synced — no key to hand them, because access is tied to their account, not a shared secret. When someone leaves the team, you revoke the person, not rotate every key they ever touched.&lt;/p&gt;

&lt;p&gt;CI needs variables too: the &lt;a href="https://docs.envpilot.dev/github-action" rel="noopener noreferrer"&gt;GitHub Action&lt;/a&gt; pulls them with a scoped token and masks every value in workflow logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one should you pick?
&lt;/h2&gt;

&lt;p&gt;Honest version: if you're solo and like the encrypted-files-in-git model, &lt;strong&gt;dotenvx&lt;/strong&gt; is free and official — use it. If you're an enterprise with compliance requirements and budget, Doppler and Infisical are proven. If you're a small team that mostly wants to stop pasting &lt;code&gt;.env&lt;/code&gt; files into Slack and know who touched what, that's the exact gap &lt;a href="https://www.envpilot.dev" rel="noopener noreferrer"&gt;we built Envpilot for&lt;/a&gt; — and the free tier covers a 3-person team end to end, no credit card.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>javascript</category>
      <category>node</category>
      <category>security</category>
    </item>
    <item>
      <title>Idea Code App: My Build Story</title>
      <dc:creator>Abdul Rafay</dc:creator>
      <pubDate>Fri, 28 Mar 2025 09:49:06 +0000</pubDate>
      <link>https://dev.to/rafay99/idea-code-app-my-build-story-2075</link>
      <guid>https://dev.to/rafay99/idea-code-app-my-build-story-2075</guid>
      <description>&lt;p&gt;Alright, let's hear it. We've all been there—you have an idea for an app that could help you out, and you realize it could help others as well. But you don’t know how to create a good application, or moreover, you have no idea how to build one.&lt;/p&gt;

&lt;p&gt;Now, you go on YouTube and start searching, and the number of videos is overwhelming. You watch one, then two, but when it comes down to actually starting from nothing, things start to get a little rough.&lt;/p&gt;

&lt;p&gt;You don’t know the app flow, how users will interact with it, or even what features you need. And that’s perfectly normal. As a beginner trying to build an app, you don’t yet understand how things should flow.&lt;/p&gt;

&lt;p&gt;The purpose of this blog post is to show you all how to build an app from scratch. I’ll go through everything—from the idea to development, all the bugs I encountered, how to solve them, and how AI can be helpful.&lt;/p&gt;

&lt;h1&gt;
  
  
  The Idea?
&lt;/h1&gt;

&lt;p&gt;For this app, I was writing my notes on a website and could access them via the web. But what about mobile? It was really inconvenient to open a browser, enter a URL, and then visit my notes. And sometimes, the internet isn’t reliable. So, now what?&lt;/p&gt;

&lt;p&gt;The solution for me was simple—I needed an app where I could read all my lecture notes with offline access.&lt;/p&gt;

&lt;p&gt;With that in mind, I started writing down what I needed. The core idea was that I should be able to read my notes without an internet connection. My website already has API access, so I could call that API and render the content in the app, right?&lt;/p&gt;

&lt;p&gt;It’s simple and can be done.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Features
&lt;/h2&gt;

&lt;p&gt;Once the core idea was set, it was time to think about what features would support it and ensure the app wasn’t misused. So, I added these features:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Offline &amp;amp; Online Access&lt;/li&gt;
&lt;li&gt;Auth System&lt;/li&gt;
&lt;li&gt;Color Choices&lt;/li&gt;
&lt;li&gt;Note-Taking&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These features would make note reading much smoother and more convenient.&lt;/p&gt;

&lt;h1&gt;
  
  
  Tech Stack
&lt;/h1&gt;

&lt;p&gt;Once you have all the features listed and know what you want, the next step is choosing a good tech stack. People say it doesn’t matter what tech stack you choose, but let me tell you—when it comes to apps, you generally have four options for the frontend:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Flutter&lt;/li&gt;
&lt;li&gt;React Native&lt;/li&gt;
&lt;li&gt;Java (Android)&lt;/li&gt;
&lt;li&gt;Swift (iOS)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And for the backend, there are several good options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Firebase&lt;/li&gt;
&lt;li&gt;Supabase&lt;/li&gt;
&lt;li&gt;Appwrite&lt;/li&gt;
&lt;li&gt;PocketBase&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All of these options have their own pros and cons. If you’d like to see a core difference between Firebase and Supabase, check out my previous &lt;a href="https://www.rafay99.com/blog/firebase-vs-supperbase/" rel="noopener noreferrer"&gt;blog post&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I haven’t used PocketBase or Appwrite yet, but I’ve heard they are good options.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Choice
&lt;/h2&gt;

&lt;p&gt;For me, I’ll be using Flutter for the UI and Appwrite for the backend. The backend will handle user sync, note-taking, user authentication, and the auth gate—nothing more than that. The UI will be coded using Flutter because I use Flutter for everything, and I love its flexibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your Choice
&lt;/h2&gt;

&lt;p&gt;Don’t choose blindly—do your research and pick what makes you comfortable and what you feel confident coding in. If I weren’t used to Flutter, I would have gone with React Native, since I know how React works but have no experience with React Native. So, choose what works best for you.&lt;/p&gt;

&lt;h1&gt;
  
  
  Defining The Flow
&lt;/h1&gt;

&lt;p&gt;You know the features, you know how to choose a tech stack, now it's time to define the flow. By flow, I mean how the user will navigate from one page to another. And this is actually very simple. When a user installs your application, what will they see first? It could be a Login Screen, an Introduction Screen, a Splash Screen, anything—it all depends on your thought process and your application.&lt;/p&gt;

&lt;p&gt;Here’s the flow I created for my application:&lt;/p&gt;

&lt;h2&gt;
  
  
  The Auth Flow
&lt;/h2&gt;

&lt;p&gt;The auth flow shows how the authentication process is handled in the app.&lt;/p&gt;

&lt;h3&gt;
  
  
  Login Flow
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Installed -&amp;gt; Introduction Screen -&amp;gt; Login -&amp;gt; Success -&amp;gt; Home Screen&lt;/li&gt;
&lt;li&gt;Installed -&amp;gt; Introduction Screen -&amp;gt; Login -&amp;gt; Failed -&amp;gt; Error Message&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Register Flow
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Installed -&amp;gt; Introduction Screen -&amp;gt; Register -&amp;gt; User Created -&amp;gt; Login -&amp;gt; Success -&amp;gt; Home Screen&lt;/li&gt;
&lt;li&gt;Installed -&amp;gt; Introduction Screen -&amp;gt; Register -&amp;gt; User NOT Created -&amp;gt; Error Message&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Auth Gate
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;User Logged In -&amp;gt; Home Screen&lt;/li&gt;
&lt;li&gt;User Not Logged In -&amp;gt; Login Screen&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Note Fetching Feature
&lt;/h2&gt;

&lt;p&gt;This flow illustrates how the app operates under the hood.&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting Notes via API
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;App Opened -&amp;gt; Internet Connection -&amp;gt; Call API (Get Data) -&amp;gt; Store in Local Database -&amp;gt; Show to User&lt;/li&gt;
&lt;li&gt;App Opened -&amp;gt; NO Internet Connection -&amp;gt; Switch to Offline Mode&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Taking Notes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Open App -&amp;gt; Switch to Note-Taking UI -&amp;gt; Create Note -&amp;gt; Write Some Content -&amp;gt; Save Content&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Flow Thoughts
&lt;/h2&gt;

&lt;p&gt;You can see how the app flow is structured. This is just the beginning. As you add more features, you’ll naturally have more flows to define. It’s better to set up a structure early and start coding, because as you develop, the flow will reveal itself more clearly.&lt;/p&gt;

&lt;p&gt;I’m only able to describe this flow because I’ve actually done it—I’ve built the app, faced real challenges, and figured out how each UI should connect to the next.&lt;/p&gt;

&lt;h1&gt;
  
  
  Coding
&lt;/h1&gt;

&lt;p&gt;Okay, you have everything set up. Now what? This is the question most people don’t know how to answer. You’ve got the design, the flow, the idea, and the tech stack. What’s next?&lt;/p&gt;

&lt;p&gt;Here’s the short answer: Google and use AI.&lt;/p&gt;

&lt;p&gt;In the beginning, Google &lt;em&gt;a lot&lt;/em&gt;. Don’t just rely on AI—you’ll forget what you learn that way. Google, Stack Overflow, documentation—they'll stick in your brain better. Watch some tutorials to get a solid foundation. And then... start coding.&lt;/p&gt;

&lt;h1&gt;
  
  
  My App Issues (The Rollercoaster)
&lt;/h1&gt;

&lt;p&gt;Now, let me be honest. It wasn’t all sunshine and rainbows.&lt;/p&gt;

&lt;p&gt;I initially aimed to finish the app in a week. If you look at my GitHub, it did take me about a week—but, oh boy, it wasn’t easy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Appwrite Woes
&lt;/h3&gt;

&lt;p&gt;Everything started fine. I created the UI, set up Appwrite as the backend, and things were rolling smoothly. Until... Appwrite refused to add users to my database. I tried everything, but the issues just kept stacking up. So, I decided: forget this, I’m switching to Firebase.&lt;/p&gt;

&lt;p&gt;Thankfully, I had separated my UI from my backend. All I had to do was change the backend implementation. Nothing broke in the frontend—lesson learned: keep frontend and backend loosely coupled via a repository file where all the logic meets.&lt;/p&gt;

&lt;h3&gt;
  
  
  API Struggles &amp;amp; Hive Mastery
&lt;/h3&gt;

&lt;p&gt;Once Firebase was up and running, it was time to fetch notes from my website's API. I used the &lt;code&gt;http&lt;/code&gt; package from pub.dev, which worked fine. But for some reason, the body of the notes wasn’t coming through.&lt;/p&gt;

&lt;p&gt;To handle offline data, I integrated Hive, which was great, but initially tricky to set up. I created models, commands, and started syncing API data to Hive. The issue? Still no note body! Eventually, after hours of frustration, it turned out the API itself had the problem, not Hive.&lt;/p&gt;

&lt;p&gt;Once fixed, data was flowing and saving properly.&lt;/p&gt;

&lt;h3&gt;
  
  
  UI Layer &amp;amp; Card-Based Madness
&lt;/h3&gt;

&lt;p&gt;Next challenge: UI design. There were multiple subjects, multiple lectures, and I wanted a card-based interface. One card leads to another, leading to lecture notes at the end. I made templates to dynamically pass data—whether it's research notes, lecture notes, etc.—and the experience became seamless.&lt;/p&gt;

&lt;h3&gt;
  
  
  Search: Fizzy Finder or Bust
&lt;/h3&gt;

&lt;p&gt;Search was another beast. Firebase’s built-in search? Nah, it won’t cut it. I wanted powerful search capabilities, so I turned to Fizzy Finder and built my own search functionality. This was crucial for scaling.&lt;/p&gt;

&lt;h1&gt;
  
  
  Note-Taking Feature (The Hell Moment)
&lt;/h1&gt;

&lt;p&gt;After deploying V1, I decided to add a note-taking module. The big question: What editor? Markdown, Rich Text, something else? I found &lt;a href="https://pub.dev/packages/flutter_quill" rel="noopener noreferrer"&gt;Flutter Quill&lt;/a&gt;—perfect!&lt;/p&gt;

&lt;p&gt;Or so I thought...&lt;/p&gt;

&lt;p&gt;I installed it, ran the emulator... and everything broke. Dependency hell. Turns out Flutter Quill v2.9 had conflicts with my Introduction Screen package. I had to pick one. So, Introduction Screen had to go. I rebuilt the entire introduction UI from scratch with custom animations and flows. Painful—but fun.&lt;/p&gt;

&lt;p&gt;Once that was sorted, Hive became my best friend again. I created &lt;code&gt;Hive Operations&lt;/code&gt; and &lt;code&gt;Note Taking Actions&lt;/code&gt; services. I set up controllers, wired everything up, and data was being saved smoothly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Google Keep-Inspired UI
&lt;/h3&gt;

&lt;p&gt;For displaying notes, I wanted something like Google Keep. I wasn’t sure how to achieve this, so I turned to Google Studio AI—it generated a UI I didn’t &lt;em&gt;love&lt;/em&gt;, but hey, it worked.&lt;/p&gt;

&lt;h3&gt;
  
  
  CRUD’s Biggest Nightmare: Deleting Notes
&lt;/h3&gt;

&lt;p&gt;Deleting notes became my nemesis. I wanted a recycle bin feature. Hive doesn’t support moving content between boxes. So, I had to manually copy content from one Hive box to another, then delete it, and vice versa if restoring. Hive team, if you’re reading: please add move support—it would make life so much easier!&lt;/p&gt;

&lt;h1&gt;
  
  
  The Sync Layer (The Final Boss 💀)
&lt;/h1&gt;

&lt;p&gt;One thing left: the sync layer.&lt;/p&gt;

&lt;p&gt;All notes were stored locally, but users needed backups in the cloud. Easy in theory, painful in practice.&lt;/p&gt;

&lt;p&gt;Where should sync happen? How should users know if their data is backed up? At first, I triggered sync on save, update, delete—but it slowed down the app by 2-5 seconds. Not acceptable.&lt;/p&gt;

&lt;p&gt;Solution? Sync runs when the app launches. It detects changes and pushes them to Firebase. I added settings to give users control over sync, and made sure sync runs off the main thread so the UI stays smooth. (Shoutout to AI for helping me figure this out—I was lost!)&lt;/p&gt;

&lt;h1&gt;
  
  
  Wrapping It Up
&lt;/h1&gt;

&lt;p&gt;After all the battles, late nights, and debug sessions, I finally had a polished app. I called it &lt;strong&gt;MS Bridge&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Every flow, feature, and issue taught me something valuable. And trust me, whatever app you’re building, you’ll hit similar hurdles. Just stay patient, keep separating concerns, Google like crazy, and enjoy the ride!&lt;/p&gt;

&lt;h1&gt;
  
  
  Try It Out Yourself
&lt;/h1&gt;

&lt;h3&gt;
  
  
  Download App
&lt;/h3&gt;

&lt;p&gt;The app isn’t live on the Google Play Store just yet, but you can still give it a spin!&lt;br&gt;
Simply head over to my website or download it directly using the link below:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="http://rafay99.com/downloads/app/msbridge/release/MSBridge-release.apk" rel="noopener noreferrer"&gt;Download MSBridge APK&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
   Explore the Codebase
&lt;/h3&gt;

&lt;p&gt;Curious to see how I built this? Check out the entire codebase on GitHub:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/rafay99-epic/MSBridge" rel="noopener noreferrer"&gt;MSBridge GitHub Repository&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I’d love to hear your thoughts!&lt;br&gt;
Tell me how I coded, what you liked (or didn’t), and how badly I might have messed things up 😄.&lt;br&gt;
Drop your feedback, suggestions, or even a roast in the comment section—I’m all ears! 👇&lt;/p&gt;

&lt;h1&gt;
  
  
  Final Thoughts
&lt;/h1&gt;

&lt;p&gt;Now, it’s time for some final thoughts. I did this not just because I’m getting a blog post and a future video out of it, but more importantly—because it was fun. Coding for fun is something I’ve missed, and writing down my thoughts, sharing my experiences—that’s something I truly enjoy.&lt;/p&gt;

&lt;p&gt;Here’s the thing: don’t be afraid. You’re an engineer. You’re &lt;em&gt;meant&lt;/em&gt; to break things, to see how they unfold, to figure out how things can be done. Coding is fun. Find an idea, and start building. It might look like chaos at times—some parts definitely felt like that—but I genuinely enjoyed every bit of it.&lt;/p&gt;

&lt;p&gt;So go on, start coding.&lt;/p&gt;

&lt;p&gt;Until then, goodbye nerds.&lt;/p&gt;




&lt;p&gt;Check out the original article at &lt;a href="https://www.rafay99.com/blog/idea_to_app/" rel="noopener noreferrer"&gt;rafay99.com&lt;/a&gt; for the full story!&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
