<?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: Tony</title>
    <description>The latest articles on DEV Community by Tony (@21ideas).</description>
    <link>https://dev.to/21ideas</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%2F3888795%2F310a48ae-9cbb-49be-8d0d-878d4ffbfcef.png</url>
      <title>DEV Community: Tony</title>
      <link>https://dev.to/21ideas</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/21ideas"/>
    <language>en</language>
    <item>
      <title>The Boring Setup That Prevents Disaster</title>
      <dc:creator>Tony</dc:creator>
      <pubDate>Sat, 09 May 2026 07:33:31 +0000</pubDate>
      <link>https://dev.to/21ideas/the-boring-setup-that-prevents-disaster-146h</link>
      <guid>https://dev.to/21ideas/the-boring-setup-that-prevents-disaster-146h</guid>
      <description>&lt;p&gt;I sat down to scaffold a new project this week. It was supposed to be simple: wire up a Cloudflare Workers AI pipeline, set a few environment variables, and start publishing. This time, I wanted to get the security setup right from day one — not scramble to patch it after the fact.&lt;/p&gt;

&lt;p&gt;A couple of weeks ago the Vercel breach forced me to rotate credentials across an existing project. Scrambling through files, checking what was exposed, hoping nothing slipped — that's not how you want to spend an afternoon. It pushed me to write &lt;a href="https://dev.to/21ideas/secret-management-for-vibe-coders-the-system-i-wish-i-had-a-year-ago-531o"&gt;a proper piece on secret management&lt;/a&gt;: a system for keeping secrets safe when AI agents are reading your code. That post was the theory. This one is what it looks like applied to a greenfield project for the first time.&lt;/p&gt;

&lt;p&gt;Before that incident, I'd been winging it. Most of my projects relied on &lt;code&gt;.gitignore&lt;/code&gt; and hope. I knew agents like Claude and Cursor could index the entire repo. I just hadn't thought seriously about what happened if they opened the wrong file.&lt;/p&gt;

&lt;p&gt;This session wasn't about shipping a feature. It was about building a foundation boring enough that most people skip it. Here's what I actually set up.&lt;/p&gt;

&lt;h2&gt;
  
  
  First: Agents Can Read Your Repo (So Limit What They See)
&lt;/h2&gt;

&lt;p&gt;AI coding tools are powerful because they have context. They see your file tree, your imports, your config files. That context becomes a liability when it includes real secrets.&lt;/p&gt;

&lt;p&gt;I was initializing the content-creator repo to call Cloudflare Workers AI, which meant I needed an API key and account ID stored somewhere local. The obvious place was &lt;code&gt;.env.local&lt;/code&gt;. The less obvious question was how to make sure no agent ever read it.&lt;/p&gt;

&lt;p&gt;Git ignore isn't enough. Git keeps files out of commits, but it doesn't stop an AI assistant from opening a file that's sitting right there in your working directory. I needed agent-level blocks, not just version control.&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.amazonaws.com%2Fuploads%2Farticles%2F047yaie551mkvj352jej.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.amazonaws.com%2Fuploads%2Farticles%2F047yaie551mkvj352jej.png" alt="visibility-split" width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Files That Actually Matter
&lt;/h2&gt;

&lt;p&gt;I ended up creating three specific guardrails.&lt;/p&gt;

&lt;p&gt;First, &lt;code&gt;.claude/settings.json&lt;/code&gt; with a denyList. This blocks Claude Code from running &lt;code&gt;Read&lt;/code&gt; or &lt;code&gt;Bash(cat)&lt;/code&gt; on &lt;code&gt;.env.local&lt;/code&gt;. I had to be careful here, because I wanted agents to still read &lt;code&gt;.env.example&lt;/code&gt; for variable names. The denyList specifically excludes &lt;code&gt;.env.example&lt;/code&gt; while blocking &lt;code&gt;.env.local&lt;/code&gt; and &lt;code&gt;.env.*.local&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"deny"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Read(.env.local)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Read(.env.*.local)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(cat .env*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(echo $CONTENT_API*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(printenv CONTENT_API*)"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's one file doing three jobs: blocks direct reads, blocks shell echoing, and blocks printenv for the specific variables that hold live credentials.&lt;/p&gt;

&lt;p&gt;Second, every AI tool has its own approach to ignoring files. Cursor, for instance, doesn't use &lt;code&gt;.gitignore&lt;/code&gt; for its indexer, so it needs a &lt;code&gt;.cursorignore&lt;/code&gt; file. If you're using a different tool, look up how it handles context exclusions. The pattern is the same everywhere: tell the agent what not to read, by name.&lt;/p&gt;

&lt;p&gt;Third, &lt;code&gt;AGENTS.md&lt;/code&gt;. I added an explicit rule telling agents they must never output values from &lt;code&gt;.env*&lt;/code&gt; files, and pointing them to &lt;code&gt;.env.example&lt;/code&gt; as the safe source for variable names.&lt;/p&gt;

&lt;p&gt;One thing none of these files can protect against: pasting your actual secrets inside the IDE where the agent is already running. Create &lt;code&gt;.env.local&lt;/code&gt; in a separate terminal, outside the IDE entirely. Type the values there, save, close. The agent never sees the keystrokes.&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.amazonaws.com%2Fuploads%2Farticles%2Fo7zjudv5grslxhtac4ph.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.amazonaws.com%2Fuploads%2Farticles%2Fo7zjudv5grslxhtac4ph.png" alt="guardrail-stack" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's the access layer handled.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Leak You Won't Find in &lt;code&gt;.gitignore&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The hidden problem was in my own script. &lt;code&gt;cc-publish&lt;/code&gt; calls Cloudflare's API using &lt;code&gt;curl&lt;/code&gt; with a &lt;code&gt;Bearer&lt;/code&gt; token header. The original version passed the key directly in the command:&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;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$CONTENT_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anyone running &lt;code&gt;ps aux&lt;/code&gt; on the machine could see that token in plain text while the request was in flight.&lt;/p&gt;

&lt;p&gt;The fix was writing the header to a temporary file, locking it down with &lt;code&gt;chmod 600&lt;/code&gt;, and passing it to &lt;code&gt;curl&lt;/code&gt; via &lt;code&gt;--config&lt;/code&gt;. The temp file gets deleted whether the call succeeds or fails. The key never touches the process list.&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.amazonaws.com%2Fuploads%2Farticles%2F8ay4eac8jog6bskcd7ej.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.amazonaws.com%2Fuploads%2Farticles%2F8ay4eac8jog6bskcd7ej.png" alt="shell-session-pattern" width="800" height="494"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The broader point is worth pausing on: do a thorough back-and-forth with your agent, walk through every moving part that touches credentials, and make sure nothing slips through. The boring parts are the ones that bite you.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The rule:&lt;/strong&gt; if you wouldn't type a password into a public terminal, don't paste it into a shell argument either.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Gist
&lt;/h2&gt;

&lt;p&gt;Environment variables aren't glamorous. Neither is spending an afternoon on scaffolding while your actual build waits. But this is the work that keeps your API keys out of AI context windows and your deployment credentials out of the process list.&lt;/p&gt;

&lt;p&gt;Three layers in practice: &lt;code&gt;.claude/settings.json&lt;/code&gt; and your tool's equivalent ignore file keep agents from reading credentials. &lt;code&gt;AGENTS.md&lt;/code&gt; sets explicit rules so agents know what to avoid even when they could technically access it. And &lt;code&gt;curl --config&lt;/code&gt; with a temp file keeps tokens out of the process list entirely. None of it is exciting. All of it is necessary.&lt;/p&gt;

&lt;p&gt;I built this while scaffolding the content-creator pipeline, but the approach applies to any project where AI tools have access to your working directory.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Building something with AI tools? I'd love to hear what you're working on. I write about vibe coding, building in public, and the workflows that actually stick. Find me on Threads – &lt;a href="https://www.threads.com/@tony_crusoe" rel="noopener noreferrer"&gt;@tony_crusoe&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Secret Management for Vibe Coders: The System I Wish I Had a Year Ago</title>
      <dc:creator>Tony</dc:creator>
      <pubDate>Mon, 20 Apr 2026 21:31:40 +0000</pubDate>
      <link>https://dev.to/21ideas/secret-management-for-vibe-coders-the-system-i-wish-i-had-a-year-ago-531o</link>
      <guid>https://dev.to/21ideas/secret-management-for-vibe-coders-the-system-i-wish-i-had-a-year-ago-531o</guid>
      <description>&lt;p&gt;Yesterday Vercel &lt;a href="https://x.com/vercel/status/2045865072074035664?s=20" rel="noopener noreferrer"&gt;announced a security incident&lt;/a&gt;. This morning I sat down with my project and realized I didn't have a clear system for what to do next.&lt;/p&gt;

&lt;p&gt;I knew I was supposed to "rotate my keys." I'd done it before — because an AI told me to. But I'd never really understood &lt;em&gt;why&lt;/em&gt;, in what order, or whether I'd been storing secrets properly this whole time.&lt;/p&gt;

&lt;p&gt;I've been building with AI tools for just over a year. No CS degree, no coding books, no deep knowledge of any single language. Everything I know came from prompting Claude, Gemini, Cursor, and building through practice. &lt;a href="https://github.com/bitcoin21ideas" rel="noopener noreferrer"&gt;I've shipped real things&lt;/a&gt;. But secret management was always something I followed by instinct rather than actual understanding.&lt;/p&gt;

&lt;p&gt;Today I fixed that. Here's the full picture — plain English, no fluff — so you can skip straight to the good system.&lt;/p&gt;

&lt;h2&gt;
  
  
  First: Not Everything in Your .env File Is a Secret
&lt;/h2&gt;

&lt;p&gt;Worth getting clear on this because most tutorials treat the whole &lt;code&gt;.env&lt;/code&gt; file as one scary blob. There are actually three distinct types of values:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real secrets&lt;/strong&gt; — if someone gets these, they can impersonate your app, rack up your API bills, or read your users' data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Database passwords&lt;/li&gt;
&lt;li&gt;API keys (your AI provider, payment processor, etc.)&lt;/li&gt;
&lt;li&gt;The key your app uses to sign user sessions&lt;/li&gt;
&lt;li&gt;Anything called a "signing key" or "encryption key"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Config values&lt;/strong&gt; — totally safe to share, they just control how your app behaves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A model name like &lt;code&gt;GEMINI_MODEL=gemini-3.0-flash&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Feature flags, version numbers, non-sensitive settings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Public values&lt;/strong&gt; — stuff you &lt;em&gt;intentionally&lt;/em&gt; expose to the browser. In Next.js these start with &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt;. Don't put anything sensitive here — it ends up in your frontend JavaScript where anyone can read it.&lt;/p&gt;

&lt;p&gt;Only the first category needs serious handling. The rest is just configuration.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Three-Layer Stack
&lt;/h2&gt;

&lt;p&gt;Here's the mental model that makes everything else make sense:&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.amazonaws.com%2Fuploads%2Farticles%2Feqjrfrkkyqz9z8z4z4qz.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.amazonaws.com%2Fuploads%2Farticles%2Feqjrfrkkyqz9z8z4z4qz.png" alt="Secrets stack" width="800" height="725"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One source of truth at the top — everything else is just a copy of it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1 — Your password manager&lt;/strong&gt; is the boss. Every secret lives here with notes on what it is and when you last rotated it. I use Bitwarden (free, open source), but 1Password and Dashlane are equally solid. One folder per project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2 — Your hosting platform's env vars&lt;/strong&gt; (Vercel, Railway, Render, Fly.io — they all have this). This is what your live app actually reads. Your code never touches these directly — it just calls &lt;code&gt;process.env.YOUR_VARIABLE_NAME&lt;/code&gt; and the platform fills it in automatically at runtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 3 — &lt;code&gt;.env.local&lt;/code&gt;&lt;/strong&gt; on your laptop. A local copy for development only. It's gitignored — git completely ignores it, it never gets committed. You sync it from your hosting platform using their CLI.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The rule:&lt;/strong&gt; update the password manager first, then update the copies. That way there's always one place to go if something goes wrong.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The One File That Prevents Most Mistakes: &lt;code&gt;.env.example&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This is the habit that pays off on every project.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;.env.example&lt;/code&gt; is your &lt;code&gt;.env.local&lt;/code&gt; with all real values removed — just the variable names and a comment about what each one is. You commit this to git. You share it with collaborators. You paste it into AI sessions.&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.example&lt;/span&gt;
&lt;span class="c"&gt;# Copy this to .env.local and fill in real values from your password manager.&lt;/span&gt;
&lt;span class="c"&gt;# NEVER commit .env.local. NEVER paste real values into AI sessions.&lt;/span&gt;

&lt;span class="nv"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;          &lt;span class="c"&gt;# Your database connection string&lt;/span&gt;
&lt;span class="nv"&gt;AUTH_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;           &lt;span class="c"&gt;# Run: openssl rand -base64 32&lt;/span&gt;
&lt;span class="nv"&gt;GEMINI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;        &lt;span class="c"&gt;# From Google AI Studio&lt;/span&gt;
&lt;span class="nv"&gt;STRIPE_SECRET_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;     &lt;span class="c"&gt;# From your Stripe dashboard&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Variable names, no values, helpful comments. Safe to share anywhere.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Start every new project by creating this file before you write a single line of code.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Keeping Secrets Out of AI Sessions
&lt;/h2&gt;

&lt;p&gt;Here's something I didn't think about carefully enough until today — and I suspect most AI builders haven't either.&lt;/p&gt;

&lt;p&gt;AI coding tools like Cursor and Claude Code index your codebase as context. If &lt;code&gt;.env.local&lt;/code&gt; is open in a tab, those real values can end up in the AI's context window. That's not how you want your database password or Stripe key traveling around.&lt;/p&gt;

&lt;p&gt;Here's the before/after on what I changed:&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.amazonaws.com%2Fuploads%2Farticles%2Fmyox6zlych9nnu41c6g2.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.amazonaws.com%2Fuploads%2Farticles%2Fmyox6zlych9nnu41c6g2.png" alt="setup checklist" width="800" height="651"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The AI only needs variable names to write correct code — it never needs the actual values.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three rules that make this a non-issue:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Never open &lt;code&gt;.env.local&lt;/code&gt; during an AI session.&lt;/strong&gt; You don't need to. Manage values in your hosting dashboard, sync locally via CLI.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Add &lt;code&gt;.env.local&lt;/code&gt; to your AI's ignore file.&lt;/strong&gt; Create a &lt;code&gt;.cursorignore&lt;/code&gt; (and/or &lt;code&gt;.claudeignore&lt;/code&gt; for Claude Code) in your project root:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.env.local
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The AI indexer will never touch that file even if you accidentally open it.&lt;/p&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start sessions by pasting &lt;code&gt;.env.example&lt;/code&gt;.&lt;/strong&gt; Just drop it in with a note: "here are my variable names, use &lt;code&gt;process.env.VARIABLE_NAME&lt;/code&gt; in any code you write." The AI produces perfectly correct code and never needs to know a single real value.  &lt;/p&gt;&lt;/li&gt;

&lt;/ol&gt;




&lt;h2&gt;
  
  
  What "Rotating Keys" Actually Means
&lt;/h2&gt;

&lt;p&gt;Rotating means: generate a new secret at the provider, update it everywhere, revoke the old one. That's it.&lt;/p&gt;

&lt;p&gt;You do it when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A provider announces a security incident (hi, Vercel)&lt;/li&gt;
&lt;li&gt;Someone who had access leaves your team&lt;/li&gt;
&lt;li&gt;You want a regular security sweep (once a year is fine for small projects)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The same five steps work for every secret:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate a new value at the provider's dashboard&lt;/li&gt;
&lt;li&gt;Update your password manager first&lt;/li&gt;
&lt;li&gt;Update your hosting platform's env vars&lt;/li&gt;
&lt;li&gt;Redeploy&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;vercel env pull .env.local&lt;/code&gt; (or your platform's equivalent) to sync locally&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Rotate in this order&lt;/strong&gt; — highest damage potential first:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Database password (full data access if leaked)&lt;/li&gt;
&lt;li&gt;Session/auth secrets (user account hijacking)&lt;/li&gt;
&lt;li&gt;Encryption keys ← &lt;em&gt;read the warning below before touching these&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;OAuth app secrets (account impersonation)&lt;/li&gt;
&lt;li&gt;Everything else (API keys, billing abuse)&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Encryption key warning&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you're encrypting data in your database, you can't just swap the key — existing encrypted records become unreadable. The right move: version your keys (&lt;code&gt;ENCRYPTION_KEY_V1&lt;/code&gt;, &lt;code&gt;ENCRYPTION_KEY_V2&lt;/code&gt;), keep the old one available for decryption, and write a migration script to re-encrypt all records with the new key before retiring the old one. If you haven't encrypted any production data yet, rotate freely — no migration needed.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  How OAuth Actually Works
&lt;/h2&gt;

&lt;p&gt;This is the part I'd been fuzzy on: "Google handles security for me, right?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not quite&lt;/strong&gt;. Here's the actual picture:&lt;/p&gt;

&lt;p&gt;Your app has its own permanent identity with Google — a Client ID and Client Secret that live in your env vars just like any other API key. They prove to Google that requests are coming from &lt;em&gt;your&lt;/em&gt; app.&lt;/p&gt;

&lt;p&gt;Then, when a user connects their account ("Sign in with Google"), Google issues two tokens that &lt;em&gt;you&lt;/em&gt; store in &lt;em&gt;your&lt;/em&gt; database:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A short-lived access token (expires in hours)&lt;/li&gt;
&lt;li&gt;A long-lived refresh token (valid until the user revokes it)&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Google stores nothing on your behalf. You're responsible for those tokens in your database, which means your database security matters a lot for OAuth too.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Setting Up a New Project the Right Way
&lt;/h2&gt;

&lt;p&gt;Four steps, five minutes, done:&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.amazonaws.com%2Fuploads%2Farticles%2Ffz4t2zhushfdgxsz6ab3.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.amazonaws.com%2Fuploads%2Farticles%2Ffz4t2zhushfdgxsz6ab3.png" alt="Setup" width="800" height="661"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And every time you add a new secret going forward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create the credential at the provider&lt;/li&gt;
&lt;li&gt;Add it to your password manager &lt;strong&gt;first&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Add it to your hosting platform's env vars&lt;/li&gt;
&lt;li&gt;Sync locally with the CLI&lt;/li&gt;
&lt;li&gt;Add the variable name (no value) to &lt;code&gt;.env.example&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two minutes per secret. Scales cleanly as your project grows.&lt;/p&gt;




&lt;h2&gt;
  
  
  Optional Nerd Check: Did Any Secrets Ever Leak Into Git?
&lt;/h2&gt;

&lt;p&gt;If you've accidentally committed an &lt;code&gt;.env&lt;/code&gt; file, you'd probably remember — it tends to be a memorable moment. But if you want to be certain, run these in your project root:&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;# Was any .env file ever committed?&lt;/span&gt;
git log &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--full-history&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s2"&gt;".env*"&lt;/span&gt;

&lt;span class="c"&gt;# Search history for anything that looks like a real secret value&lt;/span&gt;
git log &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"(API_KEY|SECRET|PASSWORD)&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="s2"&gt;*=&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="s2"&gt;*['&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;]?[a-zA-Z0-9]"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing returned — you're clean.&lt;/p&gt;

&lt;p&gt;If something does come back: &lt;strong&gt;rotate the exposed secret first&lt;/strong&gt; (that's what actually protects you), then clean up the history with &lt;code&gt;git filter-repo&lt;/code&gt; and force push. Rotating is the urgent action. The cleanup is secondary hygiene.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Gist
&lt;/h2&gt;

&lt;p&gt;Every secret your app uses is a credential that proves your app's identity to another service. Treat it like a password: store it in a password manager, keep it out of your codebase, rotate it after incidents, and don't hand it to an AI assistant.&lt;/p&gt;

&lt;p&gt;The system is four things: &lt;strong&gt;password manager&lt;/strong&gt; (source of truth) → &lt;strong&gt;hosting platform&lt;/strong&gt; (production copy) → &lt;strong&gt;&lt;code&gt;.env.local&lt;/code&gt;&lt;/strong&gt; (local copy) → &lt;strong&gt;&lt;code&gt;.env.example&lt;/code&gt;&lt;/strong&gt; (the safe version you share everywhere). Get these four right and you're doing secret management properly — the same way any experienced developer does it.&lt;/p&gt;

&lt;p&gt;No degree required. Just a bit of intentional setup at the start of each project. Now go create that &lt;code&gt;.env.example&lt;/code&gt;. &lt;/p&gt;




&lt;p&gt;&lt;em&gt;Building something with AI tools? I'd love to hear what you're working on — drop it in the comments. I write about vibe coding, building in public, and the AI-first dev workflow.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
