<?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: vavilov2212</title>
    <description>The latest articles on DEV Community by vavilov2212 (@vavilov2212).</description>
    <link>https://dev.to/vavilov2212</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%2F927865%2F853c5ee9-29cb-4120-8e38-817c86623f59.jpg</url>
      <title>DEV Community: vavilov2212</title>
      <link>https://dev.to/vavilov2212</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vavilov2212"/>
    <language>en</language>
    <item>
      <title>Deploying a headless commerce app: a Next.js storefront on Vercel and a Keystone GraphQL admin on Google Cloud Run</title>
      <dc:creator>vavilov2212</dc:creator>
      <pubDate>Tue, 21 Jul 2026 00:21:33 +0000</pubDate>
      <link>https://dev.to/vavilov2212/deploying-a-headless-commerce-app-a-nextjs-storefront-on-vercel-and-a-keystone-graphql-admin-on-4dc4</link>
      <guid>https://dev.to/vavilov2212/deploying-a-headless-commerce-app-a-nextjs-storefront-on-vercel-and-a-keystone-graphql-admin-on-4dc4</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;A digital goods store deployed as three decoupled services around one database: a &lt;a href="https://nextjs.org" rel="noopener noreferrer"&gt;Next.js&lt;/a&gt; storefront, a &lt;a href="https://keystonejs.com" rel="noopener noreferrer"&gt;Keystone 6&lt;/a&gt; admin with a &lt;a href="https://graphql.org" rel="noopener noreferrer"&gt;GraphQL&lt;/a&gt; API served by &lt;a href="https://www.apollographql.com/docs/apollo-server" rel="noopener noreferrer"&gt;Apollo Server&lt;/a&gt;, and &lt;a href="https://www.postgresql.org" rel="noopener noreferrer"&gt;Postgres&lt;/a&gt;. They run on &lt;a href="https://vercel.com" rel="noopener noreferrer"&gt;Vercel&lt;/a&gt;, &lt;a href="https://cloud.google.com/run" rel="noopener noreferrer"&gt;Google Cloud Run&lt;/a&gt;, and &lt;a href="https://neon.tech" rel="noopener noreferrer"&gt;Neon&lt;/a&gt;, and because each service is on a managed free tier, the demo costs nothing to run.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/vavilov2212/digital-products-shop" rel="noopener noreferrer"&gt;https://github.com/vavilov2212/digital-products-shop&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live shop:&lt;/strong&gt; &lt;a href="https://digital-products-shop.vercel.app" rel="noopener noreferrer"&gt;https://digital-products-shop.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Roadmap:&lt;/strong&gt; connect a real product supplier API, then build the payment flow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rest of this article is the architecture and the full deployment, one host at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The architecture, split into three services&lt;/li&gt;
&lt;li&gt;What Keystone actually is&lt;/li&gt;
&lt;li&gt;Service 1: the storefront (Next.js on Vercel)&lt;/li&gt;
&lt;li&gt;Service 2: the admin and GraphQL API (Keystone 6 on Google Cloud Run)&lt;/li&gt;
&lt;li&gt;Service 3: the database (Postgres on Neon)&lt;/li&gt;
&lt;li&gt;Local development with Docker Compose&lt;/li&gt;
&lt;li&gt;The production container (multi-stage Docker build)&lt;/li&gt;
&lt;li&gt;CI/CD with GitHub Actions&lt;/li&gt;
&lt;li&gt;How a request is served, and what a deploy does&lt;/li&gt;
&lt;li&gt;Standing it up from scratch&lt;/li&gt;
&lt;li&gt;Environment variables&lt;/li&gt;
&lt;li&gt;Locking down the public GraphQL endpoint&lt;/li&gt;
&lt;li&gt;Takeaways&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;I built a small store for digital goods and put it into production on three managed free tiers. The interesting part is not the shop itself. It is how the pieces are split into independent services and wired together so each one can scale, sleep, and deploy on its own.&lt;/p&gt;

&lt;p&gt;The stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://nextjs.org" rel="noopener noreferrer"&gt;Next.js 13&lt;/a&gt; (App Router)&lt;/strong&gt; for the storefront&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://keystonejs.com" rel="noopener noreferrer"&gt;Keystone 6&lt;/a&gt;&lt;/strong&gt; for the admin panel and a public &lt;strong&gt;&lt;a href="https://graphql.org" rel="noopener noreferrer"&gt;GraphQL&lt;/a&gt;&lt;/strong&gt; API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.postgresql.org" rel="noopener noreferrer"&gt;Postgres&lt;/a&gt;&lt;/strong&gt; for storage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.docker.com" rel="noopener noreferrer"&gt;Docker&lt;/a&gt;&lt;/strong&gt; for the containers and
&lt;strong&gt;&lt;a href="https://docs.docker.com/compose/" rel="noopener noreferrer"&gt;Docker Compose&lt;/a&gt;&lt;/strong&gt; for local development&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://docs.github.com/actions" rel="noopener noreferrer"&gt;GitHub Actions&lt;/a&gt;&lt;/strong&gt; for CI/CD&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hosted on &lt;strong&gt;&lt;a href="https://vercel.com" rel="noopener noreferrer"&gt;Vercel&lt;/a&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;a href="https://cloud.google.com/run" rel="noopener noreferrer"&gt;Google Cloud Run&lt;/a&gt;&lt;/strong&gt;, and &lt;strong&gt;&lt;a href="https://neon.tech" rel="noopener noreferrer"&gt;Neon&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture, split into three services
&lt;/h2&gt;

&lt;p&gt;The system is three services with one shared database. Each service has one job.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                           a user
                             |
                             v
        +--------------------------------------------+
        |  Vercel  -  storefront (Next.js, ISR)      |
        |  static pages, revalidate at most once/day |
        +--------------------------------------------+
                             |
              queries Postgres directly, through an
              embedded Keystone/Prisma context
              (never calls the admin service)
                             |
                             v
        +--------------------------------------------+
        |  Neon  -  Postgres (serverless)            |
        |  single source of truth                    |
        +--------------------------------------------+
                             ^
             admin writes + Prisma migrations
                             |
        +--------------------------------------------+
        |  Google Cloud Run  -  Keystone admin       |
        |  + public GraphQL API, scale to zero       |
        +--------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;The load-bearing decision.&lt;/strong&gt; The storefront reads Postgres directly&lt;br&gt;
through Keystone's embedded context and never calls the admin service. That one&lt;br&gt;
choice is what makes the free-tier setup work: the admin service is allowed to&lt;br&gt;
scale to zero and sleep, and its cold starts never touch a user request.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Keystone actually is
&lt;/h2&gt;

&lt;p&gt;Before the service breakdown, it helps to know what Keystone is under the hood, because it is doing more than one job. Keystone is a bundle of tools you would otherwise wire up yourself, all generated from a single schema:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.prisma.io" rel="noopener noreferrer"&gt;Prisma&lt;/a&gt;&lt;/strong&gt; as the ORM and migration engine&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.apollographql.com/docs/apollo-server" rel="noopener noreferrer"&gt;Apollo Server&lt;/a&gt;&lt;/strong&gt; for the GraphQL API (configured right in &lt;code&gt;keystone.ts&lt;/code&gt; via &lt;code&gt;apolloConfig&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;A generated &lt;strong&gt;Admin UI&lt;/strong&gt; built on React and Next.js&lt;/li&gt;
&lt;li&gt;Session and access-control primitives&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You could assemble these separately, a Prisma schema plus a hand-written Apollo Server plus your own admin pages, and some teams do. Keystone's pitch is that it generates all of it from one list schema so you do not have to.&lt;/p&gt;

&lt;p&gt;Keystone can also run in two shapes, and this project uses both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Standalone:&lt;/strong&gt; Keystone runs its own server for the Admin UI and the GraphQL endpoint. That is what runs on Cloud Run here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embedded:&lt;/strong&gt; Keystone's data layer is imported into another Node or Next.js process through &lt;a href="https://keystonejs.com/docs/context/overview" rel="noopener noreferrer"&gt;&lt;code&gt;getContext&lt;/code&gt;&lt;/a&gt;, so you can run Keystone queries in-process without going through the GraphQL server over the network.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The storefront uses the embedded shape to read Postgres directly, while the Admin UI and the Apollo GraphQL server run standalone on Cloud Run. That split is the whole trick, and the next three sections walk through each piece.&lt;/p&gt;

&lt;h2&gt;
  
  
  Service 1: the storefront (Next.js on Vercel)
&lt;/h2&gt;

&lt;p&gt;The storefront is a &lt;a href="https://nextjs.org/docs/app" rel="noopener noreferrer"&gt;Next.js 13 App Router&lt;/a&gt; app on &lt;a href="https://vercel.com" rel="noopener noreferrer"&gt;Vercel&lt;/a&gt;. The product, category, and cart pages are rendered with &lt;a href="https://nextjs.org/docs/app/guides/incremental-static-regeneration" rel="noopener noreferrer"&gt;ISR&lt;/a&gt;, which stands for Incremental Static Regeneration. Each page is built once into static HTML and then allowed to rebuild itself on a schedule, so it stays fast to serve and does not go stale forever. The schedule here is &lt;code&gt;revalidate: 86400&lt;/code&gt;, so 24 hours at most.&lt;/p&gt;

&lt;p&gt;The important detail is how the storefront gets its data. It does not call the Keystone GraphQL server over the network. It imports Keystone's embedded context and runs queries against Postgres in-process. The whole product fetch is a direct database query with no external call:&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;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getProducts&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;keystoneContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sudo&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;asc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`id productId label description price imageUrl currency { id code name }`&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;Because this runs at build time and during background revalidation, the data in the live pages is a snapshot from the moment of the last build. That fact matters during a deploy.&lt;/p&gt;

&lt;p&gt;Why Vercel: it runs Next.js with almost no configuration, the CDN and ISR caching are built in, and it redeploys on every push to &lt;code&gt;master&lt;/code&gt; through its GitHub integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Service 2: the admin and GraphQL API (Keystone 6 on Google Cloud Run)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://keystonejs.com" rel="noopener noreferrer"&gt;Keystone 6&lt;/a&gt; gives me two things from one schema: an admin UI for editing products, and a GraphQL API. GraphQL is a query language where the client asks for exactly the fields it wants in one request. In this project the &lt;a href="https://keystonejs.com/docs/graphql/overview" rel="noopener noreferrer"&gt;GraphQL playground&lt;/a&gt; and schema introspection are intentionally left public, so the API is browsable as part of the demo.&lt;/p&gt;

&lt;p&gt;Keystone runs as a &lt;a href="https://www.docker.com" rel="noopener noreferrer"&gt;Docker&lt;/a&gt; container on&lt;br&gt;
&lt;a href="https://cloud.google.com/run" rel="noopener noreferrer"&gt;Google Cloud Run&lt;/a&gt;. Cloud Run is Google's&lt;br&gt;
serverless container platform: you hand it an image and it runs it, scaling the number of instances up and down with traffic, including down to zero. Scale to zero means that when nobody is using the admin, Cloud Run stops the container and charges nothing. The first request after an idle period pays a cold start, a couple of seconds here while the container boots.&lt;/p&gt;

&lt;p&gt;That cold start would be unacceptable if users depended on this service. They do not, because the storefront reads Postgres directly. Only I use the admin, so a two-second wake-up on login is fine.&lt;/p&gt;

&lt;p&gt;Two things run on every container boot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.prisma.io/docs/orm/prisma-migrate" rel="noopener noreferrer"&gt;Prisma migrations&lt;/a&gt;
(&lt;code&gt;prisma migrate deploy&lt;/code&gt;), which apply any pending versioned schema changes so the database shape matches the code.&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;onConnect&lt;/code&gt; hook that optionally seeds demo data, gated behind a
&lt;code&gt;SEED_ON_STARTUP&lt;/code&gt; environment variable.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;onConnect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &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;SEED_ON_STARTUP&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;seedDemoData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&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 service is named &lt;code&gt;keystone-admin&lt;/code&gt; in the &lt;code&gt;us-central1&lt;/code&gt; region, pinned to a maximum of one instance so migrations never run concurrently.&lt;/p&gt;

&lt;p&gt;Why Cloud Run: it runs any container, it only bills while the container is awake, scale to zero costs nothing at rest, and it deploys from a GitHub Action on every push to &lt;code&gt;master&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Service 3: the database (Postgres on Neon)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://neon.tech" rel="noopener noreferrer"&gt;Neon&lt;/a&gt; is serverless &lt;a href="https://www.postgresql.org" rel="noopener noreferrer"&gt;Postgres&lt;/a&gt;.&lt;br&gt;
It is the single source of truth for products, categories, currencies, carts, and orders. Both other services talk to it.&lt;/p&gt;

&lt;p&gt;Neon also suspends the compute after about five minutes of no queries and resumes in roughly half a second to two seconds on the next query. For a demo this is invisible almost all the time.&lt;/p&gt;

&lt;p&gt;Neon hands you two connection strings, and matching the right one to the right service is load-bearing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Pooled vs direct, and where each string goes.&lt;/strong&gt;&lt;br&gt;
The &lt;strong&gt;pooled&lt;/strong&gt; string (its host contains &lt;code&gt;-pooler&lt;/code&gt;) routes through&lt;br&gt;
&lt;a href="https://www.pgbouncer.org" rel="noopener noreferrer"&gt;PgBouncer&lt;/a&gt;, which lets many short-lived requests&lt;br&gt;
share a small set of real database connections. It goes to the &lt;strong&gt;storefront on&lt;br&gt;
Vercel&lt;/strong&gt;, with &lt;code&gt;?sslmode=require&amp;amp;pgbouncer=true&amp;amp;connect_timeout=15&lt;/code&gt; appended,&lt;br&gt;
because the storefront opens many short connections.&lt;br&gt;
The &lt;strong&gt;direct&lt;/strong&gt; string goes to &lt;strong&gt;Cloud Run&lt;/strong&gt;, because&lt;br&gt;
&lt;a href="https://neon.tech/docs/connect/connection-pooling" rel="noopener noreferrer"&gt;Prisma migrations do not work through the pooler&lt;/a&gt;&lt;br&gt;
and Keystone runs migrations on boot. Swap the two and migrations fail on boot&lt;br&gt;
in confusing ways.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Why Neon: real Postgres, a genuine free tier (0.5 GB storage, 100 compute-hours a month), and fast enough resume that the suspend does not hurt.&lt;/p&gt;
&lt;h2&gt;
  
  
  Local development with Docker Compose
&lt;/h2&gt;

&lt;p&gt;Production is three separate hosts, but locally the whole thing comes up with one command through &lt;a href="https://docs.docker.com/compose/" rel="noopener noreferrer"&gt;Docker Compose&lt;/a&gt;. Docker Compose runs a set of containers defined in one YAML file. &lt;code&gt;docker-compose.dev.yml&lt;/code&gt; defines three services that mirror production:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;nextjs-front&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;      &lt;span class="c1"&gt;# Next.js storefront on :3000, ./src and ./public mounted for hot reload&lt;/span&gt;
  &lt;span class="na"&gt;keystone-server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="c1"&gt;# Keystone admin + GraphQL on :4000, SEED_ON_STARTUP=true&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;                &lt;span class="c1"&gt;# postgres:latest on :5432, data in ./.dbdata&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bring it all up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.dev.yml up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;span class="c"&gt;# storefront: http://localhost:3000&lt;/span&gt;
&lt;span class="c"&gt;# admin:      http://localhost:4000&lt;/span&gt;
&lt;span class="c"&gt;# postgres:   localhost:5432&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details make the dev loop pleasant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The storefront container mounts &lt;code&gt;./src&lt;/code&gt; and &lt;code&gt;./public&lt;/code&gt; as volumes, so front-end edits hot-reload without a rebuild.&lt;/li&gt;
&lt;li&gt;The Keystone service sets &lt;code&gt;SEED_ON_STARTUP=true&lt;/code&gt;, so a fresh database is
populated with the demo catalog on first boot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The local Postgres uses &lt;code&gt;POSTGRES_HOST_AUTH_METHOD: trust&lt;/code&gt; because it is never exposed outside the compose network.&lt;/p&gt;

&lt;h2&gt;
  
  
  The production container (multi-stage Docker build)
&lt;/h2&gt;

&lt;p&gt;The Keystone image is a &lt;a href="https://docs.docker.com/build/building/multi-stage/" rel="noopener noreferrer"&gt;multi-stage Docker build&lt;/a&gt;. Multi-stage means the build runs in phases and only the final phase ships, so build-time tooling does not bloat the runtime image. The stages here are &lt;code&gt;base&lt;/code&gt;, &lt;code&gt;deps&lt;/code&gt;, &lt;code&gt;builder&lt;/code&gt;, and &lt;code&gt;runner&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base       # Debian slim, openssl installed&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;deps               # npm ci --legacy-peer-deps&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder            # npm run keystone:build&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;runner             # CMD ["npm", "run", "keystone:start"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line in that Dockerfile is a scar from a real bug.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Use the Debian (&lt;code&gt;slim&lt;/code&gt;) base, not Alpine.&lt;/strong&gt;&lt;br&gt;
Prisma's Alpine (musl) query engine links against OpenSSL 1.1 and segfaults&lt;br&gt;
inside Node 20, which ships OpenSSL 3, during TLS handshakes. A TLS handshake&lt;br&gt;
happens on every &lt;code&gt;sslmode=require&lt;/code&gt; connection, which is exactly what Neon&lt;br&gt;
requires. So an Alpine image builds fine and then crashes the moment it tries&lt;br&gt;
to reach the database. &lt;code&gt;node:20-slim&lt;/code&gt; is Debian and ships OpenSSL 3 engines,&lt;br&gt;
which fixes it. This cost real debugging time and is not something you would&lt;br&gt;
guess from the error.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One more Cloud Run detail lives in this image: &lt;code&gt;PORT&lt;/code&gt; is deliberately not set.&lt;br&gt;
Keystone reads &lt;code&gt;process.env.PORT&lt;/code&gt; at runtime, and Cloud Run injects &lt;code&gt;PORT=8080&lt;/code&gt;.&lt;br&gt;
Letting the platform pick the port is part of the&lt;br&gt;
&lt;a href="https://cloud.google.com/run/docs/container-contract" rel="noopener noreferrer"&gt;Cloud Run container contract&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  CI/CD with GitHub Actions
&lt;/h2&gt;

&lt;p&gt;Two &lt;a href="https://docs.github.com/actions" rel="noopener noreferrer"&gt;GitHub Actions&lt;/a&gt; workflows drive continuous integration and deployment.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ci.yml&lt;/code&gt; runs on pull requests and on pushes to &lt;code&gt;dev&lt;/code&gt;. It generates the Keystone and Prisma types without a database (&lt;code&gt;keystone postinstall&lt;/code&gt;), type-checks with &lt;code&gt;tsc&lt;/code&gt;, and runs the linter. The lint step is allowed to fail without blocking, because the repo carries some known stylistic debt. This workflow does not deploy.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;deploy-keystone.yml&lt;/code&gt; runs on pushes to &lt;code&gt;master&lt;/code&gt;. It builds the Keystone&lt;br&gt;
container, pushes it to &lt;a href="https://cloud.google.com/artifact-registry" rel="noopener noreferrer"&gt;Artifact Registry&lt;/a&gt; (Google's container image store), and deploys it to Cloud Run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;master&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;google-github-actions/auth@v2&lt;/span&gt;
    &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;credentials_json&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GCP_SA_KEY }}&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker build -f docker/production/Dockerfile.keystone -t "$IMAGE:${{ github.sha }}" .&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker push "$IMAGE:${{ github.sha }}"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gcloud run deploy keystone-admin --image "$IMAGE:${{ github.sha }}" --region us-central1 ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Runtime environment variables are set once on the Cloud Run service and preserved across deploys, so the workflow never has to know any secrets beyond the two it needs to authenticate: &lt;code&gt;GCP_PROJECT_ID&lt;/code&gt; and &lt;code&gt;GCP_SA_KEY&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a request is served, and what a deploy does
&lt;/h2&gt;

&lt;p&gt;Two flows tie the services together.&lt;/p&gt;

&lt;p&gt;A user request:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The user asks Vercel for a page.&lt;/li&gt;
&lt;li&gt;Vercel serves the pre-built static HTML instantly.&lt;/li&gt;
&lt;li&gt;If the page is past its 24-hour window, Vercel rebuilds it in the background on the next request, querying Neon directly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Cloud Run never appears in that path. That is the whole point.&lt;/p&gt;

&lt;p&gt;A deploy, triggered by one push to &lt;code&gt;master&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   git push origin dev:master
             |
      +------+------------------------+
      |                               |
      v                               v
   Vercel rebuilds                GitHub Action
   the storefront                 deploy-keystone.yml
   (queries Neon, bakes           builds + deploys the
   current data into pages)       Keystone container to Cloud Run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Production is the &lt;code&gt;master&lt;/code&gt; branch. Work happens on &lt;code&gt;dev&lt;/code&gt;, which is not&lt;br&gt;
production. The two deploys run independently and do not finish at the same time, so a fresh storefront build can briefly ship with data that is about to change underneath it.&lt;/p&gt;

&lt;p&gt;Getting work onto &lt;code&gt;master&lt;/code&gt; is a fast-forward push. A fast-forward is the clean git case where &lt;code&gt;master&lt;/code&gt; is simply behind &lt;code&gt;dev&lt;/code&gt; with no divergent history, so it slides forward with nothing to merge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git fetch origin
git merge-base &lt;span class="nt"&gt;--is-ancestor&lt;/span&gt; origin/master dev &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo yes&lt;/span&gt;   &lt;span class="c"&gt;# prints yes if safe&lt;/span&gt;
git push origin dev            &lt;span class="c"&gt;# update remote dev&lt;/span&gt;
git push origin dev:master     &lt;span class="c"&gt;# fast-forward master to dev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can watch the Cloud Run deploy from the terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh run list &lt;span class="nt"&gt;--branch&lt;/span&gt; master &lt;span class="nt"&gt;--workflow&lt;/span&gt; &lt;span class="s2"&gt;"Deploy Keystone to Cloud Run"&lt;/span&gt; &lt;span class="nt"&gt;--limit&lt;/span&gt; 3
gh run watch &amp;lt;run-id&amp;gt; &lt;span class="nt"&gt;--exit-status&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Standing it up from scratch
&lt;/h2&gt;

&lt;p&gt;Everything you need to reproduce the deployment, one host at a time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Neon
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Create a free project at &lt;a href="https://neon.tech" rel="noopener noreferrer"&gt;neon.tech&lt;/a&gt;, in an AWS region near Cloud Run and Vercel (us-east or us-central).&lt;/li&gt;
&lt;li&gt;Copy both connection strings from the dashboard. The direct one
(&lt;code&gt;...@ep-xxx.region.aws.neon.tech/neondb?sslmode=require&lt;/code&gt;) goes to Cloud Run. The pooled one (its host contains &lt;code&gt;-pooler&lt;/code&gt;) goes to Vercel, with Prisma params appended: &lt;code&gt;?sslmode=require&amp;amp;pgbouncer=true&amp;amp;connect_timeout=15&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Google Cloud Run
&lt;/h3&gt;

&lt;p&gt;Billing must be linked, though usage stays inside the always-free tier at demo traffic. Every command below also runs from &lt;a href="https://cloud.google.com/shell" rel="noopener noreferrer"&gt;Cloud Shell&lt;/a&gt; in the browser, which comes pre-authenticated, so no local CLI is required.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud projects create &amp;lt;PROJECT_ID&amp;gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; gcloud config &lt;span class="nb"&gt;set &lt;/span&gt;project &amp;lt;PROJECT_ID&amp;gt;
gcloud services &lt;span class="nb"&gt;enable &lt;/span&gt;run.googleapis.com artifactregistry.googleapis.com
gcloud artifacts repositories create shop &lt;span class="nt"&gt;--repository-format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;docker &lt;span class="nt"&gt;--location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-central1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First deploy, which seeds the catalog because &lt;code&gt;SEED_ON_STARTUP=true&lt;/code&gt; is set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud auth configure-docker us-central1-docker.pkg.dev
docker build &lt;span class="nt"&gt;-f&lt;/span&gt; docker/production/Dockerfile.keystone &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-t&lt;/span&gt; us-central1-docker.pkg.dev/&amp;lt;PROJECT_ID&amp;gt;/shop/keystone:initial &lt;span class="nb"&gt;.&lt;/span&gt;
docker push us-central1-docker.pkg.dev/&amp;lt;PROJECT_ID&amp;gt;/shop/keystone:initial

gcloud run deploy keystone-admin &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt; us-central1-docker.pkg.dev/&amp;lt;PROJECT_ID&amp;gt;/shop/keystone:initial &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-central1 &lt;span class="nt"&gt;--min-instances&lt;/span&gt; 0 &lt;span class="nt"&gt;--max-instances&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--memory&lt;/span&gt; 512Mi &lt;span class="nt"&gt;--cpu&lt;/span&gt; 1 &lt;span class="nt"&gt;--allow-unauthenticated&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set-env-vars&lt;/span&gt; &lt;span class="s2"&gt;"DATABASE_URL=&amp;lt;NEON_DIRECT_URL&amp;gt;,SESSION_SECRET=&amp;lt;openssl rand -hex 32&amp;gt;,STORAGE_KIND=local,PRISMA_SCHEMA_DISABLE_ADVISORY_LOCK=true,SEED_ON_STARTUP=true,KEYSTONE_PUBLIC_URL=&amp;lt;filled after first deploy&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Right after the first deploy, confirm the catalog exists, then remove the seed flag and set the real public URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud run services update keystone-admin &lt;span class="nt"&gt;--region&lt;/span&gt; us-central1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--remove-env-vars&lt;/span&gt; SEED_ON_STARTUP &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--update-env-vars&lt;/span&gt; &lt;span class="nv"&gt;KEYSTONE_PUBLIC_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://&amp;lt;service-url&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then set a $1 budget alert in Billing so any accidental spend pings you early.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Close the &lt;code&gt;initFirstItem&lt;/code&gt; window immediately.&lt;/strong&gt; Keystone lets anyone create&lt;br&gt;
the first admin user while the User table is empty. Open the service URL and&lt;br&gt;
create yours right after the first deploy, so nobody else can.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Note: the container filesystem is ephemeral, so admin-uploaded images&lt;br&gt;
(&lt;code&gt;STORAGE_KIND=local&lt;/code&gt;) disappear on the next cold start. The catalog itself lives in Neon and in repo-shipped static images, so it is unaffected. Wire S3 or R2 and &lt;code&gt;STORAGE_KIND=s3&lt;/code&gt; if uploads ever need to persist.&lt;/p&gt;
&lt;h3&gt;
  
  
  Vercel
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Import the GitHub repo at &lt;a href="https://vercel.com/new" rel="noopener noreferrer"&gt;vercel.com/new&lt;/a&gt; with the Next.js preset.&lt;/li&gt;
&lt;li&gt;Override the build command to &lt;code&gt;npx keystone postinstall &amp;amp;&amp;amp; next build&lt;/code&gt;. The first half generates the Keystone and Prisma types without a database; the second half prerenders the pages, which does query Neon, so the pooled    &lt;code&gt;DATABASE_URL&lt;/code&gt; must be present at build time.&lt;/li&gt;
&lt;li&gt;Set the environment variables (next section).&lt;/li&gt;
&lt;li&gt;Vercel then deploys on every push through its GitHub integration.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  GitHub Actions secrets
&lt;/h3&gt;

&lt;p&gt;Create a deploy service account, grant it the three roles the workflow needs, and export a key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud iam service-accounts create github-deploy
&lt;span class="k"&gt;for &lt;/span&gt;role &lt;span class="k"&gt;in &lt;/span&gt;run.admin artifactregistry.writer iam.serviceAccountUser&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;gcloud projects add-iam-policy-binding &amp;lt;PROJECT_ID&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--member&lt;/span&gt; serviceAccount:github-deploy@&amp;lt;PROJECT_ID&amp;gt;.iam.gserviceaccount.com &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--role&lt;/span&gt; roles/&lt;span class="nv"&gt;$role&lt;/span&gt;
&lt;span class="k"&gt;done
&lt;/span&gt;gcloud iam service-accounts keys create key.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--iam-account&lt;/span&gt; github-deploy@&amp;lt;PROJECT_ID&amp;gt;.iam.gserviceaccount.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;GCP_PROJECT_ID&lt;/code&gt; (the project id) and &lt;code&gt;GCP_SA_KEY&lt;/code&gt; (the contents of&lt;br&gt;
&lt;code&gt;key.json&lt;/code&gt;) as repository secrets, then delete the local key file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Environment variables
&lt;/h2&gt;

&lt;p&gt;Set in three places, and some values must match across them.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Vercel&lt;/th&gt;
&lt;th&gt;Cloud Run&lt;/th&gt;
&lt;th&gt;Local&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DATABASE_URL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;pooled&lt;/td&gt;
&lt;td&gt;direct&lt;/td&gt;
&lt;td&gt;optional&lt;/td&gt;
&lt;td&gt;pooled for the storefront, direct for migrations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;DB_*&lt;/code&gt; (name/user/password/host/port)&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;dev docker fallback when no &lt;code&gt;DATABASE_URL&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SESSION_SECRET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;optional&lt;/td&gt;
&lt;td&gt;same value on both hosts (&lt;code&gt;openssl rand -hex 32&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;REVALIDATE_SECRET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;optional&lt;/td&gt;
&lt;td&gt;guards &lt;code&gt;/api/revalidate&lt;/code&gt; and &lt;code&gt;/api/sync-supplier&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;KEYSTONE_PUBLIC_URL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;public base URL for Keystone-served images&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PRISMA_SCHEMA_DISABLE_ADVISORY_LOCK&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;needed for Neon migrations on boot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SEED_ON_STARTUP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;temporary&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;true&lt;/code&gt; (dev)&lt;/td&gt;
&lt;td&gt;flip on to seed, then remove&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;STORAGE_KIND&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;local&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;local&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;local&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;local&lt;/code&gt; or &lt;code&gt;s3&lt;/code&gt; for admin uploads&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pair that trips people up is &lt;code&gt;DATABASE_URL&lt;/code&gt;: pooled on Vercel, direct on Cloud Run.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;&lt;code&gt;SESSION_SECRET&lt;/code&gt; must be identical on Vercel and Cloud Run.&lt;/strong&gt; The Vercel&lt;br&gt;
build imports the auth code that reads it, so a mismatch breaks the build, not&lt;br&gt;
just runtime sessions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Locking down the public GraphQL endpoint
&lt;/h2&gt;

&lt;p&gt;The Keystone GraphQL API on Cloud Run is reachable from the public internet. That raises two separate questions: who is allowed to read the data, and who is allowed to hit the endpoint at all. I handled the first one in code. The second one has a range of options, and I stopped at the point that is enough for a demo.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I did: access control in the schema
&lt;/h3&gt;

&lt;p&gt;Keystone has a per-list &lt;a href="https://keystonejs.com/docs/guides/auth-and-access-control" rel="noopener noreferrer"&gt;access control&lt;/a&gt; API, and I used it to session-gate everything that holds personal data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;User&lt;/code&gt;, &lt;code&gt;Order&lt;/code&gt;, &lt;code&gt;Cart&lt;/code&gt;, and &lt;code&gt;CartProduct&lt;/code&gt; now require an admin session for every query and mutation. An anonymous caller on the public endpoint can no longer read customer emails, enumerate users, or forge and delete orders.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Product&lt;/code&gt;, &lt;code&gt;Category&lt;/code&gt;, and &lt;code&gt;Currency&lt;/code&gt; stay publicly queryable, because they are the read-only catalog and hold no personal data. Their writes are admin-only.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also disabled the GraphQL playground and schema introspection in production. In local development Keystone serves a playground and lets you introspect the schema, which is convenient. In production both are off, so the endpoint does not advertise its shape to strangers.&lt;/p&gt;

&lt;p&gt;None of this touched the storefront. It reads and writes through the in-process Keystone context, which runs as an admin and bypasses access control, so tightening the public endpoint changed nothing for users.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Access control is the real lock, not CORS or a hidden playground.&lt;/strong&gt;&lt;br&gt;
Turning off the playground and introspection is defense-in-depth: it stops the&lt;br&gt;
endpoint from advertising its schema, but a determined caller with &lt;code&gt;curl&lt;/code&gt; does&lt;br&gt;
not need the schema to start guessing. What actually protects the data is&lt;br&gt;
per-list access control. CORS only restricts cross-origin JavaScript in a&lt;br&gt;
browser and does nothing to a bot or a script.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Going further: taking the endpoint off the public internet
&lt;/h3&gt;

&lt;p&gt;For a demo, the access control above is enough, and I stopped there. If this took real traffic, or if bots hammering &lt;code&gt;/api/graphql&lt;/code&gt; became a nuisance, the next step is to stop anonymous requests from reaching the service at all. Because the storefront never calls Cloud Run over HTTP, the whole service can be made private without breaking anything. The options run from free to paid:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cloud Run IAM auth plus an authenticated proxy (free).&lt;/strong&gt; Make the service private with &lt;code&gt;--no-allow-unauthenticated&lt;/code&gt;, and anonymous traffic is rejected at the ingress before a container even starts, so bot spam costs no instance time. To use the admin you open a local proxy signed in as your Google account. The trade-off is that there is no public, bookmarkable admin URL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A load balancer with IAP or Cloud Armor (paid).&lt;/strong&gt; A load balancer is a managed entry point that receives all traffic and forwards it to the service, and on GCP it is also where the two features below attach. It has no free tier, so this route starts around $18 a month whether or not anyone visits.

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://cloud.google.com/iap" rel="noopener noreferrer"&gt;Identity-Aware Proxy (IAP)&lt;/a&gt;&lt;/strong&gt; puts a Google sign-in gate in front of a public URL, so only identities you allow can reach the app. This is the choice if you want a public admin URL protected by login.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://cloud.google.com/armor" rel="noopener noreferrer"&gt;Cloud Armor&lt;/a&gt;&lt;/strong&gt; is Google's web application firewall (WAF) and rate-limiting layer. You can allow only your own IP, or throttle requests per IP, for example 60 a minute, so a bot hammering &lt;code&gt;/api/graphql&lt;/code&gt; gets rejected with a 429. This is the direct answer to endpoint spam.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Rough cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Run IAM + local proxy&lt;/td&gt;
&lt;td&gt;Rejects all anonymous traffic at the ingress; admin via an authenticated proxy&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IAP&lt;/td&gt;
&lt;td&gt;Google sign-in gate in front of a public admin URL&lt;/td&gt;
&lt;td&gt;~$18/mo (the load balancer)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Armor&lt;/td&gt;
&lt;td&gt;WAF and per-IP rate limiting on the endpoint&lt;/td&gt;
&lt;td&gt;~$18/mo + policy and per-request fees&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For now the endpoint stays publicly reachable, the data is protected by the access control in the schema, &lt;code&gt;--max-instances 1&lt;/code&gt; bounds the cost of any abuse, and the $1 budget alert would surface it. That is a deliberate stopping point, not an oversight.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;The whole thing rests on one architectural decision: the storefront reads the database directly through Keystone's embedded context instead of calling the admin service. That decoupling is what lets the admin scale to zero, lets each service deploy on its own, and keeps the bill at zero for a demo.&lt;/p&gt;

&lt;p&gt;Everything else follows from taking that separation seriously:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the multi-stage Docker build and the Debian base that keeps Prisma alive on TLS&lt;/li&gt;
&lt;li&gt;the pooled versus direct Neon connection strings&lt;/li&gt;
&lt;li&gt;the seed-on-boot hook gated behind an environment variable&lt;/li&gt;
&lt;li&gt;the CI/CD split between &lt;code&gt;dev&lt;/code&gt; and &lt;code&gt;master&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to poke at it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/vavilov2212/digital-products-shop" rel="noopener noreferrer"&gt;https://github.com/vavilov2212/digital-products-shop&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live shop:&lt;/strong&gt; &lt;a href="https://digital-products-shop.vercel.app" rel="noopener noreferrer"&gt;https://digital-products-shop.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>graphql</category>
      <category>googlecloud</category>
      <category>docker</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Run -&gt; Log -&gt; Distill: a self-improving memory for AI-assisted development</title>
      <dc:creator>vavilov2212</dc:creator>
      <pubDate>Sun, 12 Jul 2026 16:38:08 +0000</pubDate>
      <link>https://dev.to/vavilov2212/run-log-distill-a-self-improving-memory-for-ai-assisted-development-bgo</link>
      <guid>https://dev.to/vavilov2212/run-log-distill-a-self-improving-memory-for-ai-assisted-development-bgo</guid>
      <description>&lt;p&gt;Every AI coding session starts with amnesia.&lt;/p&gt;

&lt;p&gt;Yesterday the agent spent two hours hunting down a state-sharing bug. Today it will happily reintroduce that same bug, in the same codebase, with the same confidence. The context window closed, and everything the agent learned went with it.&lt;/p&gt;

&lt;p&gt;I'm building a full-stack GraphQL store (Next.js + Keystone 6 + PostgreSQL, dissected in the rest of this series) almost entirely through AI-assisted sessions in Claude Code. A few sessions in, I got tired of the amnesia and changed the workflow itself. I now run every task with &lt;code&gt;Run → Log → Distill&lt;/code&gt;, built from these ideas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;while the agent works, it appends to a &lt;strong&gt;WAL&lt;/strong&gt; (a write-ahead log: the work journal);&lt;/li&gt;
&lt;li&gt;when the work is done, a &lt;strong&gt;compaction&lt;/strong&gt; step melts that log into a &lt;strong&gt;post-mortem&lt;/strong&gt;: a small file of rules that every future session must read before writing code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This pattern is being reinvented all over the industry right now. The tools are shipping it as a native feature: &lt;a href="https://code.claude.com/docs/en/memory" rel="noopener noreferrer"&gt;Claude Code auto memory&lt;/a&gt;, Cursor Memories, &lt;a href="https://docs.devin.ai/desktop/cascade/memories" rel="noopener noreferrer"&gt;Devin's Cascade Memories&lt;/a&gt;. Agents that write reflections on their own failures and reread them on the next attempt are described here: &lt;a href="https://arxiv.org/abs/2303.11366" rel="noopener noreferrer"&gt;Reflexion&lt;/a&gt; or here &lt;a href="https://every.to/guides/compound-engineering" rel="noopener noreferrer"&gt;compounding engineering&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;The pattern described in this article moves with you between tools, works today in any of them, and nothing enters long-term memory without your review. More links in prior art at the end.&lt;/p&gt;

&lt;p&gt;Below are three prompts you can copy as-is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Chat history is not knowledge&lt;/li&gt;
&lt;li&gt;Two kinds of memory: a WAL and a post-mortem&lt;/li&gt;
&lt;li&gt;The lifecycle at a glance&lt;/li&gt;
&lt;li&gt;
Case 1: a planned refactor

&lt;ul&gt;
&lt;li&gt;Prompt 1: execute the plan&lt;/li&gt;
&lt;li&gt;Prompt 2: compact the journal&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
Case 2: a spontaneous session

&lt;ul&gt;
&lt;li&gt;Prompt 3: distill a spontaneous session&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The loop closes&lt;/li&gt;
&lt;li&gt;Don't keep the journal in CLAUDE.md&lt;/li&gt;
&lt;li&gt;Limitations&lt;/li&gt;
&lt;li&gt;Prior art&lt;/li&gt;
&lt;li&gt;Steal it&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Chat history is not knowledge
&lt;/h2&gt;

&lt;p&gt;Most AI coding setups already have some persistent context. Claude Code reads &lt;code&gt;CLAUDE.md&lt;/code&gt;, Cursor has rules files. In practice these hold static facts: how to run the dev server, where the tests live, what the folders mean.&lt;/p&gt;

&lt;p&gt;The knowledge that actually matters looks different:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;why&lt;/em&gt; a refactor was done, so the next session doesn't undo it;&lt;/li&gt;
&lt;li&gt;which innocent-looking pattern caused a production bug, and the exact command that diagnoses it;&lt;/li&gt;
&lt;li&gt;which corners were cut on purpose, so they get revisited instead of rediscovered.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes the agent write transcripts about its own work, while the context is still in the window.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two kinds of memory: a WAL and a post-mortem
&lt;/h2&gt;

&lt;p&gt;Cognitive science splits memory into &lt;strong&gt;episodic&lt;/strong&gt; (what happened to me: "I burned my hand on that kettle") and &lt;strong&gt;semantic&lt;/strong&gt; (what I know: "kettles are hot"). A brain constantly converts the first into the second. &lt;a href="https://arxiv.org/abs/2309.02427" rel="noopener noreferrer"&gt;CoALA - Cognitive Architectures for Language Agents&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The WAL.&lt;/strong&gt; Before a database touches its real data structures, it appends the intended change to a write-ahead log. Appending to the end of a file is the fastest, safest thing a disk can do. If the process crashes mid-write, the log replays and nothing is lost. PostgreSQL calls it WAL literally (&lt;code&gt;pg_wal&lt;/code&gt;), MySQL calls it the redo log, SQLite has a WAL mode. The price is that a WAL is write-optimized and read-terrible: it's a chronological dump, and finding the current state of anything means replaying history.&lt;/p&gt;

&lt;p&gt;Our &lt;strong&gt;work journal&lt;/strong&gt; is a WAL: an append-only file the agent writes during execution, cheap and raw, full of detail nobody will ever reread in full.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compaction.&lt;/strong&gt; Log-structured engines (RocksDB, Cassandra) periodically merge their raw files: overwritten values get dropped, duplicates collapse, and what remains is small and fast to read. Kafka literally calls its variant "log compaction". Our &lt;strong&gt;distillation step&lt;/strong&gt; is compaction: the raw journal (or the raw session) gets melted into current rules, and the journal is archived.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The compacted store.&lt;/strong&gt; Here it's a file named &lt;code&gt;POST_MORTEM.md&lt;/code&gt;, in honor of the SRE practice of blameless incident post-mortems. Purists will note that a classic post-mortem covers one incident, while this file is cumulative, more like the project's case law. Same spirit though: root causes, repair guides, action items, no blame. It holds four kinds of records: architectural decisions with their &lt;em&gt;why&lt;/em&gt;, numbered Production Guardrails, pitfalls with repair guides, and a ledger of consciously deferred tech debt.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Databases&lt;/th&gt;
&lt;th&gt;This workflow&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WAL: fast append during work, raw, chronological&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;docs/worklog/&amp;lt;date&amp;gt;-&amp;lt;plan&amp;gt;.md&lt;/code&gt;, written as the agent works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compaction: merge the log into a compact, read-optimized store&lt;/td&gt;
&lt;td&gt;Distillation prompt at the end of a plan or session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The compacted store, what readers actually query&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;POST_MORTEM.md&lt;/code&gt;: guardrails, pitfalls, tech debt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Old log segments deleted after compaction&lt;/td&gt;
&lt;td&gt;Journal archived; git keeps the history&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One more file ties the loop together. The agent's auto-loaded entry point (&lt;code&gt;CLAUDE.md&lt;/code&gt; for Claude Code) stays thin: project description, commands, and one hard pointer as its first line:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;👉 &lt;strong&gt;IMPORTANT: Before writing any new code, adding features, or refactoring, you MUST first read &lt;code&gt;POST_MORTEM.md&lt;/code&gt; and strictly follow the architectural rules and Production Guardrails recorded there.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;💡 That single line turns &lt;code&gt;POST_MORTEM.md&lt;/code&gt; from documentation the agent might stumble upon into a precondition for writing code.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The lifecycle at a glance
&lt;/h2&gt;

&lt;p&gt;Two modes, three prompts total:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Planned work&lt;/th&gt;
&lt;th&gt;Spontaneous work&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Phase 0&lt;/td&gt;
&lt;td&gt;Plan with a task dependency graph&lt;/td&gt;
&lt;td&gt;(no plan; work just happens)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Phase 1: Run → Log&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Prompt 1&lt;/strong&gt;: one item at a time, WAL entry after each&lt;/td&gt;
&lt;td&gt;the session context itself plays the role of the log&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Phase 2: Distill&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Prompt 2&lt;/strong&gt;: compact the WAL into &lt;code&gt;POST_MORTEM.md&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Prompt 3&lt;/strong&gt;: distill straight from session context&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both modes are shown below on real tasks from this store, with the prompts in full.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 1: a planned refactor
&lt;/h2&gt;

&lt;p&gt;The best showcase of the planned mode grew out of the ugliest bug in this project's history. Every page kept its server-fetched data in a module-level store object. A module in Next.js initializes once per process, so that object was silently shared between all requests and all users. When one visitor hit an error, the error state stuck, and every visitor after them saw the error page until restart. (The full detective story is a later part of this series.)&lt;/p&gt;

&lt;p&gt;The fix couldn't be a one-liner: introduce a per-request store provider, migrate every page and component to it, split client-only UI state into its own store, delete two dead abstractions along the way. A dozen-plus interdependent items, which is work that needs a plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 0: a plan with a dependency graph
&lt;/h3&gt;

&lt;p&gt;For planned work I don't start from a prompt at all. I use the &lt;a href="https://github.com/obra/superpowers" rel="noopener noreferrer"&gt;Superpowers&lt;/a&gt; plugin for Claude Code, a community skill library that forces a brainstorm → design → implementation-plan pipeline before any code gets written. Any planning approach works. The output that matters for this workflow is a plan whose appendix contains a task dependency graph: which items block which, and which are independent quick wins.&lt;/p&gt;

&lt;p&gt;That graph is what makes strictly iterative execution possible: at any moment the agent knows what one atomic, unblocked item looks like. For the refactor above, the store factory had to exist before any page could migrate to it, but the pages could migrate independently of each other.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 1: run and log
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Prompt 1: execute the plan
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are an autonomous AI coding agent. Your task is to execute the
attached implementation plan step by step.

So that the codebase and your memory evolve together, we will use the
practice "Run -&amp;gt; Log -&amp;gt; Distill -&amp;gt; Repeat". Your goal is not just to
write code, but to log the process and accumulate experience in the
work journal file `docs/worklog/&amp;lt;date&amp;gt;-&amp;lt;plan-name&amp;gt;.md`.

### RULES AND ALGORITHM:
1. Study the Appendix (the task dependency graph) at the end of the
   plan. You may not start a child task until its parents are done.
2. Work strictly iteratively: ONE specific item per ONE step. Do not
   attempt several tasks at once.
3. The algorithm for each item:
   a) [Maker] Write or change the code for the current isolated task.
   b) [Verifier] Run &amp;lt;verify command&amp;gt; and confirm nothing is broken.
   c) [Distill] Pause and append an entry to the work journal.
   d) The step is complete. Send me a short report and wait for my
      approval before moving to the next item.

### JOURNAL FORMAT:
After each completed item, append an entry to the journal, strictly
following these 5 criteria:
1. [Verified Facts] Which item is done, which files changed, which
   checks passed.
2. [What Worked] Successful patterns and approaches worth repeating.
3. [Distilled Rules] Provisional rules the next steps must respect
   (imports, dependency injection, state management).
4. [Pitfalls &amp;amp; What to Avoid] Compilation errors, dependency conflicts
   or bugs you hit on this item, and how you worked around them.
5. [What's Next] The next atomic task from the plan and its
   prerequisites.

Start with the first unblocked item (the quick wins, if the plan marks
them). What is your first concrete task? Describe what exactly you are
about to change in the code.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;📝 &lt;strong&gt;Fill in:&lt;/strong&gt; &lt;code&gt;&amp;lt;verify command&amp;gt;&lt;/code&gt; (your test or build command: &lt;code&gt;npm test&lt;/code&gt;, &lt;code&gt;flutter test&lt;/code&gt;, &lt;code&gt;cargo check&lt;/code&gt;), the journal path, and attach or reference the plan itself.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The prompt forces a rhythm: code, verify, log, report, wait. The waiting is there so that an agent drifting off course gets caught after one item instead of after ten. The logging happens right after verification, while the dead ends are still in the context window, because a diff read tomorrow won't tell you why any of it happened. And nothing enters the journal until the checks pass; that rule alone separates a work log from a chat transcript.&lt;/p&gt;

&lt;p&gt;Here's an entry from that refactor's run (lightly reconstructed):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;### Item 7: migrate category page to the per-request store&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; [Verified Facts] Item 7 done. Changed: category/[id]/page.tsx.
   &lt;span class="sb"&gt;`npm run typecheck`&lt;/span&gt; clean, smoke test green.
&lt;span class="p"&gt;2.&lt;/span&gt; [What Worked] Same pattern as items 5-6: the page fetches data and
   passes it as initState to ProductsStoreProvider; components read
   via useProductsStore(selector).
&lt;span class="p"&gt;3.&lt;/span&gt; [Distilled Rules] Selectors must return scalars or stable
   references. An object literal inside a selector caused an infinite
   re-render loop on item 6; don't repeat it here or on item 8.
&lt;span class="p"&gt;4.&lt;/span&gt; [Pitfalls &amp;amp; What to Avoid] Keystone query results are class
   instances; passing them directly into a client component triggers
   "Only plain objects can be passed to Client Components". Wrap them
   in toPlainObject() before they cross the RSC boundary.
&lt;span class="p"&gt;5.&lt;/span&gt; [What's Next] Item 8 (product page) was blocked by this item; now
   unblocked. Prerequisite: store factory (item 2, done).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Entries 3 and 4 are the valuable ones: a rule and a trap caught mid-run, written while the context was hot, phrased as advice for the next item. This is the raw material compaction feeds on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 2: compaction
&lt;/h3&gt;

&lt;p&gt;When the last item is done, I run the second prompt.&lt;/p&gt;

&lt;h4&gt;
  
  
  Prompt 2: compact the journal
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;We have fully completed the plan! All tests pass. Our final task is a
total distillation of the experience, to capitalize it for the long
term.

Melt the work journal (docs/worklog/&amp;lt;date&amp;gt;-&amp;lt;plan-name&amp;gt;.md) down into
our architectural memory, and make sure `CLAUDE.md` remains a clean
entry point. Do not limit yourself to 5-7 bullet points: lay out every
important technical insight and rule, with no length limit.

Perform two actions, strictly in this order:

### ACTION 1: Create or extend `POST_MORTEM.md`
Record the codebase's Book of Law in the project root. Add a section
for this run, labeled with the work type ([feature] / [refactor] /
[redesign] / [deploy] / [debug] / [mixed]), and integrate its lessons
into the global sections:
1. ARCHITECTURAL CHANGES: what changed in the structure of the
   codebase and why. Justify each decision so a future session doesn't
   undo it.
2. PRODUCTION GUARDRAILS: the rules for writing code that
   crystallized out of this run (imports, state management, where
   mutable state is categorically forbidden, etc.). Phrase them as
   hard, numbered standards; justify every rule.
3. PITFALLS &amp;amp; TROUBLESHOOTING: every trap we hit, with a repair guide
   for each in case it recurs: symptom, root cause, the exact
   diagnostic commands.
4. FUTURE BACKLOG: a numbered ledger of deferred tech debt and areas
   needing further work.

### ACTION 2: Verify the entry point `CLAUDE.md`
Remove any temporary statuses, logs, or plan steps that leaked into
`CLAUDE.md`. It must contain ONLY:
1. A short project description.
2. Basic commands (run, test, build).
3. This hard pointer on the first line:
"👉 IMPORTANT: Before writing any new code, adding features, or
refactoring, you MUST first read POST_MORTEM.md and strictly follow
the architectural rules and Production Guardrails recorded there."

Apply the changes, archive the work journal, and show me the structure
of the resulting POST_MORTEM.md.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compaction of that refactor's journal produced the first entries of the project's case law. Two of them:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;1. It is FORBIDDEN to declare a mutable module-level object in server components / server actions.&lt;/strong&gt; A module in Next.js initializes once per process: such an object is shared between ALL requests and ALL users. Any per-request state must be created inside the component function.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Server data reaches the client ONLY two ways:&lt;/strong&gt; (a) a per-request store created by the page with its data as &lt;code&gt;initState&lt;/code&gt;; (b) plain props. A global module-level store for server data is FORBIDDEN, as are "hydrator" components calling &lt;code&gt;setState&lt;/code&gt; during render.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Both came out numbered and imperative, with the reasoning attached, and that formatting does real work. Agents obey "FORBIDDEN / MUST" far more reliably than "prefer / consider", and numbers make rules citable: the agent cites them in code comments, and I cite them in reviews ("this violates guardrail 3").&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 2: a spontaneous session
&lt;/h2&gt;

&lt;p&gt;Everything above assumes a plan existed. Plenty of sessions don't have one, and the best example here is the day of this store's first production deploy.&lt;/p&gt;

&lt;p&gt;The plan looked trivial: push the admin-panel image to Google Cloud Run, run migrations against the managed Postgres (Neon), done. Instead, the container died on startup with the most generic error a platform can produce: "Container failed to start and listen on PORT=8080". No stack trace, and everything ran green locally. Three hours of digging later, the story emerged: the Alpine-based image shipped Prisma engines linked against OpenSSL 1.1, Node 20 links OpenSSL 3, and the query engine segfaulted during the TLS handshake with the production database. Local Postgres has no TLS, so no local run could have caught it. The eventual fix was one line in the Dockerfile; the three hours were spent finding that line.&lt;/p&gt;

&lt;p&gt;No plan predicted any of this, so no journal was being kept. Sessions like this are common (the quick fix that grows into a refactor, the deploy that turns into a debugging saga), and they produce the most valuable lessons, because nothing about them was predicted.&lt;/p&gt;

&lt;p&gt;For these sessions there's a third prompt, run once at the end.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Prompt 3 distills from the live context window, so it has to run &lt;strong&gt;before the session closes&lt;/strong&gt;. Next morning the context is gone.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Prompt 3: distill a spontaneous session
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;We've finished a working session in which we completed a number of
varied tasks and ad-hoc code fixes without rigid upfront planning. Now
we need to distill and capitalize this spontaneous experience following
the "Run -&amp;gt; Log -&amp;gt; Distill" principle (a self-improving system).

Your task: analyze the entire current session, the history of changed
files, and the context of the bugs we solved, then record that
experience in our architectural memory. Don't limit yourself on length;
dump every important engineering insight.

Perform two actions:

### ACTION 1: Extend/update the file `POST_MORTEM.md`
Find or create `POST_MORTEM.md` in the project root. Carefully
integrate this session's experience into it. Add a section for this
session, labeled with the work type ([feature] / [refactor] /
[redesign] / [deploy] / [debug] / [mixed]), organized into these
blocks:
1. [Architectural changes]: What exactly did we change in the
   structure of the codebase and why (which files we split, which
   dependencies we rewrote, which patterns we applied).
2. [Production Guardrails]: Which new rules for writing code, imports,
   state management, or the design system crystallized out of today's
   changes. Phrase them as hard, numbered standards that must not be
   violated.
3. [Lessons &amp;amp; Pitfalls]: Which non-obvious bugs, compilation errors,
   or library conflicts did we hit today? Why did they happen and how
   did we solve them? Write a detailed repair guide in case they
   recur.
4. [Residual tech debt]: What loose ends did we leave? What from
   today's work needs to be finished, refactored, or covered with
   tests in future sessions.

### ACTION 2: Refresh the entry point `CLAUDE.md`
Make sure `CLAUDE.md` in the project root is clean of process logs,
contains only current build/test commands, and has this hard pointer
on its first line:
"👉 IMPORTANT: Before writing any new code, adding features, or
refactoring, you MUST first read POST_MORTEM.md and strictly follow
the architectural rules and Production Guardrails recorded there."

Do the analysis, update the files, and give me a short summary: which
key rules and pitfalls from this session did you just commit to
memory.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It differs from Prompt 2 in three ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The source is the session context, not a journal.&lt;/strong&gt; There's no WAL to compact, so the agent works from what's still in the window: the conversation, the diffs, the bugs solved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It integrates instead of creating.&lt;/strong&gt; Prompt 2 assumes a completed plan and writes a definitive section. Prompt 3 says "find or create" and merges carefully into whatever case law already exists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It ends with a ranked summary.&lt;/strong&gt; "Tell me which rules you just committed to memory" forces the agent to prioritize, and gives me a 30-second review of what just entered long-term memory. The human stays editor-in-chief. I occasionally veto a rule that over-generalizes a one-off; a self-improving system with no editor drifts into superstition.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run at the end of the deploy saga, this prompt produced the project's most valuable pitfall entry:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;3.12 "Container failed to start on PORT=8080", which was actually a segfault.&lt;/strong&gt; Prisma's musl engines are linked against OpenSSL 1.1; &lt;code&gt;node:20-alpine&lt;/code&gt; ships OpenSSL 3; the query engine SIGSEGVs exactly at the TLS handshake with the production database. Migrations still work (separate process!), local runs are green (local Postgres has no TLS!). &lt;strong&gt;Diagnosis:&lt;/strong&gt; run the same image locally with the production &lt;code&gt;DATABASE_URL&lt;/code&gt; and read the full stderr. &lt;strong&gt;Fix:&lt;/strong&gt; &lt;code&gt;node:20-slim&lt;/code&gt;. &lt;strong&gt;Lesson:&lt;/strong&gt; "works on my machine" must include production-mode TLS.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The repair guide is deliberately a command: the next session can rerun the diagnosis in ten seconds instead of rediscovering it. A session later the lesson got generalized into guardrail 18: any image that talks TLS to a database in production gets tested against a TLS database before deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop closes
&lt;/h2&gt;

&lt;p&gt;The payoff came from a third task. Early on, the storefront called the supplier's B2B API during page rendering. One day the supplier API got flaky and took the whole storefront down with it. The distilled lesson became guardrail 4: external APIs are best-effort only, sync never sits on the render path, &lt;code&gt;try { sync } catch { log }&lt;/code&gt;, then serve data from our own DB.&lt;/p&gt;

&lt;p&gt;Sessions later, the agent was writing a new data-access function, and this comment appeared in the diff, unprompted:&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="c1"&gt;// Pure DB read: the supplier sync is NOT called here - pages are statically&lt;/span&gt;
&lt;span class="c1"&gt;// cached (ISR), so external APIs must stay off the render path (Guardrail 4,&lt;/span&gt;
&lt;span class="c1"&gt;// tech-debt #9). Sync runs on demand via POST /api/sync-supplier.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getProducts&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 rule number, the tech-debt entry that rule closed, and the alternative chosen because of them: the agent is citing the project's own case law while writing new code. This comment did more to convince me the setup works than anything else in five sessions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't keep the journal in CLAUDE.md
&lt;/h2&gt;

&lt;p&gt;An early version of Prompt 1 told the agent to keep the work journal inside &lt;code&gt;CLAUDE.md&lt;/code&gt;, the same file the agent auto-loads at the start of every session. That's also why Prompt 2 has a "clean up &lt;code&gt;CLAUDE.md&lt;/code&gt;" action: the cleanup was a symptom of the journal living in the wrong place. The prompts above already contain the fix; the reasoning behind it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The WAL stays out of the auto-loaded context file.&lt;/strong&gt; Otherwise every session pays tokens for old execution logs, and the entry point gets diluted right when it should stay sharp.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One journal file per plan or session&lt;/strong&gt;, like &lt;code&gt;docs/worklog/2026-07-08-per-request-store.md&lt;/code&gt;. Not auto-loaded. Prompt 1 names it explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt; holds only commands and pointers.&lt;/strong&gt; Mine is 48 lines: project description, how to run and verify, the hard pointer to &lt;code&gt;POST_MORTEM.md&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Archive the journal after compaction.&lt;/strong&gt; Its value has been extracted. Git keeps the raw material if you ever need it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tag sessions by work type&lt;/strong&gt;: &lt;code&gt;[feature]&lt;/code&gt;, &lt;code&gt;[refactor]&lt;/code&gt;, &lt;code&gt;[redesign]&lt;/code&gt;, &lt;code&gt;[deploy]&lt;/code&gt;, &lt;code&gt;[debug]&lt;/code&gt;, &lt;code&gt;[mixed]&lt;/code&gt; in the section header. Rules are type-agnostic (a rule born in a redesign binds a refactor just the same), but tags make future compaction of the post-mortem itself much easier.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The post-mortem grows.&lt;/strong&gt; Mine is ~300 lines after five sessions. At some point old session narratives need compaction of their own: keep the guardrails and pitfalls, collapse the story. Compaction is never finished, in databases or here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rules can over-fit.&lt;/strong&gt; A workaround for one library version can calcify into false doctrine. The numbered format helps: rules are easy to find and repeal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's discipline-dependent.&lt;/strong&gt; A good distillation pass takes a few minutes of review, and if you skip it, the knowledge from that session is simply gone.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prior art
&lt;/h2&gt;

&lt;p&gt;For digging deeper, the landscape this workflow sits in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://arxiv.org/abs/2309.02427" rel="noopener noreferrer"&gt;CoALA&lt;/a&gt;&lt;/strong&gt; (Sumers et al., 2023): the paper that mapped agent memory onto the psychology terms used above (working, episodic, semantic, procedural).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://arxiv.org/abs/2308.10144" rel="noopener noreferrer"&gt;ExpeL&lt;/a&gt;&lt;/strong&gt; (Zhao et al., 2023): agents that collect their own trajectories and extract natural-language insights from them. The academic version of the compaction step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://docs.cline.bot/prompting/cline-memory-bank" rel="noopener noreferrer"&gt;The Memory Bank pattern&lt;/a&gt;&lt;/strong&gt;: a popular markdown-memory methodology from the AI-IDE world. It solves a neighboring problem: Memory Bank reconstructs project &lt;em&gt;state&lt;/em&gt; (what we're building, where we stopped), while this workflow accumulates &lt;em&gt;lessons&lt;/em&gt; (what must never happen again). The two combine well.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://every.to/guides/compound-engineering" rel="noopener noreferrer"&gt;Compounding engineering&lt;/a&gt;&lt;/strong&gt; (Every): the same philosophy ("each unit of work should make the next one easier") grown into a full Claude Code plugin with a Plan, Work, Review, Compound cycle. A packaged alternative to the three prompts here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://addyosmani.com/blog/self-improving-agents/" rel="noopener noreferrer"&gt;Self-improving coding agents&lt;/a&gt;&lt;/strong&gt; (Addy Osmani, 2026): a survey of the looped variants, &lt;code&gt;learnings.md&lt;/code&gt; files and continuous loops included.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Steal it
&lt;/h2&gt;

&lt;p&gt;The whole system is three prompts and one pointer line. Nothing is specific to Claude Code except the &lt;code&gt;CLAUDE.md&lt;/code&gt; filename; Cursor rules, Windsurf rules, or a plain &lt;code&gt;AGENTS.md&lt;/code&gt; work the same way.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Put the hard pointer on the first line of your agent's entry-point file.&lt;/li&gt;
&lt;li&gt;Planned work: Prompt 1 to start the run, Prompt 2 when the plan is done.&lt;/li&gt;
&lt;li&gt;Everything else: Prompt 3 at the end of the session, before you close it.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;🚀 Start on an existing project today: run Prompt 3 at the end of your next session and you'll have your first guardrails by tonight. Five sessions from now, your agent will be citing them back to you.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;em&gt;This is part 1 of a build-in-public series. The project this memory system grew on, a full-stack GraphQL store (Next.js 13 + Keystone 6 + PostgreSQL) running in production for $0/month, is what the next parts dissect: the architecture, the deployment, the ISR + live supplier API mechanics.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>promptengineering</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I added a RAG-powered AI chat to my landing page - and it's 100% free</title>
      <dc:creator>vavilov2212</dc:creator>
      <pubDate>Sun, 21 Jun 2026 01:20:29 +0000</pubDate>
      <link>https://dev.to/vavilov2212/i-added-a-rag-powered-ai-chat-to-my-landing-page-and-its-100-free-48jb</link>
      <guid>https://dev.to/vavilov2212/i-added-a-rag-powered-ai-chat-to-my-landing-page-and-its-100-free-48jb</guid>
      <description>&lt;p&gt;I added a free RAG-powered AI chat assistant to the &lt;a href="https://worklog-studio-site.vercel.app/" rel="noopener noreferrer"&gt;Worklog Studio&lt;/a&gt; landing page, built with Next.js, the Gemini API, and deployed on Vercel. Here's what it does and why I bothered.&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%2Fzq7m93v240j5rg01b18y.gif" 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%2Fzq7m93v240j5rg01b18y.gif" alt="Demo of the AI chat assistant answering a question about Worklog Studio's features" width="760" height="532"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR;
&lt;/h2&gt;

&lt;p&gt;You can explore source code for this project at a public github repo &lt;a href="https://github.com/vavilov2212/worklog_studio_site" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bother with a chat assistant
&lt;/h2&gt;

&lt;p&gt;First of all - it's fun! But really, nobody reads large pieces of text anymore. Having a chatbot that knows your product is a way to motivate users to engage with what you're trying to deliver.&lt;/p&gt;

&lt;p&gt;The hard rule I set for myself: the assistant has to admit when it doesn't know something. A chatbot that confidently invents a feature that doesn't exist is worse than no chatbot at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it's wired together
&lt;/h2&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%2Fjn5rx8yl9wy3cmb0or4q.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%2Fjn5rx8yl9wy3cmb0or4q.png" alt="Worklog Studio AI chat architecture diagram" width="800" height="403"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A browser sends a question to a Next.js route running on Vercel. That route checks a rate limiter, pulls the most relevant chunks out of a small precomputed file of embeddings, and sends those chunks plus the question to Gemini. The answer streams back the same way it came in.&lt;/p&gt;

&lt;p&gt;Two entry points on the page (an inline box and a header button) share the same chat panel and the same conversation, so switching between them mid-conversation doesn't lose anything.&lt;/p&gt;

&lt;p&gt;There's no vector database, it does not use an app database, and does not need user accounts. Conversation history lives in &lt;code&gt;sessionStorage&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Worklog Studio?
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://github.com/vavilov2212/wl-studio" rel="noopener noreferrer"&gt;Worklog Studio&lt;/a&gt; is a small desktop time tracker. As of now it's a bare minimum to be useful. Projects, tasks, time entries - you can comment on each entry so you remember what you were actually doing.&lt;/p&gt;

&lt;p&gt;I built it because every existing tracker I tried was either missing something or too cluttered with features I don't need. It's open source, and if you want to poke around or contribute, here's the repo: &lt;a href="https://github.com/vavilov2212/wl-studio" rel="noopener noreferrer"&gt;github.com/vavilov2212/wl-studio&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why it needed a real server
&lt;/h2&gt;

&lt;p&gt;The site used to ship as a static export on GitHub Pages, which is fine for plain content but can't run any backend code. This feature needs one, because the Gemini API key has to stay on the server. Put that key in client-side code and anyone with dev tools open can grab it in five seconds.&lt;/p&gt;

&lt;p&gt;So the site moved to Vercel. It is Next.js app on the same repo, and deploy does not change.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs: nothing
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Used for&lt;/th&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://vercel.com/" rel="noopener noreferrer"&gt;Vercel&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;hosting + the API route&lt;/td&gt;
&lt;td&gt;Hobby (free)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://aistudio.google.com/apikey" rel="noopener noreferrer"&gt;Gemini API&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;embeddings + chat&lt;/td&gt;
&lt;td&gt;free tier, no GCP project needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://console.upstash.com/" rel="noopener noreferrer"&gt;Upstash Redis&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;rate limiting&lt;/td&gt;
&lt;td&gt;free tier&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Gemini key comes from the consumer AI Studio flow, not the Vertex AI console. I'll get into why that distinction matters in part 3.&lt;/p&gt;

&lt;h2&gt;
  
  
  Up next
&lt;/h2&gt;

&lt;p&gt;I'm splitting this into three posts because there's enough here to get lost in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Part 2&lt;/strong&gt;: the actual RAG pipeline, chunking and embedding your own content and how the retrieval code decides when to say "I don't know" instead of guessing. No vector database involved at this point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 3&lt;/strong&gt;: working with the Gemini API specifically, free tier vs. Vertex AI, and what happens when a model is overloaded.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The site's repo is &lt;a href="https://github.com/vavilov2212/worklog_studio_site" rel="noopener noreferrer"&gt;worklog_studio_site&lt;/a&gt; if you want to follow along, and &lt;a href="https://github.com/vavilov2212/wl-studio" rel="noopener noreferrer"&gt;Worklog Studio&lt;/a&gt; itself is open for contributions if time tracking is your thing.&lt;/p&gt;

&lt;p&gt;Try the assistant yourself: &lt;a href="https://worklog-studio-site.vercel.app/" rel="noopener noreferrer"&gt;worklog-studio-site.vercel.app&lt;/a&gt;. Click "Ask AI" and see what it knows.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>nextjs</category>
      <category>gemini</category>
    </item>
    <item>
      <title>🕵️‍♂️ The Case of the Missing S3 Metadata: A CORS Deep Dive</title>
      <dc:creator>vavilov2212</dc:creator>
      <pubDate>Thu, 20 Nov 2025 21:15:09 +0000</pubDate>
      <link>https://dev.to/vavilov2212/the-case-of-the-missing-s3-metadata-a-cors-deep-dive-1gag</link>
      <guid>https://dev.to/vavilov2212/the-case-of-the-missing-s3-metadata-a-cors-deep-dive-1gag</guid>
      <description>&lt;p&gt;When you upload a file to S3 with custom metadata (like fileName or uploadedAt), everything looks correct in the AWS Console. But when you try to read that metadata from your frontend using &lt;code&gt;HeadObjectCommand&lt;/code&gt;, the data clearly exists on the cloud, yet the AWS SDK response returns &lt;em&gt;empty Metadata object&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;There's a &lt;a href="https://github.com/aws/aws-sdk-js-v3/issues/3446" rel="noopener noreferrer"&gt;resolved GitHub issue&lt;/a&gt; on one of the JavaScript AWS packages.&lt;/p&gt;

&lt;p&gt;This is not an bug, the cause isn’t in the AWS SDK or your upload logic. It comes from the browser, and specifically how it handles cross-origin requests.&lt;/p&gt;

&lt;p&gt;The rest of this article breaks down where this behavior comes from and how to fix it.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;If you read my earlier article, &lt;a href="https://dev.to/vavilov2212/stop-uploading-files-to-s3-the-wrong-way-50le"&gt;“Stop Uploading Files to S3 the Wrong Way”&lt;/a&gt;, you saw a complete example of how to upload and download files efficiently using the AWS SDK with a client/server flow.&lt;/p&gt;

&lt;p&gt;This article builds directly on top of that and acts as a follow-up for developers searching for “client-side S3 metadata not showing” issues.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;This is Part 2 in a small S3 series&lt;/li&gt;
&lt;li&gt;Client and Server behave differently&lt;/li&gt;
&lt;li&gt;Why no Metadata on the Client&lt;/li&gt;
&lt;li&gt;How to Fix It with S3 CORS Rules&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  This is Part 2 in a small S3 series
&lt;/h2&gt;

&lt;p&gt;In the first part, the focus was sending and retrieving files. Here, we look at reading custom metadata from a cloud S3 provider in the browser.&lt;/p&gt;

&lt;p&gt;You don’t need to read Part 1 to follow this article, but both posts fit together if you’re building a file upload pipeline end-to-end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Client and Server behave differently
&lt;/h2&gt;

&lt;p&gt;The issue only shows up in certain execution environments, so it helps to look at how the same code behaves in your backend (when it runs on the server) versus in the browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  Server-side (✅ works as expected)
&lt;/h3&gt;

&lt;p&gt;When &lt;code&gt;HeadObjectCommand&lt;/code&gt; runs in a nodejs backend environment - for example, a Next.js API route - it returns the full metadata without any issues. Server-to-server requests aren’t limited by browser security rules, so all response headers are available.&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="c1"&gt;// src/app/api/s3/private/meta/route.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;s3Service&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;../../_lib&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;documentKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;metadata&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;s3Service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMetadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;documentKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;metadata&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;h3&gt;
  
  
  Client-side (❌ metadata comes back empty)
&lt;/h3&gt;

&lt;p&gt;The problem appears when the same request is made from browser JavaScript.&lt;br&gt;
The call succeeds, but the Metadata object in the AWS SDK response is empty.&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="c1"&gt;// src/app/api/s3/_lib/s3Service.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getFileMetadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&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;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;HeadObjectCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;Bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bucketName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;id&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Metadata&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// {} without proper CORS&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// src/components/File.tsx&lt;/span&gt;
&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;setFilename&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fileMetadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getFileMetadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// fileMetadata is empty, so this becomes undefined&lt;/span&gt;
        &lt;span class="na"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fileMetadata&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;filename&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;span class="nf"&gt;setFilename&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="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The request itself isn’t failing. S3 returns the metadata correctly.&lt;br&gt;
But because the code runs in the browser, certain response headers - including S3’s custom metadata headers - are not exposed to JavaScript by default.&lt;/p&gt;

&lt;p&gt;The next section explains why this happens.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why no Metadata on the Client
&lt;/h2&gt;

&lt;p&gt;Two fundamental security mechanisms are involved here: the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy" rel="noopener noreferrer"&gt;Same-Origin Policy (SOP)&lt;/a&gt; and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" rel="noopener noreferrer"&gt;Cross-Origin Resource Sharing (CORS)&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;S3 CORS expose headers, custom metadata missing browser, or AWS CORS not working.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Same-Origin Policy&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Browsers enforce a rule that pages can only freely access resources from the same “origin,” defined by scheme, host, and port.&lt;br&gt;
Your frontend (&lt;code&gt;https://your-app.com&lt;/code&gt;) and S3 (&lt;code&gt;https://your-bucket.s3.amazonaws.com&lt;/code&gt;) are different origins, so &lt;em&gt;the browser restricts what your code can access&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. CORS as the exception&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;CORS&lt;/em&gt; is a system that allows a server to relax these restrictions.&lt;br&gt;
If you configure S3 with an &lt;code&gt;AllowedOrigin&lt;/code&gt; matching your frontend, the browser is permitted to make the request.&lt;/p&gt;

&lt;p&gt;However, this only covers whether the request can happen - not what the browser exposes back to your JavaScript.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Access-Control-Expose-Headers&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Even with &lt;em&gt;CORS&lt;/em&gt; enabled, the browser only exposes a small set of safe default headers to the client.&lt;br&gt;
S3’s custom metadata headers (x-amz-meta-*) are not part of that list.&lt;/p&gt;

&lt;p&gt;&lt;u&gt;This means:&lt;/u&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;S3 sends the metadata correctly.&lt;/li&gt;
&lt;li&gt;The browser receives it.&lt;/li&gt;
&lt;li&gt;But the browser filters it out or hides it before the AWS SDK gets the response.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The SDK sees no metadata headers and correctly (from its perspective) returns an empty Metadata object. Unless your S3 bucket explicitly exposes the metadata headers through CORS, your client code will always see Metadata: {}.&lt;/p&gt;
&lt;h2&gt;
  
  
  How to Fix It with S3 CORS Rules
&lt;/h2&gt;

&lt;p&gt;How do I fix S3 CORS so metadata shows up in HeadObject?&lt;/p&gt;

&lt;p&gt;To make your metadata visible in the browser, you need to add your metadata fields to &lt;code&gt;Access-Control-Expose-Headers&lt;/code&gt; in your S3 bucket’s CORS config so the browser is allowed to expose the headers you care about.&lt;/p&gt;

&lt;p&gt;Add them to its HEAD request responses. This is done by updating the CORS configuration on your S3 bucket, as detailed in the official &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/ManageCorsUsing.html" rel="noopener noreferrer"&gt;Amazon S3 User Guide on Managing CORS&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A working XML example looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- S3 Bucket CORS Configuration (XML) --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;CORSConfiguration&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;CORSRule&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;AllowedOrigin&amp;gt;&lt;/span&gt;https://your-app.com&lt;span class="nt"&gt;&amp;lt;/AllowedOrigin&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;AllowedMethods&amp;gt;&lt;/span&gt;GET&lt;span class="nt"&gt;&amp;lt;/AllowedMethod&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;AllowedMethods&amp;gt;&lt;/span&gt;HEAD&lt;span class="nt"&gt;&amp;lt;/AllowedMethod&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;AllowedHeaders&amp;gt;&lt;/span&gt;*&lt;span class="nt"&gt;&amp;lt;/AllowedHeader&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ExposeHeaders&amp;gt;&lt;/span&gt;x-amz-meta-filename&lt;span class="nt"&gt;&amp;lt;/ExposeHeader&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ExposeHeaders&amp;gt;&lt;/span&gt;x-amz-meta-uploaded-by&lt;span class="nt"&gt;&amp;lt;/ExposeHeader&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- Add an &amp;lt;ExposeHeader&amp;gt; for each custom metadata key you use --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/CORSRule&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/CORSConfiguration&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;JSON Configuration Example:&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;S&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Bucket&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;CORS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Configuration&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(JSON)&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;"CORSRules"&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;span class="nl"&gt;"AllowedOrigins"&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="s2"&gt;"&amp;lt;https://your-app.com&amp;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;"AllowedMethods"&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="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HEAD"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"AllowedHeaders"&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="s2"&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;"ExposeHeaders"&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="s2"&gt;"x-amz-meta-filename"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"x-amz-meta-uploadedat"&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;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;This enables client-side S3 HeadObject to return the full metadata object.&lt;/p&gt;

&lt;p&gt;&lt;u&gt;A few important details:&lt;/u&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Expose only the headers you need.
The browser will ignore everything else.&lt;/li&gt;
&lt;li&gt;HEAD and GET both matter.
If you’re calling &lt;code&gt;HeadObjectCommand&lt;/code&gt; from the client, the HEAD method must be included or the request may fail depending on your setup.&lt;/li&gt;
&lt;li&gt;Wildcard &lt;code&gt;AllowedHeaders&lt;/code&gt; is fine here. It simply allows the browser to send custom headers, not expose them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After this configuration is applied, the browser will no longer filter out the metadata, and your AWS SDK call will return the full Metadata object instead of {}.&lt;/p&gt;

&lt;p&gt;👉 Missed Part 1?&lt;br&gt;
Read the full upload/download pipeline here: &lt;a href="https://dev.to/vavilov2212/stop-uploading-files-to-s3-the-wrong-way-50le"&gt;Stop Uploading Files to S3 the Wrong Way&lt;/a&gt;&lt;/p&gt;

</description>
      <category>s3</category>
      <category>websecurity</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>🚀 Stop Uploading Files to S3 the Wrong Way</title>
      <dc:creator>vavilov2212</dc:creator>
      <pubDate>Tue, 14 Oct 2025 01:12:53 +0000</pubDate>
      <link>https://dev.to/vavilov2212/stop-uploading-files-to-s3-the-wrong-way-50le</link>
      <guid>https://dev.to/vavilov2212/stop-uploading-files-to-s3-the-wrong-way-50le</guid>
      <description>&lt;p&gt;When you let users upload files to S3, the most intuitive solution is to create an API route that accepts a &lt;em&gt;file&lt;/em&gt; and uses the &lt;em&gt;aws-sdk&lt;/em&gt; on the server to forward it to your bucket. This way each file transfer is done twice (2x) consuming resources and creating a bottleneck. &lt;br&gt;
Uploading files directly from the browser can save your server’s bandwidth and simplify infrastructure. Here’s how to do it securely and cleanly with react and Next.js.&lt;/p&gt;


&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;TL;DR&lt;/li&gt;
&lt;li&gt;Motivation&lt;/li&gt;
&lt;li&gt;Disposition&lt;/li&gt;
&lt;li&gt;The Core:  server operates entities&lt;/li&gt;
&lt;li&gt;Step 1: The Server Side API Route for Uploads&lt;/li&gt;
&lt;li&gt;
Step 2: A Decoupled Client Side Architecture

&lt;ul&gt;
&lt;li&gt;The Data Access Layer&lt;/li&gt;
&lt;li&gt;The Business Logic Layer&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
Step 3: Implementing Secure Downloads

&lt;ul&gt;
&lt;li&gt;A Deeper Look at the Client Side Download&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Handling Environment Variables&lt;/li&gt;
&lt;li&gt;Where to go from here?&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;TL;DR&lt;/strong&gt;
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;If you know what you’re doing and just need a working example reference, here you can find the &lt;a href="https://github.com/vavilov2212/s3-cloud-with-aws-sdk" rel="noopener noreferrer"&gt;complete code&lt;/a&gt; used in this article.&lt;br&gt;
This guide covers a secure file upload to S3 using react, next.js and AWS SDK v3 with pre-signed URLs.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Motivation
&lt;/h2&gt;

&lt;p&gt;The react next.js frontend app lets users perform a secure file upload to S3 directly from the browser and let them have access to those files afterwards. Pretty straightforward feature, but let’s get deeper:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There is an operations or support team that reviews those files in an internal admin board (e.g. verify contracts, evaluate numbers in reports etc.)&lt;/li&gt;
&lt;li&gt;These files contain sensitive information and can not be accessed publicly.&lt;/li&gt;
&lt;li&gt;The client user can only see his files and not other user’s files.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of implementing your own static server solution, you can delegate storing and serverless file management to any S3-compatible object storage.&lt;/p&gt;
&lt;h2&gt;
  
  
  Disposition
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Whether you’re using Amazon S3, DigitalOcean Spaces, Linode Object Storage etc., this applies to any S3-compatible storage service. In order to upload and read files, we can use &lt;a href="https://github.com/aws/aws-sdk-js-v3/tree/main/clients/client-s3" rel="noopener noreferrer"&gt;@aws-sdk/client-s3&lt;/a&gt; for S3 compatible Object Storage.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The most intuitive solution is often to create an API route that accepts a file and then uses the AWS-SDK on the server to forward it to your bucket. This server proxy makes your application the bottleneck.&lt;br&gt;
&lt;/p&gt;

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

  +------------------+       +---------------+       +---------------+
  │  Client Browser  │  →→→  │  Your Server  │  →→→  │   S3 Bucket   │
  +------------------+       +---------------+       +---------------+
         │                         │
   (1) Upload File           (2) Forward File
         │                         │
         ↖─────────────────────────↗
              File travels twice
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every uploaded megabyte must be processed by &lt;em&gt;your server&lt;/em&gt;, consuming its bandwidth and memory, &lt;em&gt;before being sent on to S3&lt;/em&gt;. The file is transferred twice potentially exceeding memory or execution time limits (for large files).&lt;/p&gt;

&lt;p&gt;The same happens when a user downloads a file.&lt;/p&gt;

&lt;p&gt;The industry-standard solution is to offload this work by letting the client (browser) upload &lt;em&gt;directly&lt;/em&gt; to S3.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Core:  server operates entities&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Instead of doing the heavy lifting, your api route performs a quick, secure handshake.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Client Requests an Upload:&lt;/strong&gt; Your front-end makes a small API call to your server, providing metadata like the file's content type (MIME type).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Server Generates a pre-signed URL:&lt;/strong&gt; Your server &lt;em&gt;which safely stores your s3 credential&lt;/em&gt; uses the SDK to generate a temporary, single-use URL with specific permissions (e.g., "allow a &lt;code&gt;PUT&lt;/code&gt; operation for &lt;code&gt;image/png&lt;/code&gt; for the next 10 minutes").&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Client Uploads Directly to S3:&lt;/strong&gt; The client receives this URL and uses it to send the file straight to S3, bypassing your server entirely.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                     +----------------------+
                     |      Your Server     |
                     | (Auth + Signed URLs) |
                     +----------+-----------+
                                ↕
                +---------------+---------------+
                |                               |
      (1) Request Upload URL           (1) Request Download URL
                |                               |
                ↓                               ↓
          +-----+-----+                   +-----+-----+
          |  Client   |                   |  Client   |
          | (Browser) |                   | (Browser) |
          +-----+-----+                   +-----+-----+
                |                               |
     (2) Upload File using URL       (2) Use URL to Download File
                |                               |
                ↓                               ↓
          +-----+-----+                   +-----+-----+
          |    S3     |   ←–––––––––––→   |    S3     |
          | (Storage) |  (3) File Stored  | (Storage) |
          +-----------+                   +-----------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern is secure, highly scalable, and more cost-effective. Let’s build this step by step.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;The following code examples are taken from a Next.js project that uses Feature-Sliced Design (FSD) for its architecture.&lt;br&gt;
You can follow along with the complete, working code in the example repository linked above.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Step 1: The Server Side API Route for Uploads&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This Next.js API route for S3 uploads, validates the request and generates the pre-signed URL. It's also the perfect place to enforce business logic, like attaching metadata.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/app/api/upload/route.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;S3Client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PutObjectCommand&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="s2"&gt;@aws-sdk/client-s3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getSignedUrl&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="s2"&gt;@aws-sdk/s3-request-presigner&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&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="s2"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// --- This is a placeholder for your authentication logic ---&lt;/span&gt;
&lt;span class="c1"&gt;// In a real app, you would get the user's session from the request&lt;/span&gt;
&lt;span class="c1"&gt;// headers, cookies, or a token.&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getUserIdFromSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Example: return the user ID from your auth system (e.g., NextAuth, Clerk)&lt;/span&gt;
  &lt;span class="c1"&gt;// For now, we'll return a hardcoded ID.&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// -------------------------------------------------------------&lt;/span&gt;

&lt;span class="c1"&gt;// This is safe to initialize here as it only runs on the server.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s3&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;S3Client&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;region&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;S3_REGION&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`https://&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;S3_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;accessKeyId&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;S3_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;secretAccessKey&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;S3_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class="o"&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;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;contentType&lt;/span&gt; &lt;span class="p"&gt;}&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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

      &lt;span class="c1"&gt;// const userId = 123; // In a real app, derive this from the user's session.&lt;/span&gt;
      &lt;span class="c1"&gt;// 1. Get the current user's ID securely on the server&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getUserIdFromSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;command&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;PutObjectCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;Bucket&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;S3_BUCKET_NAME&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;ContentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;Metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;uploadedBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
          &lt;span class="na"&gt;uploadedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signedUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getSignedUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;expiresIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// URL is valid for 10 minutes&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;signedUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// ...error handling&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;This API route is the foundation for secure serverless file uploads in any Next.js + AWS S3 SDK integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Step 2: A Decoupled Client Side Architecture&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;To keep our code clean and testable, we'll separate frontend file upload logic from the business logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;The Data Access Layer&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;This file's only job is to execute &lt;code&gt;fetch&lt;/code&gt; requests. It handles &lt;em&gt;how&lt;/em&gt; we communicate with our APIs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/shared/api/s3.ts&lt;/span&gt;

&lt;span class="c1"&gt;// Requests a pre-signed URL from our backend.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getPresignedUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signedUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/upload&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;contentType&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Failed to get pre-signed URL.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Uploads the file to the provided S3 URL.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;uploadToS3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signedUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;File&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signedUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PUT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;S3 upload failed.&lt;/span&gt;&lt;span class="dl"&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;h3&gt;
  
  
  &lt;strong&gt;The Business Logic Layer&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;This file defines &lt;em&gt;what&lt;/em&gt; the feature does - the sequence of steps - without knowing the implementation details.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/features/file-management/model/uploadFile.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getPresignedUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;uploadToS3&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="s2"&gt;@shared/api/s3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// This function orchestrates the upload process.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;uploadFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;File&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signedUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getPresignedUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;uploadToS3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signedUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Step 3: Upload file key to store in database&lt;/span&gt;
    &lt;span class="c1"&gt;// console.log("Uploading file key to store in database...");&lt;/span&gt;
    &lt;span class="c1"&gt;// await storeUploadedFileKey(key, additionalData);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Upload failed:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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;With this separation, your UI component only needs to import and call &lt;code&gt;uploadFile(theFile)&lt;/code&gt;. The component remains simple, and your logic is reusable and easy to unit test.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Step 3: Implementing Secure Downloads&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The download flow uses the same secure pattern. First, we create an API route to generate a pre-signed &lt;code&gt;GetObject&lt;/code&gt; URL for a secure S3 file download.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/app/api/download/route.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;S3Client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;GetObjectCommand&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="s2"&gt;@aws-sdk/client-s3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ... s3 client setup is the same ...&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* handle error */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;command&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;GetObjectCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;Bucket&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;S3_BUCKET_NAME&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// This header forces the browser to download the file.&lt;/span&gt;
        &lt;span class="na"&gt;ResponseContentDisposition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`attachment;`&lt;/span&gt;
        &lt;span class="c1"&gt;// You can override the filename for the user here&lt;/span&gt;
        &lt;span class="c1"&gt;// But then you must also know the file extension&lt;/span&gt;
        &lt;span class="c1"&gt;// ResponseContentDisposition: `attachment; filename="${key}"`&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signedUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getSignedUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;expiresIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// 5 minute validity&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;signedUrl&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;Now for the client-side, which involves a browser hack to trigger the download without navigating away from the page.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/features/file-management/model/downloadFile.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getDownloadUrl&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="s2"&gt;@shared/api/s3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Assumes a getDownloadUrl function in the shared API layer&lt;/span&gt;

&lt;span class="c1"&gt;// This function orchestrates the business logic for downloading a file.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;downloadFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Step 1: Get the pre-signed download URL&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signedUrl&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getDownloadUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Step 2: Trigger the download in the browser&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signedUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;download&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;File download process failed:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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;Together, these steps form a scalable, production-ready file upload and download workflow using react Next.js, TypeScript, and AWS S3 SDK.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;A Deeper Look at the Client Side Download&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;You might be wondering about the &lt;code&gt;link.click()&lt;/code&gt; part. Why not just use &lt;code&gt;window.location.href = signedUrl&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;First, a direct navigation would rip the user out of your single-page application. Second, for content types the browser can render (like images, PDFs, or text files), it would likely just display the file instead of saving it.&lt;/p&gt;

&lt;p&gt;The programmatic anchor tag (&lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt;) solves both problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;const link = document.createElement('a');&lt;/code&gt; We create a new, invisible link element in memory.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;link.href = signedUrl;&lt;/code&gt; We set its destination to our secure, temporary S3 URL.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;link.setAttribute('download', key);&lt;/code&gt; This is the most important part. The &lt;code&gt;download&lt;/code&gt; attribute is a command to the browser: "Do not navigate to this &lt;code&gt;href&lt;/code&gt;. Instead, treat its content as a file to be saved, and use this attribute's value as the suggested filename." This forces the download behaviour.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;document.body.appendChild(link);&lt;/code&gt; The link must be attached to the document for it to be clickable.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;link.click();&lt;/code&gt; We programmatically simulate a user clicking the link, which triggers the browser's "Save As..." dialog.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;document.body.removeChild(link);&lt;/code&gt; The link has done its job, so we immediately remove it from the DOM to keep our page clean.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Handling Environment Variables&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;As a final note, manage your credentials using a &lt;code&gt;.env&lt;/code&gt; file and provide a template for other developers with a &lt;code&gt;.env.example&lt;/code&gt;. Be sure to add &lt;code&gt;.env&lt;/code&gt; to your &lt;code&gt;.gitignore&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# .env.example

# S3 Configuration

S3_BUCKET_NAME=your-s3-bucket-name
S3_REGION=your-bucket-region
S3_ENDPOINT=your-s3-endpoint
S3_ACCESS_KEY_ID=your-access-key-id
S3_SECRET_ACCESS_KEY=your-secret-access-key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Proper environment variable management for credentials is crucial for a secure S3 integration in any web app that uses serverless solutions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go from here?
&lt;/h2&gt;

&lt;p&gt;Once a file is in your S3 bucket, the next logical step is often to trigger serverless functions based on S3 events (e.g. S3 automation, Lambda triggers, and object storage workflows).&lt;/p&gt;

&lt;p&gt;You could:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generate thumbnails for uploaded images.&lt;/li&gt;
&lt;li&gt;Transcode files into different formats.&lt;/li&gt;
&lt;li&gt;Run data processing jobs on uploaded files.&lt;/li&gt;
&lt;li&gt;Notify other services that a new file is available.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👇 Please share how you treat user-provided data?&lt;/p&gt;

&lt;p&gt;Do you analyse it with metrics? If so, what kinds of insights are you looking to uncover?&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>cloud</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Enforce Module Imports in FSD (using eslint-plugin-import)</title>
      <dc:creator>vavilov2212</dc:creator>
      <pubDate>Mon, 22 Sep 2025 02:21:17 +0000</pubDate>
      <link>https://dev.to/vavilov2212/enforce-module-imports-in-fsd-using-eslint-plugin-import-2d72</link>
      <guid>https://dev.to/vavilov2212/enforce-module-imports-in-fsd-using-eslint-plugin-import-2d72</guid>
      <description>&lt;p&gt;Have you ever refactored a project and realised every file had a &lt;em&gt;different import style&lt;/em&gt;? This article dives deeper into managing code structure by enforcing rules with ESLint.&lt;/p&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;TL;DR&lt;/li&gt;
&lt;li&gt;Motivation&lt;/li&gt;
&lt;li&gt;Unforeseen Consequences&lt;/li&gt;
&lt;li&gt;Enforcing Code Structure&lt;/li&gt;
&lt;li&gt;Customising the Rule&lt;/li&gt;
&lt;li&gt;Future Exploration&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;If you already know what you’re doing, check out the example repo: &lt;a href="https://github.com/vavilov2212/eslint-fsd-example" rel="noopener noreferrer"&gt;eslint-fsd-example&lt;/a&gt;. It follows the FSD (Feature Sliced Design) and&amp;nbsp;serves as a practical showcase.&lt;/p&gt;




&lt;h2&gt;
  
  
  Motivation
&lt;/h2&gt;

&lt;p&gt;As a lead developer, one of my responsibilities is to ensure that the codebase stays coherent and manageable while complexity grows. When new features and fixes are introduced by a team of developers, it’s inevitable that their code looks different. That’s fine — each one brings a unique vision and perspective. Sometimes, the team even adopts those practices at scale.&lt;/p&gt;

&lt;p&gt;But coding style and structure are another story. Everyone has their own vision, and that’s great… until you want to refactor 😑.&lt;/p&gt;

&lt;p&gt;Imagine different parts of your code doing things their own way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&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="s2"&gt;shared/ui/Comment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// baseUrl + direct file&lt;/span&gt;
&lt;span class="k"&gt;import&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="s2"&gt;../../ui/Comment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// relative path&lt;/span&gt;
&lt;span class="k"&gt;import&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="s2"&gt;@/shared/ui&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// alias + index file&lt;/span&gt;
&lt;span class="k"&gt;import&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="s2"&gt;../../ui/index.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// relative path to index&lt;/span&gt;
&lt;span class="c1"&gt;// and so on&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This happens in real projects and leads to a &lt;strong&gt;zoo of approaches&lt;/strong&gt; scattered everywhere. Problems include circular imports, and broken refactors (auto-import often fails to replace paths correctly).&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;del&gt;Un&lt;/del&gt;foreseen Consequences
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://feature-sliced.design/docs/reference/public-api#circular-imports" rel="noopener noreferrer"&gt;Circular imports&lt;/a&gt; often appear when you import from an index file that re-exports the same module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/shared/ui/index.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="o"&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;./Comment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="o"&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;./Like&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="o"&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;./ViewsCounter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// src/shared/ui/Comment&lt;/span&gt;
&lt;span class="k"&gt;import&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;shared/ui&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// &amp;lt;-- 🔥 causes a cycle&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or even without an index file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/shared/ui/Comment&lt;/span&gt;
&lt;span class="k"&gt;import&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;./Like&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// src/shared/ui/Like&lt;/span&gt;
&lt;span class="k"&gt;import&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;./ViewsCounter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// src/shared/ui/ViewsCounter&lt;/span&gt;
&lt;span class="k"&gt;import&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;./Comment&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;💡 This can even lead to &lt;strong&gt;runtime errors that are very hard to debug&lt;/strong&gt; (ask me how i know it).&lt;/p&gt;




&lt;h2&gt;
  
  
  Enforcing Code Structure
&lt;/h2&gt;

&lt;p&gt;FSD conventionally enforces strict import/export principles (&lt;a href="https://feature-sliced.design/docs/reference/public-api" rel="noopener noreferrer"&gt;they’re even described as mandatory in the docs&lt;/a&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Any module may only be imported via its &lt;strong&gt;public API&lt;/strong&gt; (index file) using an &lt;strong&gt;absolute path&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Inside a module, all imports must use &lt;strong&gt;relative paths&lt;/strong&gt; (avoids cycles)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But what stops developers from breaking this rule? Nothing 🤔 — unless you enforce it with ESLint.&lt;/p&gt;

&lt;p&gt;FSD provides a dedicated &lt;a href="https://github.com/feature-sliced/eslint-config" rel="noopener noreferrer"&gt;feature-sliced/eslint-config&lt;/a&gt; with just three rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;public-api&lt;/code&gt; – uses &lt;a href="https://github.com/import-js/eslint-plugin-import" rel="noopener noreferrer"&gt;eslint-plugin-import&lt;/a&gt;, adds glob expression to match patterns in your import statements (internally then converts glob expressions to regexp via &lt;a href="https://github.com/isaacs/minimatch" rel="noopener noreferrer"&gt;&lt;em&gt;minimatch&lt;/em&gt;&lt;/a&gt; )&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;layers-slices&lt;/code&gt; – uses &lt;a href="https://github.com/javierbrea/eslint-plugin-boundaries" rel="noopener noreferrer"&gt;eslint-plugin-boundaries&lt;/a&gt;, analyses relations between the dependent element (your module) and the dependency (what’s being imported) and checks if it’s allowed or not (see &lt;a href="https://feature-sliced.design/docs/reference/layers#import-rule-on-layers" rel="noopener noreferrer"&gt;&lt;em&gt;fsd docs&lt;/em&gt;&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;import-order&lt;/code&gt; – uses &lt;a href="https://github.com/import-js/eslint-plugin-import" rel="noopener noreferrer"&gt;eslint-plugin-import&lt;/a&gt;, sorts and groups imports alphabetically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s take a look at a &lt;a href="https://github.com/vavilov2212/eslint-fsd-example" rel="noopener noreferrer"&gt;Next.js example project&lt;/a&gt; that tries to follows FSD (Feature Sliced Design) methodology. Boilerplate comes from &lt;a href="https://github.com/vercel/next.js/tree/canary/packages/create-next-app" rel="noopener noreferrer"&gt;&lt;code&gt;create-next-app&lt;/code&gt;&lt;/a&gt; using the &lt;a href="https://github.com/vercel/next.js/tree/canary/examples/with-eslint" rel="noopener noreferrer"&gt;&lt;code&gt;with-eslint&lt;/code&gt;&lt;/a&gt; example.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-next-app &lt;span class="se"&gt;\&lt;/span&gt;
eslint-fsd-example &lt;span class="se"&gt;\ &lt;/span&gt;&lt;span class="c"&gt;# project name&lt;/span&gt;
&lt;span class="nt"&gt;-e&lt;/span&gt; https://github.com/vercel/next.js/tree/canary &lt;span class="se"&gt;\ &lt;/span&gt;&lt;span class="c"&gt;# example name or url&lt;/span&gt;
&lt;span class="nt"&gt;--example-path&lt;/span&gt; examples/with-eslint &lt;span class="c"&gt;# if example url has a branch name in it and then path to the example, it has to be specified separately&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Customising the Rule
&lt;/h2&gt;

&lt;p&gt;Project structure (simplified):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tsx&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="nx"&gt;Home&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="nx"&gt;signin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tsx&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="nx"&gt;Signin&lt;/span&gt; &lt;span class="nx"&gt;Page&lt;/span&gt;
&lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;_pages&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="nx"&gt;signin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;views&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;Signin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tsx&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="nx"&gt;Signin&lt;/span&gt; &lt;span class="nx"&gt;main&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="nx"&gt;components&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;               &lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;               &lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="nx"&gt;Signin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;                   &lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;Signin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tsx&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;                   &lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="nx"&gt;Signin&lt;/span&gt; &lt;span class="nx"&gt;actual&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;                   &lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;
&lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;_features&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="nx"&gt;sigin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;types&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="nx"&gt;signinFormValues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="nx"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="nx"&gt;useSigninForm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;_pages&lt;/code&gt; and &lt;code&gt;_features&lt;/code&gt; folders start with underscores to avoid naming conflicts with Next.js reserved folders. Other FSD layers use the same convention for consistency.&lt;/p&gt;

&lt;p&gt;We want to enforce imports only from &lt;strong&gt;public APIs&lt;/strong&gt; (index files). Here’s a simplified ESLint config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FS_LAYERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;entities&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;features&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;widgets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;processes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pages&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shared&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FS_SEGMENTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ui&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;types&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;model&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lib&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;assets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextjsConfigRules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Linter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rules&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;import/no-internal-modules&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="cm"&gt;/**
         * Allow not segments import from slices
         * @example
         * 'entities/user/ui' // Pass
         * 'entities/user' // Also Pass
         */&lt;/span&gt;
        &lt;span class="s2"&gt;`**/*(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;FS_SLICED_LAYERS_REG&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)/!(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;FS_SEGMENTS_REG&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)/*(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;FS_SEGMENTS_REG&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

        &lt;span class="cm"&gt;/**
         * Allow slices with structure grouping
         * @example
         * 'features/auth/form' // Pass
         */&lt;/span&gt;
        &lt;span class="s2"&gt;`**/*(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;FS_SLICED_LAYERS_REG&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)/!(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;FS_SEGMENTS_REG&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)/!(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;FS_SEGMENTS_REG&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;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;blockquote&gt;
&lt;p&gt;💡 For the &lt;code&gt;shared&lt;/code&gt; directory different rules apply since it’s somewhat special.&lt;br&gt;
Look in the source code for details.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Reaching past &lt;code&gt;src/_features/signin/lib/index.ts&lt;/code&gt; is prohibited — exactly what we want ✅.&lt;/p&gt;

&lt;p&gt;Here’s the proof:&lt;br&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%2Fmwbb8wkfn9blazgf4nkm.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%2Fmwbb8wkfn9blazgf4nkm.png" alt="Proof that the rule works" width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The FSD guidelines say you should only import from the slice root. Our team, however, prefers to allow imports from specific segments (like &lt;code&gt;ui&lt;/code&gt;, &lt;code&gt;lib&lt;/code&gt;, etc.).&lt;/p&gt;

&lt;p&gt;To achieve this, we had to adjust &lt;a href="https://github.com/import-js/eslint-plugin-import" rel="noopener noreferrer"&gt;eslint-plugin-import&lt;/a&gt; directly.&lt;/p&gt;

&lt;p&gt;If you want to follow the official FSD recommendations, you can just use the &lt;a href="https://github.com/feature-sliced/eslint-config" rel="noopener noreferrer"&gt;feature-sliced/eslint-config&lt;/a&gt; &lt;code&gt;public-api&lt;/code&gt; rule as is.&lt;/p&gt;




&lt;h2&gt;
  
  
  Future Exploration
&lt;/h2&gt;

&lt;p&gt;While this setup covers the basics of enforcing FSD imports, there are still some &lt;strong&gt;open questions&lt;/strong&gt; worth exploring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://feature-sliced.design/docs/reference/public-api#public-api-for-cross-imports" rel="noopener noreferrer"&gt;&lt;strong&gt;Cross-imports&lt;/strong&gt;&lt;/a&gt; with &lt;code&gt;@x&lt;/code&gt; in FSD – Cross-imports are part of the official FSD methodology. They allow integration between slices or layers in a controlled way. But how do we enforce them properly with ESLint, and what are the best practices for keeping them consistent?&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nextjs.org/docs/app/guides/lazy-loading#nextdynamic" rel="noopener noreferrer"&gt;&lt;strong&gt;Dynamic imports&lt;/strong&gt;&lt;/a&gt; – Sometimes you need to use &lt;code&gt;import()&lt;/code&gt; directly for code-splitting. But what happens when you want to dynamically import something from a &lt;em&gt;public API&lt;/em&gt; instead of a deep file? Is that possible, or do you always end up importing the raw file?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing strategies&lt;/strong&gt; – Should tests respect the same public API import rules, or is it acceptable for them to reach into private modules?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I don’t have all the answers yet (still experimenting 😅), but these are areas I’m actively digging into.&lt;/p&gt;

&lt;p&gt;👉 In a &lt;strong&gt;follow-up post&lt;/strong&gt;, I’ll share experiments and possible solutions for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Making dynamic imports work with public APIs&lt;/li&gt;
&lt;li&gt;Handling test imports without breaking conventions&lt;/li&gt;
&lt;li&gt;Enforcing and scaling cross-imports without losing clarity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So if that sounds interesting — stay tuned, because &lt;strong&gt;Part 2&lt;/strong&gt; is coming!&lt;/p&gt;




&lt;p&gt;👇 Share your experience in the comments — I’d love to hear how you handle these challenges!&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>javascript</category>
      <category>tooling</category>
      <category>frontend</category>
    </item>
    <item>
      <title>🧠 Master Your Prompt: A Practical Process to Improve AI Interactions</title>
      <dc:creator>vavilov2212</dc:creator>
      <pubDate>Fri, 11 Jul 2025 00:36:29 +0000</pubDate>
      <link>https://dev.to/vavilov2212/master-your-prompt-a-practical-process-to-improve-ai-interactions-2jg3</link>
      <guid>https://dev.to/vavilov2212/master-your-prompt-a-practical-process-to-improve-ai-interactions-2jg3</guid>
      <description>&lt;h2&gt;
  
  
  Step-by-Step Guide to Analyze and Enhance Your Prompts:
&lt;/h2&gt;

&lt;p&gt;In today’s AI-driven world, the precision and structure of input prompts directly influence output quality, relevance, and consistency. Effective prompt engineering is a critical skill for developers, data scientists, and AI practitioners aiming to optimize model performance and reduce ambiguity. This article presents a practical, step-by-step process to analyze, refine, and enhance your prompts—helping you unlock the full potential of AI interactions.&lt;/p&gt;

&lt;h2&gt;
  
  
  📄 The Original Prompt
&lt;/h2&gt;

&lt;p&gt;The full prompt in text format → &lt;a href="https://gist.github.com/vavilov2212/7da0e77b682d108ec11478bb17dd752d" rel="noopener noreferrer"&gt;Prompt Refiner&lt;/a&gt; or click the image 👇&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gist.github.com/vavilov2212/7da0e77b682d108ec11478bb17dd752d" rel="noopener noreferrer"&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%2Fzpgyte91rjwm972ztjsq.png" alt="Prompt Preview Image" width="800" height="1036"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🎥 Source
&lt;/h2&gt;

&lt;p&gt;I stumbled upon this prompt structure on &lt;a href="https://www.facebook.com/matthewhitcham/videos/1182955480180921" rel="noopener noreferrer"&gt;Social media&lt;/a&gt; and titled as &lt;em&gt;"The World's best AI methods"&lt;/em&gt; by a content creator who sells prompt libraries.  &lt;/p&gt;




&lt;h2&gt;
  
  
  🌤️ Strong Design (for reference)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Clear, modular 2-phase structure&lt;/li&gt;
&lt;li&gt;Low cognitive load with simple diagnostics&lt;/li&gt;
&lt;li&gt;Deliverables are copy/paste-friendly&lt;/li&gt;
&lt;li&gt;Useful for tooling and collaborative workflows&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  ‼️ Flaws and How to Enhance
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1. ⚡️ Assumes intent is always worth preserving&lt;/strong&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ It prioritizes retaining original intent, even when that intent is vague or misguided.&lt;br&gt;
&lt;em&gt;But in real-world usage, input prompts are often vague, contradictory, or low-quality.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; “Make it cool and viral.” What does that mean? Tone? Format? Medium?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;✅ Add an early clarification step:&lt;br&gt;
“If the original intent is unclear, poorly defined, or counterproductive, rewrite the goal before proceeding.”&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;2. ⚡️ No fallback logic for poor or minimal inputs&lt;/strong&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ You can't go forward if the prompt is unusable.&lt;br&gt;
&lt;em&gt;It should trigger structured fallback behavior, but there's none.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;br&gt;
    - Extremely short prompts (“Make it nice”)&lt;br&gt;
    - Prompts lacking context (“Write an article”)&lt;br&gt;
    - Unstructured lists without instruction&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;✅ Insert a &lt;strong&gt;Phase 0 – Precheck&lt;/strong&gt; step:&lt;br&gt;
“If input prompt lacks purpose, audience, or content type, request clarification before continuing.”&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;3. ⚡️ Doesn’t distinguish prompt types (creative vs. functional)&lt;/strong&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ The same criteria are applied to all prompt types.&lt;br&gt;
&lt;em&gt;Prompts for storytelling or ideation shouldn’t be judged by the same standards as instructional prompts.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; A story prompt might intentionally break clarity or tone norms to achieve voice, metaphor, or ambiguity.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;✅ Add a &lt;strong&gt;Prompt Type Detection&lt;/strong&gt; step:&lt;br&gt;
“If prompt appears to be creative, conversational, or open-ended, adapt evaluation criteria. Skip irrelevant items like accuracy or resource usage.”&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;4. ⚡️ No resolution strategy for conflicting criteria&lt;/strong&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ In practice, criteria can contradict each other:&lt;br&gt;
    • Improving &lt;strong&gt;clarity&lt;/strong&gt; may reduce &lt;strong&gt;brevity&lt;/strong&gt;&lt;br&gt;
    • Strengthening &lt;strong&gt;tone&lt;/strong&gt; may hurt &lt;strong&gt;resource efficiency&lt;/strong&gt;&lt;br&gt;
    • Increasing &lt;strong&gt;specificity&lt;/strong&gt; might restrict &lt;strong&gt;flexibility&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There’s no prioritization&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;✅ Define a default priority order: Clarity → Tone → Brevity → Efficiency&lt;br&gt;
“If two criteria conflict, prioritize the one that maximizes usefulness and comprehension.”&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🛠️ Enhanced Prompt
&lt;/h2&gt;

&lt;p&gt;The full prompt in text format → &lt;a href="https://gist.github.com/vavilov2212/230ef8b963bb5ed5a102b1d5c0966b08" rel="noopener noreferrer"&gt;Prompt Refiner Enhanced&lt;/a&gt; or click the image 👇&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gist.github.com/vavilov2212/230ef8b963bb5ed5a102b1d5c0966b08" rel="noopener noreferrer"&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%2Fxo7m13asu65xc6yicr0d.png" alt="Prompt Preview Image" width="800" height="1092"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 Would You Trust This?
&lt;/h2&gt;

&lt;p&gt;Community Insights Welcome!&lt;/p&gt;

&lt;p&gt;👉 Drop your thoughts below. I’ll reply to every comment.&lt;/p&gt;

</description>
      <category>promptengineering</category>
      <category>ai</category>
      <category>llm</category>
      <category>chatgpt</category>
    </item>
    <item>
      <title>From @import to @use: Migrating Sass the Right Way in a Next.js Project</title>
      <dc:creator>vavilov2212</dc:creator>
      <pubDate>Wed, 02 Jul 2025 02:21:53 +0000</pubDate>
      <link>https://dev.to/vavilov2212/from-import-to-use-migrating-sass-the-right-way-in-a-nextjs-project-5h01</link>
      <guid>https://dev.to/vavilov2212/from-import-to-use-migrating-sass-the-right-way-in-a-nextjs-project-5h01</guid>
      <description>&lt;h2&gt;
  
  
  &lt;strong&gt;TL;DR: Sass Migration in a Nutshell&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Still using &lt;code&gt;@import&lt;/code&gt; in your SCSS? Time to level up. Here’s what I did:&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Replaced&lt;/strong&gt; &lt;code&gt;@import&lt;/code&gt; with &lt;code&gt;@use&lt;/code&gt; 'styles/...' as *&lt;/p&gt;

&lt;p&gt;🛠️ Added &lt;strong&gt;resolving Sass Paths&lt;/strong&gt; for cleaner imports&lt;/p&gt;

&lt;p&gt;🧪 &lt;strong&gt;Verified&lt;/strong&gt; everything using npx sass-migrator — no issues found&lt;/p&gt;

&lt;p&gt;🧹 &lt;strong&gt;Reordered&lt;/strong&gt; CSS declarations above nested rules to remove deprecation warnings&lt;/p&gt;

&lt;p&gt;📁 &lt;strong&gt;Renamed&lt;/strong&gt; confusing css/ folder to styles/ for clarity&lt;/p&gt;

&lt;p&gt;🧘‍♂️ &lt;strong&gt;Result&lt;/strong&gt;: No more terminal spam, cleaner code, and a future-proof SCSS setup&lt;/p&gt;




&lt;p&gt;When working on a growing frontend project using &lt;strong&gt;Next.js&lt;/strong&gt; and &lt;strong&gt;Sass/SCSS&lt;/strong&gt;, it’s easy to miss the warnings piling up in the console — until they become impossible to ignore.&lt;/p&gt;

&lt;p&gt;Recently, I took on the task of cleaning up a Next.js codebase that was riddled with deprecated &lt;code&gt;@import&lt;/code&gt; usage and upcoming breaking changes related to how nested declarations behave. Here’s how I approached it, what I learned, and how you can apply it to your project.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;🚨 Why Migrate from &lt;code&gt;@import&lt;/code&gt;?&lt;/strong&gt;
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 The &lt;code&gt;@import&lt;/code&gt; rule is deprecated and will be removed in future versions of Sass. Use &lt;code&gt;@use&lt;/code&gt; or &lt;code&gt;@forward&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://sass-lang.com/documentation/breaking-changes/import/" rel="noopener noreferrer"&gt;sass-lang.com/documentation/breaking-changes/import/&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You may still see &lt;code&gt;@import&lt;/code&gt; sprinkled throughout your .scss files, and while it works &lt;em&gt;for now&lt;/em&gt;, it comes with hidden costs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Unpredictable global scoping&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No clear module boundaries&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Duplicate style injection&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tons of warnings&lt;/strong&gt; in your terminal and browser console&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In my case, the project had &lt;code&gt;@import&lt;/code&gt; '~css/colors' and similar statements all over .module.scss files. The tilde (~) was working magically, resolving to the src/ folder — although we never explicitly configured that (likely a leftover from legacy webpack behavior).&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;🔁 The &lt;code&gt;@use&lt;/code&gt; Syntax&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The new module system introduced by Sass is smarter, safer, and designed to prevent namespace collisions. Here’s how I migrated:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;✅ Before&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;'~css/colors'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;'~css/vars'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;'~css/mixins'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;✅ After&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="k"&gt;@use&lt;/span&gt; &lt;span class="s1"&gt;'styles/colors'&lt;/span&gt; &lt;span class="nt"&gt;as&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@use&lt;/span&gt; &lt;span class="s1"&gt;'styles/vars'&lt;/span&gt; &lt;span class="nt"&gt;as&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@use&lt;/span&gt; &lt;span class="s1"&gt;'styles/mixins'&lt;/span&gt; &lt;span class="nt"&gt;as&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Along the way, I also renamed the folder from css/ to styles/ — it just makes more sense semantically and reads better for anyone joining the project.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;🧭 Resolving Sass Paths in Next.js&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;To avoid long relative imports like ../../styles/colors, I initially configured the project like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// next.config.mjs&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;path&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;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;sassOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;includePaths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src&lt;/span&gt;&lt;span class="dl"&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;This allowed me to write clean import statements such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="k"&gt;@use&lt;/span&gt; &lt;span class="s1"&gt;'styles/colors'&lt;/span&gt; &lt;span class="nt"&gt;as&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✅ It worked.&lt;/p&gt;

&lt;p&gt;⚠️ But there’s more you should know.&lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;❗includePaths Is a Legacy Pattern&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;While includePaths is still supported by Dart Sass (and by the sassOptions field in Next.js), it’s not ideal for long-term use — especially with modern Sass modules.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 “The includePaths option is not recommended for use with &lt;code&gt;@use&lt;/code&gt;. Instead, prefer using relative or package-based URLs.”&lt;br&gt;
&lt;a href="https://sass-lang.com/documentation/at-rules/use/#url" rel="noopener noreferrer"&gt;sass-lang.com/documentation/at-rules/use/#url&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;🧩 Better Path Resolution Options&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;✅ 1. Use Relative Paths (Recommended)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Instead of relying on global resolution, you can write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="k"&gt;@use&lt;/span&gt; &lt;span class="s1"&gt;'../../styles/colors'&lt;/span&gt; &lt;span class="nt"&gt;as&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It’s a bit more verbose, but it’s:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Explicit ✅&lt;/li&gt;
&lt;li&gt;Native to Sass ✅&lt;/li&gt;
&lt;li&gt;Portable across tools ✅&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;✅ 2. Use Webpack Aliases (Optional, with Caveats)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;If you want clean, short import paths &lt;strong&gt;and&lt;/strong&gt; you’re using Next.js with Webpack, you can configure an alias:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// next.config.mjs&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;path&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;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;webpack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@styles&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/styles&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;config&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;Then in SCSS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="k"&gt;@use&lt;/span&gt; &lt;span class="s1"&gt;'@styles/colors'&lt;/span&gt; &lt;span class="nt"&gt;as&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ Sass doesn’t know what &lt;code&gt;@styles&lt;/code&gt; means unless your tooling resolves it (e.g. via sass-loader in Webpack).&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;✅ 3. Use the SASS_PATH Environment Variable&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Define your base path globally using an environment variable.&lt;/p&gt;

&lt;p&gt;In a .env file:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Then in your SCSS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="k"&gt;@use&lt;/span&gt; &lt;span class="s1"&gt;'styles/colors'&lt;/span&gt; &lt;span class="nt"&gt;as&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works in &lt;strong&gt;Next.js&lt;/strong&gt; and many other Sass toolchains, and is a nice middle ground between global includes and explicit paths.&lt;/p&gt;

&lt;p&gt;✅ Built-in support&lt;/p&gt;

&lt;p&gt;✅ Cleaner imports&lt;/p&gt;

&lt;p&gt;⚠️ Slightly hidden magic&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📚&amp;nbsp;&lt;a href="https://github.com/sass/node-sass#environment-variables" rel="noopener noreferrer"&gt;node-sass on GitHub&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;🛠 Using sass-migrator to Validate Migration&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Sass provides a handy CLI tool to help you migrate - &lt;a href="https://sass-lang.com/documentation/cli/migrator/" rel="noopener noreferrer"&gt;sass-migrator&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here’s what I ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx sass-migrator module &lt;span class="nt"&gt;--migrate-deps&lt;/span&gt; &lt;span class="nt"&gt;-I&lt;/span&gt; ./src ./src/&lt;span class="k"&gt;**&lt;/span&gt;/&lt;span class="k"&gt;*&lt;/span&gt;.scss

&lt;span class="c"&gt;# stdout&lt;/span&gt;
Nothing to migrate!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which meant my manual changes were already valid under the module system — a good sanity check after a global find-and-replace in VSCode.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;🔃 Fixing Declaration Order in Nested Rules&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Another warning that was spamming my console came from how some SCSS blocks had declarations &lt;em&gt;after&lt;/em&gt; nested selectors. Sass is aligning more with CSS behavior here.&lt;/p&gt;

&lt;p&gt;📚 More info here: &lt;a href="https://sass-lang.com/documentation/breaking-changes/mixed-decls/" rel="noopener noreferrer"&gt;https://sass-lang.com/documentation/breaking-changes/mixed-decls/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The fix? Move all property declarations &lt;strong&gt;above&lt;/strong&gt; nested selectors. &lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;⚠️ Deprecated Style&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="nc"&gt;.fee&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;align-items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nt"&gt;Text&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;txt2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="na"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6px&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;h3&gt;
  
  
  &lt;strong&gt;✅ Fixed Style&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="nc"&gt;.fee&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;align-items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="na"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nt"&gt;Text&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;txt2&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;h2&gt;
  
  
  &lt;strong&gt;🧹 Final Thoughts&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This was a small but meaningful refactor that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Removed console spam&lt;/li&gt;
&lt;li&gt;Future-proofed our stylesheets&lt;/li&gt;
&lt;li&gt;Improved module clarity&lt;/li&gt;
&lt;li&gt;Made onboarding easier&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re using Sass in 2025, migrating to &lt;code&gt;@use&lt;/code&gt; isn’t just a nice-to-have If you’re maintaining a similar setup in Next.js. I highly recommend doing this migration sooner than later. It took me a couple of commits.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>sass</category>
      <category>frontend</category>
      <category>webdev</category>
    </item>
    <item>
      <title>🔀 macOS Split-Tunneling VPN Guide: Route Only Specific Subnets</title>
      <dc:creator>vavilov2212</dc:creator>
      <pubDate>Sat, 14 Jun 2025 09:15:56 +0000</pubDate>
      <link>https://dev.to/vavilov2212/routing-only-specific-subnets-through-vpn-split-tunneling-on-macos-2aig</link>
      <guid>https://dev.to/vavilov2212/routing-only-specific-subnets-through-vpn-split-tunneling-on-macos-2aig</guid>
      <description>&lt;p&gt;Many remote developers need to access company-internal services behind a VPN. The standard VPN setup? Connect using &lt;strong&gt;L2TP over IPsec&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But that usually &lt;strong&gt;routes &lt;em&gt;all&lt;/em&gt; your traffic through the VPN&lt;/strong&gt; — from websites to updates and personal apps.&lt;/p&gt;

&lt;p&gt;😬 Not ideal when you only need access to a specific internal subnet.&lt;/p&gt;

&lt;p&gt;Let’s fix that with this &lt;strong&gt;split tunneling&lt;/strong&gt; guide.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 What Is Split Tunneling?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Split tunneling&lt;/strong&gt; means routing &lt;em&gt;only&lt;/em&gt; specific IP ranges (like your company subnet) through the VPN, while everything else uses your normal internet connection.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Internet traffic  ─────────► stays local
Internal subnet   ─────────► goes through VPN

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This results in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⚡ Better internet performance&lt;/li&gt;
&lt;li&gt;🔒 Better privacy&lt;/li&gt;
&lt;li&gt;🎯 More precise access to internal resources&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  ❗ Default VPN Behavior on macOS
&lt;/h2&gt;

&lt;p&gt;If you're using &lt;strong&gt;System Settings &amp;gt; Network&lt;/strong&gt; to connect via L2TP over IPsec:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;macOS &lt;strong&gt;routes all traffic&lt;/strong&gt; through the VPN once connected&lt;/li&gt;
&lt;li&gt;There’s &lt;strong&gt;no tun/ppp interface&lt;/strong&gt; visible in &lt;code&gt;ifconfig&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;No native UI option for split tunneling&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔧 How To Manually Add a Route
&lt;/h2&gt;

&lt;p&gt;Let’s say:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your &lt;strong&gt;internal company subnet&lt;/strong&gt; is &lt;code&gt;&amp;lt;YOUR_SUBNET&amp;gt;&lt;/code&gt; (e.g., &lt;code&gt;192.168.50.0/24&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Your &lt;strong&gt;VPN gateway IP&lt;/strong&gt; is &lt;code&gt;&amp;lt;VPN_GATEWAY_IP&amp;gt;&lt;/code&gt; (e.g., &lt;code&gt;10.8.0.1&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After connecting to VPN:&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="nb"&gt;sudo &lt;/span&gt;route &lt;span class="nt"&gt;-n&lt;/span&gt; add &amp;lt;YOUR_SUBNET&amp;gt; &amp;lt;VPN_GATEWAY_IP&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  🔍 Example:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;route &lt;span class="nt"&gt;-n&lt;/span&gt; add 192.168.50.0/24 10.8.0.1
&lt;span class="c"&gt;# ✅ Expected output:&lt;/span&gt;
&lt;span class="c"&gt;# add net 192.168.50.0: gateway 10.8.0.1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  ⚙️ Automating with &lt;code&gt;/etc/ppp/ip-up&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Let’s automate route setup when PPP-based VPN connects.&lt;/p&gt;

&lt;p&gt;Create a new script:&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="nb"&gt;sudo &lt;/span&gt;nano /etc/ppp/ip-up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste this:&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;#!/bin/sh&lt;/span&gt;
&lt;span class="c"&gt;# /etc/ppp/ip-up is triggered after a successful VPN connection&lt;/span&gt;
&lt;span class="c"&gt;# $5 is the VPN gateway IP passed by macOS&lt;/span&gt;

&lt;span class="nv"&gt;VPN_GATEWAY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;VPN_GATEWAY_IP&amp;gt;"&lt;/span&gt;  &lt;span class="c"&gt;# e.g., 10.8.0.1&lt;/span&gt;
&lt;span class="nv"&gt;TARGET_SUBNET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;YOUR_SUBNET&amp;gt;"&lt;/span&gt;   &lt;span class="c"&gt;# e.g., 192.168.50.0/24&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;5&lt;/span&gt;&lt;span class="k"&gt;:-}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;VPN_GATEWAY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    /sbin/route &lt;span class="nt"&gt;-n&lt;/span&gt; add &lt;span class="nt"&gt;-net&lt;/span&gt; &lt;span class="nv"&gt;$TARGET_SUBNET&lt;/span&gt; &lt;span class="nv"&gt;$5&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/ppp.log 2&amp;gt;&amp;amp;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make it executable:&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="nb"&gt;sudo chmod&lt;/span&gt; +x /etc/ppp/ip-up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;📄 The script logs output to:&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="nb"&gt;sudo cat&lt;/span&gt; /tmp/ppp.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🧼 Optional: Cleaning Up with &lt;code&gt;/etc/ppp/ip-down&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;To remove the route automatically when VPN disconnects:&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="nb"&gt;sudo &lt;/span&gt;nano /etc/ppp/ip-down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="nv"&gt;VPN_GATEWAY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;VPN_GATEWAY_IP&amp;gt;"&lt;/span&gt;  &lt;span class="c"&gt;# e.g., 10.8.0.1&lt;/span&gt;
&lt;span class="nv"&gt;TARGET_SUBNET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;YOUR_SUBNET&amp;gt;"&lt;/span&gt;   &lt;span class="c"&gt;# e.g., 192.168.50.0/24&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;5&lt;/span&gt;&lt;span class="k"&gt;:-}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;VPN_GATEWAY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    /sbin/route &lt;span class="nt"&gt;-n&lt;/span&gt; delete &lt;span class="nt"&gt;-net&lt;/span&gt; &lt;span class="nv"&gt;$TARGET_SUBNET&lt;/span&gt; &lt;span class="nv"&gt;$5&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make it executable:&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="nb"&gt;sudo chmod&lt;/span&gt; +x /etc/ppp/ip-down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  📡 How to Test If the Route Works
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Check the Routing Table
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;netstat &lt;span class="nt"&gt;-nr&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &amp;lt;YOUR_SUBNET_PREFIX&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🧪 Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;netstat &lt;span class="nt"&gt;-nr&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;192.168.50
&lt;span class="c"&gt;# Expected output:&lt;/span&gt;
&lt;span class="c"&gt;# 192.168.50.0/24   10.8.0.1        UGSc           25       0     en0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Try a Ping
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ping &amp;lt;INTERNAL_SERVER_IP&amp;gt;
&lt;span class="c"&gt;# Example:&lt;/span&gt;
&lt;span class="c"&gt;# ping 192.168.50.10&lt;/span&gt;
&lt;span class="c"&gt;# Expected output:&lt;/span&gt;
&lt;span class="c"&gt;# 64 bytes from 192.168.50.10: icmp_seq=0 ttl=64 time=30.1 ms&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Read the Log File
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /tmp/ppp.log
&lt;span class="c"&gt;# Example log:&lt;/span&gt;
&lt;span class="c"&gt;# add net 192.168.50.0: gateway 10.8.0.1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;📝 If there's an error (e.g., gateway not reachable), it will show up here.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧱 Visual: Full Tunnel vs. Split Tunnel
&lt;/h2&gt;

&lt;h3&gt;
  
  
  🔒 Full Tunnel (Default Behavior)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;All traffic
    │
    ▼
[ VPN Gateway ] ─────► [ Company Resources ]
      ▲
      │
[ Your System ]──────► Internet (via VPN)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ⚡ Split Tunnel (Your Configuration)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ Your System ]
   │       │
   │       └───────► Internet (direct)
   │
   └───────► VPN Gateway ───► &amp;lt;YOUR_SUBNET&amp;gt;
                         ▲
                         │
                 [ Company Resources ]

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  ❗ What If VPN Pushes a Default Route?
&lt;/h2&gt;

&lt;p&gt;Some VPN servers force your system to route &lt;em&gt;all&lt;/em&gt; traffic through them.&lt;/p&gt;

&lt;p&gt;To override:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Find Your Default Gateway
&lt;/h3&gt;

&lt;p&gt;Before connecting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;netstat &lt;span class="nt"&gt;-nr&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;default
&lt;span class="c"&gt;# Example output:&lt;/span&gt;
&lt;span class="c"&gt;# default            192.168.1.1        UGSc           86       0     en0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save this value.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. After Connecting to VPN
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;route delete default
&lt;span class="nb"&gt;sudo &lt;/span&gt;route add default &amp;lt;YOUR_LOCAL_GATEWAY&amp;gt;
&lt;span class="c"&gt;# Example&lt;/span&gt;
&lt;span class="c"&gt;# sudo route delete default&lt;/span&gt;
&lt;span class="c"&gt;# sudo route add default 192.168.1.1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ Do this with caution — if you get it wrong, you may temporarily lose internet.&lt;/p&gt;




&lt;h2&gt;
  
  
  ✅ Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🔌 L2TP/IPsec VPN on macOS routes all traffic by default&lt;/li&gt;
&lt;li&gt;🔀 Split tunneling lets you access only what's needed&lt;/li&gt;
&lt;li&gt;🛠️ Automate routes with &lt;code&gt;/etc/ppp/ip-up&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;🧹 Clean up with &lt;code&gt;/etc/ppp/ip-down&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;🧾 Logs are saved to &lt;code&gt;/tmp/ppp.log&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;🧪 Testing ensures everything’s working as expected&lt;/li&gt;
&lt;li&gt;🚧 Optionally override VPN-pushed routes&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔗 Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;man route&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;man pppd&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.apple.com/documentation/networkextension" rel="noopener noreferrer"&gt;Apple VPN Developer Docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>Inject sass global variables from next.config.js - sass env variables</title>
      <dc:creator>vavilov2212</dc:creator>
      <pubDate>Tue, 29 Apr 2025 13:53:40 +0000</pubDate>
      <link>https://dev.to/vavilov2212/inject-sass-global-variables-from-nextconfigjs-sass-env-variables-3nco</link>
      <guid>https://dev.to/vavilov2212/inject-sass-global-variables-from-nextconfigjs-sass-env-variables-3nco</guid>
      <description>&lt;h2&gt;
  
  
  Add global variable to sass compiler
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://github.com/vercel/next.js/tree/main/examples/with-next-sass" rel="noopener noreferrer"&gt;with-next-sass&lt;/a&gt; - This official next.js example demonstrates how to use Next.js' built-in Global Sass/Scss imports  and Component-Level Sass/Scss modules support.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This &lt;a href="https://stackoverflow.com/a/61705701" rel="noopener noreferrer"&gt;stackoverflow answears&lt;/a&gt; helped me.&lt;/p&gt;

&lt;p&gt;Nextjs has built in sapport for sass. To use component isolated .sass or .scss files you need to install &lt;code&gt;sass&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;sass
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To configure sass compiler you can use &lt;code&gt;sassOptions&lt;/code&gt; in &lt;code&gt;next.config.js&lt;/code&gt;. &lt;a href="https://nextjs.org/docs/basic-features/built-in-css-support#customizing-sass-options" rel="noopener noreferrer"&gt;nextjs.org/#customizing-sass-options&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For instance, if you want to load assets from styles like &lt;code&gt;@font-face&lt;/code&gt; src and have different locations on prod and dev environments, you can do the following:&lt;/p&gt;

&lt;p&gt;In next config set &lt;code&gt;assetPrefix&lt;/code&gt; depending on the environment. Then, add &lt;code&gt;sassOptions&lt;/code&gt; variable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// next.config.js&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// when started in development mode `next dev` or `npm run dev`&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isDev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;phase&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;PHASE_DEVELOPMENT_SERVER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// when `next build` or `npm run build` is used&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isProd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;phase&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;PHASE_PRODUCTION_BUILD&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;assetPrefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;isProd&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/prod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sassOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;prependData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`$prefix: "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;assetPrefix&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;span class="c1"&gt;// Other configuration&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="nx"&gt;sassOptions&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;In .scss files now you can use this &lt;code&gt;prefix&lt;/code&gt; variable to prepend import paths.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@font-face&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;'Open-Sans'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url("#{$prefix}fonts/OpenSans-Regular.ttf")&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>sass</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Sentry Dynamic Environment at Runtime on Client with Next.js</title>
      <dc:creator>vavilov2212</dc:creator>
      <pubDate>Sat, 14 Sep 2024 16:45:21 +0000</pubDate>
      <link>https://dev.to/vavilov2212/sentry-environment-on-client-in-nextjs-2opj</link>
      <guid>https://dev.to/vavilov2212/sentry-environment-on-client-in-nextjs-2opj</guid>
      <description>&lt;p&gt;This article explores how to configure the Sentry SDK with environment-specific information in a Next.js application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exposition
&lt;/h2&gt;

&lt;p&gt;Sentry understands and reads the following environment variables, so you don’t have to explicitly set them in your Sentry SDK init payload (refer to &lt;a href="https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/options/#common-options" rel="noopener noreferrer"&gt;&lt;strong&gt;Sentry Docs - Common Config Options&lt;/strong&gt;&lt;/a&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SENTRY_DSN&lt;/li&gt;
&lt;li&gt;SENTRY_ENVIRONMENT&lt;/li&gt;
&lt;li&gt;SENTRY_RELEASE&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Challenge with .env variables:
&lt;/h2&gt;

&lt;p&gt;With Next.js, this only works for server side, because the framework DOES NOT expose .env variables on client for security reasons (refer to &lt;a href="https://nextjs.org/docs/app/building-your-application/configuring/environment-variables#bundling-environment-variables-for-the-browser" rel="noopener noreferrer"&gt;&lt;strong&gt;Next.js Docs - Bundling Environment Variables for the Browser&lt;/strong&gt;&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Sentry even states this in the &lt;a href="https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/options/#environment" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;environment&lt;/code&gt; section of the configuration docs&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In order to distinguish Sentry traces between staging environments on client we need to explicitly set the &lt;code&gt;environment&lt;/code&gt; key in the Sentry SDK init payload to a desired value.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Remember, NEXT_PUBLIC variables are hardcoded into JS bundle at build time&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// sentry.client.config.js&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;

&lt;span class="nx"&gt;Sentry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;

  &lt;span class="cm"&gt;/*
   * The current environment of your application (e.g. "production").
   * Must be explicitly set, as SENTRY_ENVIRONMENT .env variable is not exposed on client
   */&lt;/span&gt;
  &lt;span class="na"&gt;environment&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;NEXT_PUBLIC_SENTRY_ENVIRONMENT&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;As we deploy different staging environments - we need to assign actual names of those environments to our Sentry SDK &lt;code&gt;environment&lt;/code&gt; key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Possible Solutions
&lt;/h2&gt;

&lt;p&gt;Currently there’s no way to natively achieve this in Next.js and people have been struggling with this (check out &lt;a href="https://github.com/getsentry/sentry-javascript/issues/5130" rel="noopener noreferrer"&gt;&lt;strong&gt;Github issue - Init with environment config not working&lt;/strong&gt;&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;The official recommended way by Next.js is stated in their documentation &lt;a href="https://nextjs.org/docs/app/building-your-application/configuring/environment-variables#bundling-environment-variables-for-the-browser" rel="noopener noreferrer"&gt;&lt;strong&gt;Next.js docs - Bundling Environment Variables for the Browser&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you need access to runtime environment values, you'll have to setup your own API to provide them to the client (either on demand or during initialization).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;However, this approach doesn’t work for the Sentry Browser SDK as it's initiated during build time and runs before first components mount (as far as i understand).&lt;/p&gt;

&lt;p&gt;But there are multiple other ways to achieve this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Have multiple builds for each environment - While technically possible, having separate builds for each environment is cumbersome and inefficient and is simply a bad practice (read &lt;a href="https://www.mikemcgarr.com/blog/build-once-deploy-many.html" rel="noopener noreferrer"&gt;&lt;strong&gt;Build Once, Deploy Many&lt;/strong&gt;&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/getsentry/sentry-javascript/issues/9580#issuecomment-2140778308" rel="noopener noreferrer"&gt;&lt;strong&gt;Init Client Sentry SDK in Layout Component&lt;/strong&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Point the &lt;code&gt;tunnel&lt;/code&gt; option of Sentry SDK to a proxy and set init values there (found in comments of the discussion from previous entry, but yet to be explored).&lt;/li&gt;
&lt;li&gt;Replace “inlined” &lt;code&gt;NEXT_PUBLIC&lt;/code&gt; variables values before stating the application - this is what i ended up utilising in commercial production&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recommended Method (Workaround)
&lt;/h2&gt;

&lt;p&gt;The best practice so far is to actually run through JS bundle files right before starting the application and manually replacing values of &lt;code&gt;NEXT_PUBLIC&lt;/code&gt; variables “inlined” in the code like described in multiple sources already:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/itsrennyman/manage-nextpublic-environment-variables-at-runtime-with-docker-53dl"&gt;&lt;strong&gt;Manage NEXT_PUBLIC Environment Variables at Runtime with Docker&lt;/strong&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://phase.dev/blog/nextjs-public-runtime-variables/" rel="noopener noreferrer"&gt;&lt;strong&gt;Updating public Next.js environment variables without rebuilds&lt;/strong&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/vercel/next.js/discussions/44628#discussioncomment-7632258" rel="noopener noreferrer"&gt;&lt;strong&gt;Before Starting the Server, We Change All NEXT_PUBLIC values&lt;/strong&gt;&lt;/a&gt; &lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;p&gt;Set &lt;code&gt;NEXT_PUBLIC_SENTRY_ENVIRONMENT&lt;/code&gt; .env variable to any value you like as placeholder for future replacement. Make sure it’s unique, since that’s whats going to be searched and replaced.&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&lt;/span&gt;

&lt;span class="nv"&gt;NEXT_PUBLIC_SENTRY_ENVIRONMENT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;NEXT_PUBLIC_SENTRY_ENVIRONMENT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Search and Replace
&lt;/h3&gt;

&lt;p&gt;Add shell script file to the root of your sources, it executes when running the container. It replaces the placeholder value you've set in previous step with the name of your actual staging environment before starting the application.&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;# replace_public_env_vars_and_start_server.sh&lt;/span&gt;

&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;

&lt;span class="sb"&gt;`&lt;/span&gt;find &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-type&lt;/span&gt; f  &lt;span class="nt"&gt;-exec&lt;/span&gt; &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"s/NEXT_PUBLIC_SENTRY_ENVIRONMENT/&lt;/span&gt;&lt;span class="nv"&gt;$SENTRY_ENVIRONMENT&lt;/span&gt;&lt;span class="s2"&gt;/g"&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt; +&lt;span class="sb"&gt;`&lt;/span&gt;

&lt;span class="c"&gt;# server.js is created by next build from the standalone output&lt;/span&gt;
&lt;span class="c"&gt;# https://nextjs.org/docs/pages/api-reference/next-config-js/output&lt;/span&gt;
&lt;span class="c"&gt;# CMD ["node", "server.js"]&lt;/span&gt;
node server.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Be aware, that in order to successfully run, the script requires write permissions to files and the directory they are in - it must be executed under appropriate role on wherever you're running it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build and Run
&lt;/h3&gt;

&lt;p&gt;After building the image - simply point to shell script from the previous step.&lt;/p&gt;

&lt;p&gt;I use the &lt;a href="https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile" rel="noopener noreferrer"&gt;&lt;strong&gt;official Next.js "with-docker" example&lt;/strong&gt;&lt;/a&gt; file to build and run my applications - the following snippet is the bottom portion of this file with some changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Dockerfile&lt;/span&gt;
...

# Make sure it can be executed
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"chmod"&lt;/span&gt;, &lt;span class="s2"&gt;"+x"&lt;/span&gt;, &lt;span class="s2"&gt;"/app/replace_public_env_vars_and_start_server.sh"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; [ "/app/replace_public_env_vars_and_start_server.sh" ]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Result
&lt;/h3&gt;

&lt;p&gt;In the browser sentry environment is set to different value on each staging server (in this instance - &lt;code&gt;tst&lt;/code&gt;)&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F85cs14a9ysfu85jq4p51.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F85cs14a9ysfu85jq4p51.png" alt="Image description" width="800" height="613"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion:
&lt;/h2&gt;

&lt;p&gt;The "replace inlined variables" method offers a practical workaround for achieving environment differentiation in your application, but remember, that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This method involves additional build complexity.&lt;/li&gt;
&lt;li&gt;It's a workaround, not an officially supported solution by Next.js.&lt;/li&gt;
&lt;li&gt;It may not work if your deploy environments have limitations on executing scripts or assigning roles under which files are written and/or executed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Please, let me know in the comments if you're using some other solution to this or have any insight on how to make this one better! 😊 Thanks for reading!&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Windows Installation won't work due to 'Unexpected 0x00005 Error' - How to Fix</title>
      <dc:creator>vavilov2212</dc:creator>
      <pubDate>Sun, 04 Aug 2024 13:25:05 +0000</pubDate>
      <link>https://dev.to/vavilov2212/windows-installation-wont-work-due-to-unexpected-0x00005-error-how-to-fix-406m</link>
      <guid>https://dev.to/vavilov2212/windows-installation-wont-work-due-to-unexpected-0x00005-error-how-to-fix-406m</guid>
      <description>&lt;h2&gt;
  
  
  Introduction to the problem
&lt;/h2&gt;

&lt;p&gt;When attempting to install windows 10/11 (I tried several distributions including official images) i encountered error “Unexpected 0x00005 error, verify source is available”. Possible causes are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;faulty memory (storage or RAM)&lt;/li&gt;
&lt;li&gt;usb installation source filesystem - during installation some fíes may be created that are larger than 4gb - it’s better to use NTFS instead of FAT32&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There also might be indication that something’s off, when before the installation begins, selecting the unallocated space on the target storage drive triggers &lt;code&gt;Setup was unable to create a new system partition or locate an existing one&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 If you’re flashing the USD drive from windows - make sure you run Rufus or any other program as Administrator - it’s absolutely necessary.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In my case, while installing the Tiny11 build of Windows, the installation failed at 33%. I figured this might be because some files generated during the process were larger than 4GB. On my Mac, I could only format the thumb drive to FAT32 since NTFS is not supported. I stumbled upon another solution: since the storage drive of the target machine is formatted with NTFS, we can boot from it using one of the partitions as the installation source.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;p&gt;Following guide in &lt;a href="https://answers.microsoft.com/en-us/windows/forum/all/we-couldnt-create-a-new-partition-or-locate-an/acdb1dae-01d9-44f7-846c-433e5addcfc2" rel="noopener noreferrer"&gt;this Microsoft support unswear&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/o4ilMAAk1Q8"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;We shall be using &lt;code&gt;diskpart&lt;/code&gt; command line utility. When in boot screen of windows installer press fn+F10 to open the terminal. First format the disk, which you will use to install windows&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;diskpart
list disk
# list volume
# list partition
select disk 0 # from the output of the command above
clean
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If there’s any sort of issues with cleaning the disk, we can check it for corrupt memory slots and fix them. Exit the diskpart by typing exit, then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;chkdsk /f /r
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another way is to use System File Checker&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;em&gt;Side note&lt;/em&gt;&lt;br&gt;
There might be issues involved, that are out of the scope of this article, like it might be defective CPU or RAM, specific BIOS settings or just anything else really. While researching this issue there were many discussions that resolved in faulty hardware - so be aware.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Next, we need to create partition on the target storage drive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;diskpart
list disk
select disk 0 # from the output of the command above
create partition primary size=5000 # without the size argument it'll take all unallocated space
# 5gb is enough for the tiny11 build, official images require more space
format fs=ntfs quick # quick is really quicker
assign # to choose the letter must be followed by letter=H
active
list partition # check that partition exists
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 On newer machines the disk is required to have GPT style partition in order for the installation to start, but on older machines it has to be MBR and setting it active is crucial in this case. To clarify what your case might be you need to conduct your own research.&lt;/p&gt;


&lt;pre class="highlight plaintext"&gt;&lt;code&gt;diskpart
select disk 0
convert GPT
&lt;/code&gt;&lt;/pre&gt;

&lt;/blockquote&gt;

&lt;p&gt;Next, &lt;a href="https://stackoverflow.com/questions/7170683/copy-all-files-and-folders-from-one-drive-to-another-drive-using-dos-command-pr" rel="noopener noreferrer"&gt;copy your installation media contents&lt;/a&gt; to this new partition we’ve just created&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;xcopy c: d: /h /i /c /k /e /r /y
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart the machine and boot from storage device - unplug the USB - it’ll assure the installation screen actually comes from the internal drive.&lt;/p&gt;

&lt;p&gt;Following these steps should help resolve the "Unexpected 0x00005 error".&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Tips
&lt;/h2&gt;

&lt;p&gt;In BIOS, configure boot options:&lt;br&gt;
• Disable CSM and use legacy boot if needed.&lt;br&gt;
• For Tiny11 builds, disable TPM; for official Windows 11, enable TPM (since it requires it)&lt;br&gt;
• Consider disabling virtualization settings (e.g., Intel Virtualization).&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Please add your tips and solutions in the comments to help others who are struggling to resolve these installation errors!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tiny11</category>
      <category>windows</category>
      <category>partition</category>
      <category>errors</category>
    </item>
  </channel>
</rss>
