NextJSSecurity5 MIN READ
09.05.2026
.md

Securing My Blog's Secrets with Varlock and Doppler

Last updated: 09.05.2026

varlock

The Problem With .env Files

Every Next.js project starts the same way: you create a .env file, paste in your API keys, and add it to .gitignore. Job done, right?

Not quite. That file sits on your machine in plain text. It gets copied into CI pipelines, shared over Slack, duplicated across .env.local and .env.production. One accidental commit or a misconfigured deploy and your tokens are exposed. Even with gitignore, the secrets live on every developer's laptop, in every backup, in every disk image. I wanted a setup where secrets never touch the filesystem at all.

What Is Varlock?

Varlock is a modern replacement for dotenv, built on the @env-spec specification. Instead of scattering environment variables across multiple .env files with no structure, you define a single .env.schema file that describes every variable your app needs: its type, whether it's required, and whether it's sensitive.

The schema is committed to version control. It contains no secret values, only the structure and resolver functions. From this schema, Varlock gives you automatic TypeScript type generation, validation at startup with clear error messages, sensitivity markers that control client-side bundling, and log redaction so sensitive values never leak into terminal output.

For Next.js, Varlock provides a drop-in integration that replaces the built-in @next/env loader. You configure a package manager override, wrap your Next config with their plugin, and everything just works. Your existing process.env calls keep functioning while you gain validation and type safety on top.

Why Doppler as the Secrets Provider

Varlock supports multiple secrets backends through plugins and CLI integrations. I went with Doppler for a few reasons.

The free tier is generous: 50 service tokens, 10 projects, and 3 users. The dashboard is clean. You create a project, it gives you dev, staging, and production configs out of the box. Add your secrets, and they are available instantly through the CLI or API.

The killer feature for me was the native Vercel integration. Doppler syncs secrets directly to Vercel as environment variables. No tokens needed at deploy time, no API calls during the build. When you rotate a secret in Doppler, Vercel picks it up on the next deploy automatically.

The Integration: Zero .env Files

Here is what makes this setup different from most tutorials: there is no .env file. Not .env.local, not .env.production, nothing. The .env file in my project is empty. All secrets are resolved at runtime through two paths depending on the environment.

For local development, the .env.schema uses Varlock's exec() function to call the Doppler CLI for each secret. A line like SANITY_API_READ_TOKEN=exec(`doppler secrets get SANITY_API_READ_TOKEN --plain -p ask-blog -c dev`) tells Varlock to run that shell command and use the output as the value. The Doppler CLI authenticates through your browser login session, so no tokens or credentials are stored anywhere on disk. You run doppler login once, and every exec() call uses that session.

For production on Vercel, Doppler's native sync pushes all values directly as environment variables. When Varlock sees that a variable is already set in the environment, it skips the exec() call entirely and uses the existing value. The Doppler CLI is not even installed on Vercel, and it does not need to be. The schema validates the values regardless of where they came from.

This dual-path approach means the same .env.schema works everywhere without modification. Locally, exec() fetches from Doppler. On Vercel, environment variables are already present. Varlock validates both.

Security Benefits I Actually Got

The biggest win: truly zero secrets on disk. There is no .env file to accidentally commit, no token to leak in a screenshot, no credential file sitting in a backup. The Doppler CLI authenticates through your OS keychain via a browser-based login. Secrets exist only in memory when your app runs.

Schema validation catches configuration errors before the app starts. If a required variable is missing or a URL field contains something invalid, Varlock tells you exactly what is wrong. No more debugging a cryptic runtime crash because someone forgot to set an environment variable.

The sensitivity model is another practical win. Variables prefixed with NEXT_PUBLIC_ are automatically marked non-sensitive. Everything else is sensitive by default. Varlock redacts sensitive values in logs, so even if you accidentally log a token, it shows up as redacted characters.

Finally, the .env.schema file is AI-friendly by design. When tools like Claude Code work on my project, they can read the schema to understand what configuration exists and what types each variable expects, all without ever seeing the actual secret values. This is a real improvement over the old setup where an AI assistant might read a .env file and expose tokens in a conversation.

What I'd Do Differently

I initially tried using Varlock's Doppler plugin, which uses service tokens for authentication. It worked locally but broke on Vercel because the plugin requires an initialized connection. If the token is missing, every resolver call fails hard. I spent time trying to make it conditional before realising the exec() approach with the Doppler CLI is simpler and more robust.

If I were starting over, I would skip the plugin entirely and go straight to the CLI approach. Set up the Doppler Vercel integration first so production deploys work immediately, then configure the exec() calls for local development. The CLI approach has one tradeoff: each variable makes a separate shell call at startup, which is slightly slower than a bulk API call. In practice, it adds about a second to dev server startup, which is a price I am happy to pay for never storing secrets on disk.

Continue Reading
ai agent
AINextJSSanity2 MIN

Serving Your Blog as Markdown So AI Agents Can Actually Read It

The post explains how to serve clean Markdown versions of blog content so AI agents can consume it without HTML noise like navigation, scripts, and cookie banners. Instead of parsing full HTML pages, agents can request Markdown either by appending .md to post URLs or by sending an Accept: text/markdown header. In Next.js, two rewrite rules in next.config.ts route both patterns to an internal /md/posts/[slug] handler. That route fetches posts from Sanity with sanityFetch, converts them to Markdown, and returns a text/markdown response with short caching headers for freshness. The buildPostMarkdown function constructs a complete Markdown document: H1 title, canonical URL, optional hero image, auto-generated summary, and the main body converted from Sanity Portable Text via @portabletext/markdown. Code blocks stored in Sanity as custom _type: "code" objects are rendered as fenced code blocks with language tags preserved. A /posts.md index provides a machine-readable sitemap listing all posts with metadata and links, enabling agents to discover content before fetching individual Markdown posts. The .md suffix is ideal for sharing, while the Accept header method suits tools and AI clients that control HTTP headers.

Read post
retro version of image
AINextJS2 MIN

Building a Pixel Art Converter: From DOOM to Modern Portraits

The project began as a simple DOOM-inspired image converter, evolving into a comprehensive pixel art transformation tool. Initially inspired by DOOM's iconic color palette, the tool recreates the retro look using a 32-color palette with Euclidean distance color matching. As it developed, the tool expanded beyond DOOM aesthetics to include a Portrait palette optimized for human faces, making it suitable for profile pictures and avatars. Key features of the tool include seven color palettes: DOOM, Portrait, Skin Tones, Game Boy, PICO-8, Commodore 64, and Grayscale. It also offers Floyd-Steinberg dithering for smooth gradients, adjustable pixel scaling for a retro look, and PNG export with timestamped filenames. Built with Next.js and the Canvas API, the tool processes images through background removal, pixelation, and color reduction or palette mapping. AI-assisted development played a significant role in the project's rapid iteration and refinement, showcasing how modern tools can accelerate prototyping. The result is a practical tool that blends retro gaming aesthetics with modern web development, available for use at /image-converter.

Read post