# Securing Environment Variables in Next.js and Flutter

Environment variables are the easiest thing to get wrong in a production app. They're convenient — drop a value in `.env`, read it back with one line of code — and that convenience is exactly why API keys, database URLs, and Firebase configs end up leaking into client bundles, APKs, and public GitHub repos. Here's how I handle env security across the two stacks I ship on most: Next.js on the web and Flutter for mobile/desktop apps.

## Next.js: `process.env` is safe by default, until you export it

Next.js gives you `process.env` out of the box, and the framework's own build step is your first line of defense — it statically replaces `process.env.*` references at build time and only inlines variables prefixed with `NEXT_PUBLIC_` into the client bundle. Anything without that prefix stays server-side: API routes, server components, and middleware can read it, but it never ships to the browser.

The practical rules I follow:

*   Anything that touches a secret key, a database connection string, or a third-party API secret has **no** `NEXT_PUBLIC_` prefix. If it doesn't need to run in the browser, don't give it browser access.
    
*   `.env.local` (and `.env*.local` in general) is git-ignored by default in a Next.js project — keep it that way, and never commit `.env.production` with real values either. Only commit an `.env.example` with placeholder keys so collaborators know what to set.
    
*   On Vercel, environment variables are configured per environment (Development, Preview, Production) in the dashboard rather than in the repo. CMS tokens and payment provider keys get scoped to Production only, so a preview deploy from a feature branch never has access to live credentials.
    
*   Vercel's serverless/edge sandbox isolates each deployment's execution environment, so a variable set for one project or environment doesn't bleed into another — but that isolation only protects you if you've actually kept the secret server-side in the first place. Sandboxing doesn't fix a `NEXT_PUBLIC_` mistake.
    

If you need a secret at build time (say, to fetch content from a headless CMS during static generation) but never want it in the output HTML/JS, keep it unprefixed and only reference it inside `getStaticProps`/server components/route handlers — never inside a client component.

## Flutter: there's no such thing as a client-side secret

This is the mindset shift that matters most. A Next.js app has a real server boundary — code that literally never leaves your infrastructure. A Flutter app ships as a compiled binary that runs entirely on someone else's device. Anything bundled into that binary — a `.env` file included as an asset, a string baked in with `--dart-define`, a Firebase config — can be extracted by anyone with the APK/IPA and a bit of patience (`strings`, an APK unzip, a decompiler). Flutter env security is really about raising the cost of extraction and locking down the *device-level* attack surface, not achieving true secrecy for anything embedded in the app.

With that caveat, here's the layered approach:

**Loading config values.** I use [`flutter_dotenv`](https://pub.dev/packages/flutter_dotenv) to load a `.env` file at runtime instead of hardcoding values:

```dart
import 'package:flutter_dotenv/flutter_dotenv.dart';

appId: _env('FIREBASE_LINUX_APP_ID'),
```

This keeps per-platform Firebase config (Linux, Android, iOS, macOS, web each get their own app ID) out of source control and out of a single hardcoded `firebase_options.dart`. It's the right pattern for keeping config *manageable and out of git* — just remember the `.env` still ships as an asset inside the app package, so treat any value in it as recoverable, not secret. For anything genuinely sensitive, `--dart-define-from-file` at compile time is a slightly stronger option since the values aren't sitting in a loose asset file, though they're still embedded in the compiled binary and extractable with effort.

**Android hardening.** In `AndroidManifest.xml`, I set:

```xml
android:allowBackup="false"
android:fullBackupContent="false"
```

`allowBackup="true"` is the default, and it lets `adb backup` pull app data — including SharedPreferences, local databases, and cached files — off a device without root. If your app caches tokens or config locally, disabling backup closes off that extraction path.

**iOS/macOS hardening.** In the entitlements file (and I make sure the same requirement applies to the debug entitlements, not just `Release.entitlements`, since a debug build under App Sandbox needs it too):

```xml
<!-- See Release.entitlements — same requirement applies to debug builds under App Sandbox. -->
<key>com.apple.security.network.client</key>
<true/>
```

App Sandbox on macOS blocks outbound network access unless you explicitly declare the `network.client` entitlement — this isn't strictly a "hide the secret" measure, it's what keeps a sandboxed macOS build from silently failing every network call (including the one that fetches your remote config or talks to Firebase). Worth checking both entitlement files so a debug build doesn't behave differently from release.

## The general checklist

Across both stacks, the practices that actually move the needle:

1.  `.env*` files are git-ignored everywhere, with a checked-in `.env.example` for onboarding.
    
2.  Anything that must stay secret runs server-side (Next.js API routes/server components) — never in a Flutter client, never behind `NEXT_PUBLIC_`.
    
3.  Environment values are scoped per deployment environment (Vercel's dashboard, CI secrets) rather than duplicated across branches or hardcoded.
    
4.  For Flutter, assume every embedded value is recoverable and design around it: real secrets (payment keys, private API credentials) go through a backend you control, and the app only ever talks to your own server, which then talks to the third party.
    
5.  Platform-level hardening — `allowBackup="false"` on Android, correct entitlements on iOS/macOS — closes off the easy, tooling-based extraction paths even if it can't stop a determined reverse engineer.
    
6.  Rotate keys when a collaborator leaves a project or a repo's visibility changes, and audit `.env.example` against production periodically so it doesn't quietly drift into containing real values.
    

The short version: on the web, the framework gives you a real client/server boundary — use it. On mobile and desktop, there is no such boundary, so the job shifts from "hide the secret" to "keep secrets off the device entirely, and make everything that *is* on the device as hard to lift as reasonably possible."
