<?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: Arvin Gopi</title>
    <description>The latest articles on DEV Community by Arvin Gopi (@arvin_gopi).</description>
    <link>https://dev.to/arvin_gopi</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%2F3909848%2Fac6b8661-b739-42a0-96c6-4f34f8fc2542.png</url>
      <title>DEV Community: Arvin Gopi</title>
      <link>https://dev.to/arvin_gopi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/arvin_gopi"/>
    <language>en</language>
    <item>
      <title>Your .env file should have one line</title>
      <dc:creator>Arvin Gopi</dc:creator>
      <pubDate>Sun, 03 May 2026 05:05:18 +0000</pubDate>
      <link>https://dev.to/arvin_gopi/your-env-file-should-have-one-line-598</link>
      <guid>https://dev.to/arvin_gopi/your-env-file-should-have-one-line-598</guid>
      <description>&lt;p&gt;Every AI app I've shipped recently rewrote the same plumbing. The OAuth dance for Slack. Encrypted storage for an API key. Refresh-token logic that finally fails on the 3rd call after an hour. Wiring up an MCP client to a server behind a bearer token someone pasted into a Notion page.                                                                                                                                                                                                              &lt;/p&gt;

&lt;p&gt;I'd write it, copy-paste it into the next app, watch it rot. Each new agent built by a different teammate, slightly differently, with slightly different bugs. We were a small team and the integration code became most of the code.                   &lt;/p&gt;

&lt;p&gt;## The pattern under all of it                                                                                                                                                                                                                          &lt;/p&gt;

&lt;p&gt;Strip away the providers and the AI-specific bits, and every app needed the same four things from the platform:                                                                                                                                         &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Env vars&lt;/strong&gt; — a database URL, a Stripe key, the boring stuff. Not in a &lt;code&gt;.env&lt;/code&gt; file in a Docker image. Not in a CI secret. Somewhere the app can ask for at runtime.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-built integrations&lt;/strong&gt; — Gmail, Calendar, Drive. The user logs in once on the platform; every app gets typed access on their behalf.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom OAuth&lt;/strong&gt; — the providers no platform pre-builds. Slack, Notion, the company's SSO. The customer holds the &lt;code&gt;client_id&lt;/code&gt;/&lt;code&gt;secret&lt;/code&gt;; their app shouldn't.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom MCP&lt;/strong&gt; — internal MCP servers, third-party MCPs. The customer holds the URL and the bearer token; their app shouldn't.
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's the spine of &lt;a href="https://leash.build/docs/sdk/overview" rel="noopener noreferrer"&gt;the SDK we ended up shipping&lt;/a&gt;. Four primitives, every app uses some of them, none of them require integration code in the app.                                                              &lt;/p&gt;

&lt;p&gt;## Register once at the org level                                                                                                                                                                                                                       &lt;/p&gt;

&lt;p&gt;The flip is registration. The org owner registers their things one time on the dashboard:                                                                                                                                                               &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Drop a Slack &lt;code&gt;client_id&lt;/code&gt; + &lt;code&gt;client_secret&lt;/code&gt; into the "Custom OAuth providers" card. Encrypted with the org's KMS key. The app never sees it.
&lt;/li&gt;
&lt;li&gt;Drop the URL of an internal MCP server + a bearer token into the "Custom MCP servers" card. Same treatment.
&lt;/li&gt;
&lt;li&gt;Connect Doppler / 1Password / GCP Secret Manager as a secret source — or just type secrets into the dashboard.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now every app you deploy in that org gets typed access through four SDK calls.                                                                                                                                                                          &lt;/p&gt;

&lt;p&gt;## The four calls&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;LeashIntegrations&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@leash/sdk/integrations&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;                                                                                                                                                                                             

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LeashIntegrations&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LEASH_API_KEY&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;                                                                                                                                                                             

  &lt;span class="c1"&gt;// 1. Env var (resolves through your configured secret source)                                                                                                                                                                                          &lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dbUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getEnv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                                                                                                                                                                                       

  &lt;span class="c1"&gt;// 2. Pre-built integration                                                                                                                                                                                                                             &lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gmail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listMessages&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;maxResults&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;                                                                                                                                                                                     

  &lt;span class="c1"&gt;// 3. Custom OAuth — fresh access token for any provider you've registered                                                                                                                                                                              &lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;slackToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAccessToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;slack&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// 4. Custom MCP — { url, headers } including bearer Authorization                                                                                                                                                                                    &lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCustomMcpConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;acme-tools&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                                                                                                                                                                               
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same shape across TypeScript, Python, Go, Ruby, Rust, and Java. No &lt;code&gt;client_secret&lt;/code&gt; in the app code. No refresh-token handler. No MCP boilerplate.&lt;/p&gt;

&lt;p&gt;## Your &lt;code&gt;.env&lt;/code&gt; collapses to one line                                                                                                                                                                                                                  &lt;/p&gt;

&lt;p&gt;The thing we noticed only after living with it: once you're using this, the only secret your app's &lt;code&gt;.env&lt;/code&gt; actually needs is the platform API key.&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;# .env  (yes, this is the whole thing)                                                                                                                                                                                                                  &lt;/span&gt;
  &lt;span class="nv"&gt;LEASH_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;lsk_live_...                                                                                                                                                                                                                              
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No more &lt;code&gt;.env.example&lt;/code&gt; drift. No more "did we set &lt;code&gt;DATABASE_URL&lt;/code&gt; in staging?" debugging at 11pm. Rotation happens at the source — no rebuild, no redeploy.                                                                                   &lt;/p&gt;

&lt;p&gt;## What it deliberately doesn't do                                                                                                                                                                                                                      &lt;/p&gt;

&lt;p&gt;A few decisions that came up that I'll defend:                                                                                                                                                                                                          &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Doesn't proxy MCP traffic.&lt;/strong&gt; We hand the app &lt;code&gt;{url, headers}&lt;/code&gt; (with bearer Authorization already attached) and the app calls the MCP directly. Leash isn't in the request path. Tool calls are on the LLM's critical path; an extra hop hurts. We also&lt;br&gt;
   didn't want to reimplement every MCP transport (streamable HTTP, SSE, stdio) with our own bugs.                                                                                                                                                      &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Doesn't force you to use the platform for secrets.&lt;/strong&gt; If you'd rather hold them in Doppler or 1Password, point the platform at your existing source. &lt;code&gt;getEnv&lt;/code&gt; resolves through whichever the org configured.                                           &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Doesn't pretend to be multi-cloud.&lt;/strong&gt; Single-region GCP today. If you're betting on us, you're betting on a small surface area — not a multi-cloud promise.                                                                                            &lt;/p&gt;

&lt;p&gt;## The why behind the shape                                                                                                                                                                                                                             &lt;/p&gt;

&lt;p&gt;Customer apps can't hold credentials safely. Their AI agent runs on someone's laptop, in CI, on a Cloud Run revision someone's about to redeploy. Putting &lt;code&gt;client_secret&lt;/code&gt; in the app means rotating it everywhere whenever it leaks. So we put the&lt;br&gt;&lt;br&gt;
  credential in one place and gave the app a thin retrieval call instead.                                                                                                                                                                               &lt;/p&gt;

&lt;p&gt;Same logic for MCP. The bearer token for a customer's internal tool server isn't something we want their AI app to know. The app gets a config dictionary right before it calls the MCP. That's as long as the credential lives anywhere near user code.&lt;/p&gt;

&lt;p&gt;The four-primitive surface area is small on purpose. Anything else (token caching, retries, pagination on Gmail, etc.) lives in the SDK or in the customer's code, not in the platform contract. We'd rather grow the SDK than the API.                 &lt;/p&gt;

&lt;p&gt;## Try it&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://leash.build/install.sh | sh                                                                                                                                                                                                        
  leash login                                                                                                                                                                                                                                             
  leash deploy                            
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or just sign up at &lt;a href="https://leash.build" rel="noopener noreferrer"&gt;leash.build&lt;/a&gt;, register a Slack app or an internal MCP, and call the SDK from any project. Custom OAuth + custom MCP are gated to the Growth plan; built-in integrations work on every plan including free.&lt;/p&gt;

&lt;p&gt;Curious what others have done for this. Especially the proxy-vs-config-handoff call for MCP — I made the bet, but it's the architecture choice I'd most welcome a counterargument on.                                                                 &lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>programming</category>
      <category>localhost</category>
    </item>
  </channel>
</rss>
