<?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>Forem: Lewis kori</title>
    <description>The latest articles on Forem by Lewis kori (@lewiskori).</description>
    <link>https://forem.com/lewiskori</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F195365%2F59dd5683-c113-4455-bff5-cf91fe010628.jpg</url>
      <title>Forem: Lewis kori</title>
      <link>https://forem.com/lewiskori</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/lewiskori"/>
    <language>en</language>
    <item>
      <title>Deploying a Next.js Monorepo to Cloudflare Workers: Lessons from the Trenches</title>
      <dc:creator>Lewis kori</dc:creator>
      <pubDate>Tue, 07 Apr 2026 20:11:18 +0000</pubDate>
      <link>https://forem.com/lewiskori/deploying-a-nextjs-monorepo-to-cloudflare-workers-lessons-from-the-trenches-1ok8</link>
      <guid>https://forem.com/lewiskori/deploying-a-nextjs-monorepo-to-cloudflare-workers-lessons-from-the-trenches-1ok8</guid>
      <description>&lt;p&gt;Earlier this year, I migrated my &lt;a href="https://lewiskori.com/blog/from-gridsome-to-astro-rebuilding-my-personal-site-for-the-next-phase/" rel="noopener noreferrer"&gt;personal site from Netlify to Cloudflare Pages&lt;/a&gt;. The experience was smooth enough to get me thinking about what Cloudflare could do for a more demanding workload.&lt;/p&gt;

&lt;p&gt;A real production monorepo. Multiple Next.js apps. Shared packages. Real users.&lt;/p&gt;

&lt;p&gt;A few months later, that project is running entirely on Cloudflare Workers.&lt;/p&gt;

&lt;p&gt;This post is the story of how we got there.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;The monorepo contains three Next.js 16 apps and a set of shared packages, all managed with &lt;strong&gt;Nx 22&lt;/strong&gt; and &lt;strong&gt;pnpm workspaces&lt;/strong&gt;. Shared code, such as components, utilities, types, hooks, and config, lives in &lt;code&gt;packages/&lt;/code&gt; and is consumed by each app via TypeScript path aliases.&lt;/p&gt;

&lt;p&gt;The apps range from a lightweight customer-facing site to a heavier dashboard with auth middleware, i18n, and Sentry.&lt;/p&gt;

&lt;p&gt;We were deployed on &lt;strong&gt;Firebase Hosting&lt;/strong&gt; with Cloud Functions and a custom GitHub Actions pipeline.&lt;/p&gt;

&lt;p&gt;It worked.&lt;br&gt;
Until it didn’t.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why We Left Firebase
&lt;/h2&gt;

&lt;p&gt;This was not a single decision. It was a slow accumulation of friction.&lt;/p&gt;
&lt;h3&gt;
  
  
  The CI/CD pipeline became a liability
&lt;/h3&gt;

&lt;p&gt;We built a fairly complex GitHub Actions workflow to build and deploy each app independently of the monorepo. Over time, it became fragile. Deploys were flaky. Build ordering introduced hidden dependencies. Silent failures made debugging painful.&lt;/p&gt;

&lt;p&gt;At some point, the pipeline stopped feeling like infrastructure and started feeling like something we had to constantly babysit.&lt;/p&gt;

&lt;p&gt;And it was quietly burning through our GitHub Actions minutes.&lt;/p&gt;
&lt;h3&gt;
  
  
  Firebase's Next.js support had not kept up
&lt;/h3&gt;

&lt;p&gt;Firebase Hosting’s native Next.js integration works reasonably well for older versions of the framework, but &lt;a href="https://firebase.google.com/docs/hosting/frameworks/nextjs" rel="noopener noreferrer"&gt;support for newer App Router features, React Server Components, and server actions is limited&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Every time we wanted to use something up-to-date, we had to check whether Firebase could handle it.&lt;/p&gt;

&lt;p&gt;Usually, the answer was &lt;em&gt;sort of, with caveats&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;That is not a position you want to be in.&lt;/p&gt;
&lt;h4&gt;
  
  
  Cost unpredictability
&lt;/h4&gt;

&lt;p&gt;Firebase pricing scales with function invocations, bandwidth, and compute. It is predictable at low traffic, but becomes harder to reason about as you grow.&lt;/p&gt;

&lt;p&gt;For SSR workloads, it adds up faster than expected.&lt;/p&gt;

&lt;p&gt;When I rebuilt &lt;a href="https://lewiskori.com/" rel="noopener noreferrer"&gt;my personal site&lt;/a&gt; on &lt;strong&gt;Cloudflare Pages&lt;/strong&gt;, I got a glimpse of a simpler model.&lt;/p&gt;

&lt;p&gt;Git-integrated builds.&lt;br&gt;
Instant global propagation.&lt;br&gt;
No operational overhead.&lt;/p&gt;

&lt;p&gt;It felt calm.&lt;/p&gt;

&lt;p&gt;I wanted that for production.&lt;/p&gt;

&lt;p&gt;The real question was: could Cloudflare Workers handle a full SSR monorepo?&lt;/p&gt;


&lt;h2&gt;
  
  
  Enter @opennextjs/cloudflare
&lt;/h2&gt;

&lt;p&gt;The piece that makes Next.js on Cloudflare Workers possible is &lt;a href="https://opennext.js.org/cloudflare" rel="noopener noreferrer"&gt;&lt;code&gt;@opennextjs/cloudflare&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It takes a Next.js build output and adapts it to run as a Cloudflare Worker.&lt;/p&gt;

&lt;p&gt;The config is minimal:&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;// open-next.config.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;defineCloudflareConfig&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;@opennextjs/cloudflare&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="nf"&gt;defineCloudflareConfig&lt;/span&gt;&lt;span class="p"&gt;({});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most of the real configuration lives in &lt;code&gt;wrangler.jsonc&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node_modules/wrangler/config-schema.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".open-next/worker.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compatibility_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-04-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compatibility_flags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"nodejs_compat"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"global_fetch_strictly_public"&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;"assets"&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;"directory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".open-next/assets"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"binding"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ASSETS"&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;"services"&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;"binding"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"WORKER_SELF_REFERENCE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-app"&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;"limits"&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;"cpu_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30000&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;"vars"&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;"NEXT_BASE_URL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.example.com"&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;Deploying from the monorepo root looks like 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="nb"&gt;cd &lt;/span&gt;apps/my-app &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx opennextjs-cloudflare build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx opennextjs-cloudflare deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We wrapped this in a &lt;code&gt;Makefile&lt;/code&gt;, so the commands stay short and consistent across all apps.&lt;/p&gt;




&lt;h2&gt;
  
  
  Protecting Preview Deployments with Cloudflare Access
&lt;/h2&gt;

&lt;p&gt;One of the easiest wins in this migration was using &lt;strong&gt;&lt;a href="https://www.cloudflare.com/products/zero-trust/access/" rel="noopener noreferrer"&gt;Cloudflare Zero Trust Access&lt;/a&gt;&lt;/strong&gt; to protect non-production environments.&lt;/p&gt;

&lt;p&gt;Previously, staging URLs were open to anyone who found them.&lt;/p&gt;

&lt;p&gt;Not a disaster.&lt;br&gt;
But not ideal.&lt;/p&gt;

&lt;p&gt;With Cloudflare Access, you can gate any deployment behind identity. Email OTP, Google, GitHub, or any OIDC-compatible provider. No code changes required.&lt;/p&gt;

&lt;p&gt;It sits in front of your Worker at the network level.&lt;/p&gt;

&lt;p&gt;For us, staging environments now require authentication. Everyone else hits a login wall.&lt;/p&gt;

&lt;p&gt;Setup took about ten minutes.&lt;/p&gt;

&lt;p&gt;This is one of those features you do not realise you needed until you have it.&lt;/p&gt;

&lt;p&gt;If you have not looked at Zero Trust Access for protecting internal tools or preview environments, it is worth a look. The free plan covers up to 50 users.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Gotchas Nobody Warns You About
&lt;/h2&gt;

&lt;p&gt;Every migration has sharp edges. These are the ones that cost us the most time.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. &lt;code&gt;compatibility_date&lt;/code&gt; and &lt;code&gt;process.env&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;We centralise environment variable handling through a Zod schema that calls &lt;code&gt;envSchema.parse(process.env)&lt;/code&gt; at startup.&lt;/p&gt;

&lt;p&gt;On Cloudflare Workers, variables set in &lt;code&gt;wrangler.jsonc&lt;/code&gt; or through the CLI do not automatically appear in &lt;code&gt;process.env&lt;/code&gt; unless your &lt;code&gt;compatibility_date&lt;/code&gt; is &lt;code&gt;2025-04-01&lt;/code&gt; or later.&lt;/p&gt;

&lt;p&gt;Before that date, the &lt;code&gt;nodejs_compat_populate_process_env&lt;/code&gt; flag is not auto-enabled, and &lt;code&gt;process.env&lt;/code&gt; is effectively empty at runtime. Your app boots, Zod parses an empty object, and everything breaks in a way that is not immediately obvious.&lt;/p&gt;

&lt;p&gt;Set your &lt;code&gt;compatibility_date&lt;/code&gt; to &lt;code&gt;2025-04-01&lt;/code&gt; or higher from the start.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. No &lt;code&gt;export const runtime = 'edge'&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;If you have &lt;code&gt;export const runtime = 'edge'&lt;/code&gt; in any route files, remove it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@opennextjs/cloudflare&lt;/code&gt; does not support the edge runtime directive. The adapter handles the runtime itself.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Middleware cannot use &lt;code&gt;cookies()&lt;/code&gt; from &lt;code&gt;next/headers&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;cookies()&lt;/code&gt; API from &lt;code&gt;next/headers&lt;/code&gt; is Node.js only. If you are using it in &lt;code&gt;middleware.ts&lt;/code&gt; for auth or session handling, it will not work on Workers.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;req.cookies&lt;/code&gt; from &lt;code&gt;NextRequest&lt;/code&gt; instead:&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;// Works on Workers&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;middleware&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&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;cookies&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;access_token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Node.js only. Won't work in Workers middleware&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;cookies&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;next/headers&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 is easy to miss because &lt;code&gt;next dev&lt;/code&gt; runs fine locally. The break only surfaces when you run the actual Workers preview.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The 3 MiB free plan limit
&lt;/h3&gt;

&lt;p&gt;Cloudflare’s free plan has a 3 MiB gzip limit per worker. For simple apps, this is fine. For anything with heavy dependencies such as Sentry server-side, complex auth libraries, or i18n, you will likely hit it.&lt;/p&gt;

&lt;p&gt;The paid plan at $5 per month raises the limit to 10 MiB.&lt;/p&gt;

&lt;p&gt;For apps close to the boundary, audit your server-side bundle. For us, Sentry server instrumentation was the biggest contributor. We stripped it from the lighter apps and kept only client-side Sentry there. The heavier dashboard stays on the paid plan.&lt;/p&gt;




&lt;h2&gt;
  
  
  Environment Variables and Secrets
&lt;/h2&gt;

&lt;p&gt;Cloudflare has a clear &lt;a href="https://developers.cloudflare.com/workers/configuration/environment-variables/" rel="noopener noreferrer"&gt;three-tier model for environment variables&lt;/a&gt;, and it is worth understanding before you start:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;How to set&lt;/th&gt;
&lt;th&gt;When available&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NEXT_PUBLIC_*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.env&lt;/code&gt; file&lt;/td&gt;
&lt;td&gt;Build time. Inlined into client bundles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regular vars&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;vars&lt;/code&gt; block in &lt;code&gt;wrangler.jsonc&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Runtime. Accessible via &lt;code&gt;process.env&lt;/code&gt; on the server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secrets&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;wrangler secret put&lt;/code&gt; CLI&lt;/td&gt;
&lt;td&gt;Runtime. Encrypted, accessible via &lt;code&gt;process.env&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local dev vars&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.dev.vars&lt;/code&gt; file (gitignored)&lt;/td&gt;
&lt;td&gt;Local development only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For local development, create a &lt;code&gt;.dev.vars&lt;/code&gt; file in each app directory:&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;# apps/my-app/.dev.vars&lt;/span&gt;
&lt;span class="nv"&gt;NEXTJS_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;development
&lt;span class="nv"&gt;NEXT_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://staging.api.example.com
&lt;span class="nv"&gt;APP_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-dev-key-here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wrangler reads this automatically during &lt;code&gt;wrangler dev&lt;/code&gt; and &lt;code&gt;wrangler preview&lt;/code&gt;. Add &lt;code&gt;.dev.vars*&lt;/code&gt; to your &lt;code&gt;.gitignore&lt;/code&gt; so staging variants are covered too.&lt;/p&gt;

&lt;p&gt;Secrets for production:&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;cd &lt;/span&gt;apps/my-app
npx wrangler secret put APP_API_KEY
npx wrangler secret put APP_API_KEY &lt;span class="nt"&gt;--env&lt;/span&gt; staging
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One thing we had to fix during the migration was our env schema. It had hardcoded &lt;code&gt;.default()&lt;/code&gt; values for several secrets, leftovers from earlier development. We removed those and made optional secrets actually optional in Zod, with runtime guards in the server actions that use them:&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;someAction&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;apiKey&lt;/span&gt; &lt;span class="o"&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;APP_API_KEY&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;apiKey&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&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;APP_API_KEY is not configured&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="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Setting Up Staging Environments
&lt;/h2&gt;

&lt;p&gt;This part tripped us up more than expected.&lt;/p&gt;

&lt;p&gt;Cloudflare Workers Builds, their git-integrated CI/CD, creates preview URLs for non-production branches. The catch is that preview URLs share the same environment variables as production. There is no way to override them per preview.&lt;/p&gt;

&lt;p&gt;So if your staging environment points to a different backend, preview URLs are not enough.&lt;/p&gt;

&lt;p&gt;The solution is &lt;a href="https://developers.cloudflare.com/workers/wrangler/environments/" rel="noopener noreferrer"&gt;wrangler environments&lt;/a&gt;. Add an &lt;code&gt;env.staging&lt;/code&gt; block to your &lt;code&gt;wrangler.jsonc&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"vars"&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;"NEXT_BASE_URL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.example.com"&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;"env"&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;"staging"&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;"vars"&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;"NEXT_BASE_URL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://staging.api.example.com"&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;"services"&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;"binding"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"WORKER_SELF_REFERENCE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-app-staging"&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;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 creates a separate &lt;code&gt;my-app-staging&lt;/code&gt; worker.&lt;/p&gt;

&lt;p&gt;Important: &lt;code&gt;vars&lt;/code&gt; and &lt;code&gt;services&lt;/code&gt; are non-inheritable keys in Wrangler. They do not cascade from the top-level config into your environments. You must fully specify them in each &lt;code&gt;env&lt;/code&gt; block, or the staging worker starts with empty vars.&lt;/p&gt;

&lt;p&gt;Deploying to staging:&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;cd &lt;/span&gt;apps/my-app &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx opennextjs-cloudflare build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx wrangler deploy &lt;span class="nt"&gt;--env&lt;/span&gt; staging
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For automated staging builds via the Cloudflare dashboard, connect the same repo to both the production and staging workers, with different deploy commands:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Production worker&lt;/strong&gt;: &lt;code&gt;npx wrangler deploy&lt;/code&gt;, watching your &lt;code&gt;main&lt;/code&gt; branch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Staging worker&lt;/strong&gt;: &lt;code&gt;npx wrangler deploy --env staging&lt;/code&gt;, watching your staging branch&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In monorepo setups, also set the &lt;strong&gt;root directory&lt;/strong&gt; to the app’s folder and configure build watch paths to include &lt;code&gt;packages/**&lt;/code&gt; so builds only trigger when relevant files change.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cost Breakdown
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Resource&lt;/th&gt;
&lt;th&gt;Monthly cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Small apps on free plan&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dashboard app on Workers Paid&lt;/td&gt;
&lt;td&gt;$5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom domains via Cloudflare DNS&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$5&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The free plan covers 100,000 requests per day, which is plenty for most apps. The paid plan gives you 10 million requests per month and the 10 MiB bundle limit the heavier dashboard needed.&lt;/p&gt;

&lt;p&gt;For comparison, Vercel teams start at $20 per user per month. Firebase Cloud Functions pricing scales in ways that are harder to predict. At $5 per month for global edge delivery across all our apps, we are satisfied with where we landed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Was It Worth It?
&lt;/h2&gt;

&lt;p&gt;Yes. Unreservedly.&lt;/p&gt;

&lt;p&gt;The deployment pipeline went from a custom, increasingly brittle GitHub Actions setup that chewed through CI minutes to a first-class, git-integrated workflow that just works.&lt;/p&gt;

&lt;p&gt;Push to &lt;code&gt;main&lt;/code&gt;, and the production worker updates. No YAML archaeology required.&lt;/p&gt;

&lt;p&gt;Firebase’s Next.js limitations are gone. We can use anything current in Next.js 16 without checking whether the hosting layer can keep up.&lt;/p&gt;

&lt;p&gt;The performance is genuinely better. Running at the edge in 300+ locations means users in Nairobi, Paris, and São Paulo all get fast responses, not just the ones nearest to a data centre we picked.&lt;/p&gt;

&lt;p&gt;And the Cloudflare ecosystem is increasingly coherent. Workers, Pages, Access, DNS, R2. They work together in a way that makes the platform feel like an actual platform rather than a collection of services.&lt;/p&gt;

&lt;p&gt;After running my personal site there for several months, and now production workloads, I have become a genuine believer.&lt;/p&gt;

&lt;p&gt;If you are running a Next.js monorepo and your Firebase or Vercel setup is starting to feel like a liability, Cloudflare Workers is worth a serious look.&lt;/p&gt;




&lt;h2&gt;
  
  
  What We’re Looking Forward To: The Next.js Adapter API
&lt;/h2&gt;

&lt;p&gt;One thing I am genuinely excited about as a next step is the official &lt;a href="https://nextjs.org/docs/app/api-reference/config/next-config-js/adapterPath" rel="noopener noreferrer"&gt;Next.js Adapter API&lt;/a&gt;, which shipped as stable in Next.js 16.2.&lt;/p&gt;

&lt;p&gt;For a long time, deploying Next.js anywhere other than Vercel meant reverse-engineering the build output. &lt;code&gt;@opennextjs/cloudflare&lt;/code&gt; was essentially a sophisticated workaround. It worked, but it was always playing catch-up with new framework features.&lt;/p&gt;

&lt;p&gt;The Adapter API changes that.&lt;/p&gt;

&lt;p&gt;It gives every platform the same contract: a typed, versioned description of the application, including routes, prerenders, static assets, runtime targets, and caching rules, that an adapter can consume and map onto its own infrastructure.&lt;/p&gt;

&lt;p&gt;Crucially, Vercel’s own adapter is built on this same public API. No private hooks. No undocumented behaviour.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://nextjs.org/blog/nextjs-across-platforms" rel="noopener noreferrer"&gt;Next.js Across Platforms blog post&lt;/a&gt; is worth reading for the broader context.&lt;/p&gt;

&lt;p&gt;For Cloudflare specifically, a verified adapter built on this API is already in active development. Once it ships, the &lt;code&gt;@opennextjs/cloudflare&lt;/code&gt; integration will sit on a stable, tested foundation that evolves with Next.js rather than chasing it.&lt;/p&gt;

&lt;p&gt;For developers, that means first-class Next.js features such as Partial Prerendering, Cache Components, and on-demand revalidation should eventually work consistently on Cloudflare Workers, with upstream documentation and testing.&lt;/p&gt;

&lt;p&gt;We are already on the platform. When the official adapter lands, migrating to it should be straightforward.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>nextjs</category>
      <category>programming</category>
    </item>
    <item>
      <title>AI Is Not Your Intern. And That’s the Problem.</title>
      <dc:creator>Lewis kori</dc:creator>
      <pubDate>Mon, 30 Mar 2026 15:02:23 +0000</pubDate>
      <link>https://forem.com/lewiskori/ai-is-not-your-intern-and-thats-the-problem-2ec9</link>
      <guid>https://forem.com/lewiskori/ai-is-not-your-intern-and-thats-the-problem-2ec9</guid>
      <description>&lt;p&gt;When I first started using coding agents seriously, I made a familiar mistake.&lt;/p&gt;

&lt;p&gt;I treated the model as the main thing.&lt;/p&gt;

&lt;p&gt;I wrote better prompts. Then longer ones. Then, more carefully structured ones.&lt;/p&gt;

&lt;p&gt;Sometimes it worked beautifully.&lt;br&gt;&lt;br&gt;
Sometimes it drifted.&lt;/p&gt;

&lt;p&gt;Same repo. Same goal. Different result.&lt;/p&gt;

&lt;p&gt;That inconsistency bothered me, because it revealed something deeper:&lt;/p&gt;

&lt;p&gt;The bottleneck was not raw intelligence.&lt;br&gt;&lt;br&gt;
It was context.&lt;/p&gt;



&lt;p&gt;In &lt;a href="https://lewiskori.com/blog/ai-is-not-replacing-you-its-reshaping-how-you-think/" rel="noopener noreferrer"&gt;AI Is Not Replacing You. It’s Reshaping How You Think&lt;/a&gt;, I wrote about how AI shifts the bottleneck upward. When generation becomes cheap, the differentiator becomes judgment, constraints, and systems thinking.&lt;/p&gt;

&lt;p&gt;That same pattern shows up in agentic engineering.&lt;/p&gt;

&lt;p&gt;The real leverage does not come from asking a model to do more.&lt;/p&gt;

&lt;p&gt;It comes from designing a better system around the model.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Mistake I Kept Making
&lt;/h2&gt;

&lt;p&gt;Early on, I was treating the agent like a very smart freelancer with amnesia.&lt;/p&gt;

&lt;p&gt;I would drop it into a task, explain the problem, and expect one well-written instruction block to carry the entire interaction.&lt;/p&gt;

&lt;p&gt;Sometimes it did.&lt;/p&gt;

&lt;p&gt;But as soon as the work became real, things broke down.&lt;/p&gt;

&lt;p&gt;Design fidelity mattered.&lt;br&gt;&lt;br&gt;
SEO mattered.&lt;br&gt;&lt;br&gt;
Content structure mattered.&lt;br&gt;&lt;br&gt;
Localisation mattered.&lt;br&gt;&lt;br&gt;
Existing conventions mattered.&lt;br&gt;&lt;br&gt;
Small decisions from earlier in the build mattered.&lt;/p&gt;

&lt;p&gt;One prompt was never going to carry all of that.&lt;/p&gt;

&lt;p&gt;That’s when it clicked:&lt;/p&gt;

&lt;p&gt;Agentic engineering is not prompting.&lt;br&gt;&lt;br&gt;
It’s environment design.&lt;/p&gt;



&lt;p&gt;On a recent workflow, I wasn’t asking an agent to “build a website.”&lt;/p&gt;

&lt;p&gt;I was asking it to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;work inside a multilingual Astro codebase
&lt;/li&gt;
&lt;li&gt;preserve structured SEO
&lt;/li&gt;
&lt;li&gt;keep markdown content editable
&lt;/li&gt;
&lt;li&gt;support Arabic RTL layouts
&lt;/li&gt;
&lt;li&gt;respect project-specific rules already established
&lt;/li&gt;
&lt;li&gt;generate UI directions using &lt;a href="https://stitch.withgoogle.com/" rel="noopener noreferrer"&gt;Stitch&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;interact with external tools through MCP integrations
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s not a prompt problem.&lt;br&gt;&lt;br&gt;
That’s a context problem.&lt;/p&gt;

&lt;p&gt;And context has to be layered.&lt;/p&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If the model is the engine, skills are the practised moves, MCPs are the ports to the outside world, and AGENTS.md is the field manual for the project.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once I started seeing the stack this way, everything became more stable.&lt;/p&gt;


&lt;h2&gt;
  
  
  Skills Are Reusable Judgment
&lt;/h2&gt;

&lt;p&gt;The clearest explanations of skills come from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview" rel="noopener noreferrer"&gt;Anthropic’s Agent Skills Overview&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://strapi.io/blog/what-are-agent-skills-and-how-to-use-them" rel="noopener noreferrer"&gt;Strapi: What Are Agent Skills&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://agentskills.io/what-are-skills" rel="noopener noreferrer"&gt;Agent Skills Format&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://skills.sh" rel="noopener noreferrer"&gt;skills.sh&lt;/a&gt; (a curated and security-conscious skill registry)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The idea is simple:&lt;/p&gt;

&lt;p&gt;A skill packages repeatable expertise into something an agent can reuse.&lt;/p&gt;

&lt;p&gt;That matters because prompts are temporary.&lt;/p&gt;

&lt;p&gt;Skills are accumulated judgment.&lt;/p&gt;

&lt;p&gt;Instead of re-explaining how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;audit SEO
&lt;/li&gt;
&lt;li&gt;structure content
&lt;/li&gt;
&lt;li&gt;apply system constraints
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…you encode it once.&lt;/p&gt;

&lt;p&gt;Now every task starts from a higher baseline.&lt;/p&gt;



&lt;p&gt;In practice, skills do three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reduce repetition
&lt;/li&gt;
&lt;li&gt;reduce drift
&lt;/li&gt;
&lt;li&gt;make quality portable
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is the real shift.&lt;/p&gt;

&lt;p&gt;Without skills, success depends on remembering the right phrasing at the right time.&lt;/p&gt;

&lt;p&gt;With skills, you turn one-off cleverness into infrastructure.&lt;/p&gt;

&lt;p&gt;That’s leverage.&lt;/p&gt;


&lt;h2&gt;
  
  
  MCPs Give Agents Reach
&lt;/h2&gt;

&lt;p&gt;If skills shape how an agent thinks, MCPs shape what it can do.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://modelcontextprotocol.io/docs/getting-started/intro" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt; is an open standard that connects models to external systems.&lt;/p&gt;

&lt;p&gt;Models know patterns.&lt;/p&gt;

&lt;p&gt;MCPs let them touch reality.&lt;/p&gt;

&lt;p&gt;That reality includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;repositories
&lt;/li&gt;
&lt;li&gt;APIs
&lt;/li&gt;
&lt;li&gt;documentation
&lt;/li&gt;
&lt;li&gt;browsers
&lt;/li&gt;
&lt;li&gt;design tools like Stitch
&lt;/li&gt;
&lt;li&gt;databases
&lt;/li&gt;
&lt;li&gt;internal tools
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without MCPs, an agent is reasoning in isolation.&lt;/p&gt;

&lt;p&gt;With MCPs, it can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;observe
&lt;/li&gt;
&lt;li&gt;verify
&lt;/li&gt;
&lt;li&gt;act
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s the difference between generating suggestions and executing work.&lt;/p&gt;


&lt;h2&gt;
  
  
  AGENTS.md Gives Agents Memory
&lt;/h2&gt;

&lt;p&gt;The part most teams underestimate is memory.&lt;/p&gt;

&lt;p&gt;That’s where &lt;a href="https://agents.md/" rel="noopener noreferrer"&gt;AGENTS.md&lt;/a&gt; comes in.&lt;/p&gt;

&lt;p&gt;The simplest way to think about it:&lt;/p&gt;

&lt;p&gt;It’s a README for agents.&lt;/p&gt;

&lt;p&gt;But unlike a typical README, it captures the operational reality of a project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;how the codebase behaves
&lt;/li&gt;
&lt;li&gt;Which commands are safe
&lt;/li&gt;
&lt;li&gt;what constraints must be respected
&lt;/li&gt;
&lt;li&gt;what has already been learned
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In one of my workflows, this included things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;why Tailwind must stay locally compiled
&lt;/li&gt;
&lt;li&gt;why English routes remain unprefixed
&lt;/li&gt;
&lt;li&gt;how RTL must be handled
&lt;/li&gt;
&lt;li&gt;what business details must remain consistent
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without this, the agent rediscovers the project every time.&lt;/p&gt;

&lt;p&gt;With it, the agent starts from context.&lt;/p&gt;



&lt;p&gt;This ties back to something I wrote in &lt;a href="https://lewiskori.com/blog/why-i-write/" rel="noopener noreferrer"&gt;Why I Write&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;Writing forces clarity.&lt;/p&gt;

&lt;p&gt;Good documentation does the same thing for systems.&lt;/p&gt;

&lt;p&gt;It turns implicit knowledge into something reusable.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Agent Is Not the System
&lt;/h2&gt;

&lt;p&gt;This is the part that matters most.&lt;/p&gt;

&lt;p&gt;People talk about agentic engineering as if the intelligence lives entirely inside the agent.&lt;/p&gt;

&lt;p&gt;It doesn’t.&lt;/p&gt;

&lt;p&gt;The agent is just one layer.&lt;/p&gt;

&lt;p&gt;What makes the system reliable is the composition:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the human provides intent and judgment
&lt;/li&gt;
&lt;li&gt;skills provide reusable expertise
&lt;/li&gt;
&lt;li&gt;MCPs provide access to reality
&lt;/li&gt;
&lt;li&gt;AGENTS.md provides memory
&lt;/li&gt;
&lt;li&gt;the agent executes within that structure
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s the system.&lt;/p&gt;



&lt;p&gt;Recently, I watched a talk by Jacob Bank (ex-Google, founder of Relay.app) that reinforced this perspective:&lt;/p&gt;

&lt;p&gt;A few ideas stood out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI is not your intern
&lt;/li&gt;
&lt;li&gt;managing people becomes less central than designing systems
&lt;/li&gt;
&lt;li&gt;build one simple agent at a time
&lt;/li&gt;
&lt;li&gt;fire agents that don’t deliver
&lt;/li&gt;
&lt;li&gt;clarity of intent is everything
&lt;/li&gt;
&lt;li&gt;your personality becomes the differentiator
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That framing aligns closely with what I’ve experienced.&lt;/p&gt;

&lt;p&gt;The leverage doesn’t come from one powerful agent.&lt;/p&gt;

&lt;p&gt;It comes from how well the system around it is designed.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/OHJJKrJCSRU"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;




&lt;p&gt;That’s why I don’t think the future belongs to people who are just good at prompting.&lt;/p&gt;

&lt;p&gt;It belongs to people who can design systems of context.&lt;/p&gt;

&lt;p&gt;People who can decide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what should be reusable
&lt;/li&gt;
&lt;li&gt;what should be connected
&lt;/li&gt;
&lt;li&gt;what should be constrained
&lt;/li&gt;
&lt;li&gt;what should remain human
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;In &lt;a href="https://lewiskori.com/blog/building-better-looking-back-on-2025/" rel="noopener noreferrer"&gt;Building Better: Looking Back on 2025&lt;/a&gt;, I wrote about building better systems around my life and work.&lt;/p&gt;

&lt;p&gt;This is the same instinct.&lt;/p&gt;

&lt;p&gt;The goal isn’t to layer AI onto existing habits.&lt;/p&gt;

&lt;p&gt;The goal is to redesign the workflow so intelligence has somewhere useful to go.&lt;/p&gt;




&lt;p&gt;Agentic engineering is not the art of finding one perfect prompt.&lt;/p&gt;

&lt;p&gt;It’s the discipline of building context architecture that makes good work more likely.&lt;/p&gt;

&lt;p&gt;That’s where the leverage is.&lt;/p&gt;

&lt;p&gt;That’s where the quality comes from.&lt;/p&gt;

&lt;p&gt;And that’s where the real craft of this era is being built.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I’m Exploring Next
&lt;/h2&gt;

&lt;p&gt;This piece is part of a broader exploration into agentic systems.&lt;/p&gt;

&lt;p&gt;Next, I’ll be diving deeper into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Model Context Protocol (MCP) and how it actually works in practice&lt;/li&gt;
&lt;li&gt;Designing skill libraries for real production workflows&lt;/li&gt;
&lt;li&gt;How to structure agent-first systems in real applications&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://strapi.io/blog/what-are-agent-skills-and-how-to-use-them" rel="noopener noreferrer"&gt;What Are Agent Skills and How To Use Them&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview" rel="noopener noreferrer"&gt;Anthropic: Agent Skills Overview&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://modelcontextprotocol.io/docs/getting-started/intro" rel="noopener noreferrer"&gt;Model Context Protocol: Introduction&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://agents.md/" rel="noopener noreferrer"&gt;AGENTS.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://agentskills.io/what-are-skills" rel="noopener noreferrer"&gt;What Are Skills?&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://skills.sh" rel="noopener noreferrer"&gt;skills.sh&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>agentskills</category>
      <category>mcp</category>
      <category>programming</category>
    </item>
    <item>
      <title>AI Is Not Replacing You. It’s Reshaping How You Think</title>
      <dc:creator>Lewis kori</dc:creator>
      <pubDate>Wed, 18 Feb 2026 18:17:15 +0000</pubDate>
      <link>https://forem.com/lewiskori/ai-is-not-replacing-you-its-reshaping-how-you-think-2e9m</link>
      <guid>https://forem.com/lewiskori/ai-is-not-replacing-you-its-reshaping-how-you-think-2e9m</guid>
      <description>&lt;p&gt;When AI started writing decent code, I did not feel excitement.&lt;/p&gt;

&lt;p&gt;I felt unsettled.&lt;/p&gt;

&lt;p&gt;Part of it was personal. I had built a career on being able to untangle complex systems, reason through architectural tradeoffs, and hold messy abstractions in my head until they became clear.&lt;/p&gt;

&lt;p&gt;But part of it was practical.&lt;/p&gt;

&lt;p&gt;If a model can scaffold refactors, generate tests, reason about edge cases, and produce solid patterns in seconds, then the question becomes unavoidable:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What happens to engineers when parts of their skill set become automated?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That fear is not irrational.&lt;/p&gt;

&lt;p&gt;Teams are getting leaner. Productivity expectations are rising. Companies are under pressure to ship more with fewer people. AI is not a novelty anymore. It is embedded into workflows.&lt;/p&gt;

&lt;p&gt;The work is changing in real time.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Misunderstanding About AI
&lt;/h2&gt;

&lt;p&gt;There are two extreme reactions to AI.&lt;/p&gt;

&lt;p&gt;One camp believes it will replace human engineers entirely.&lt;br&gt;
The other dismisses it as glorified autocomplete.&lt;/p&gt;

&lt;p&gt;Both miss something important.&lt;/p&gt;

&lt;p&gt;AI does not think like a human. It does not understand systems in the experiential sense. It does not care about uptime, team velocity, or long-term maintainability.&lt;/p&gt;

&lt;p&gt;It predicts patterns.&lt;/p&gt;

&lt;p&gt;And prediction at scale can look remarkably competent.&lt;/p&gt;

&lt;p&gt;But prediction is not judgment.&lt;/p&gt;

&lt;p&gt;That distinction is critical.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Real Fear: Economics and Value
&lt;/h2&gt;

&lt;p&gt;The anxiety around AI usually blends two concerns.&lt;/p&gt;

&lt;p&gt;The first is economic.&lt;br&gt;
Will this reduce demand for engineers?&lt;/p&gt;

&lt;p&gt;The second is personal.&lt;br&gt;
Will this reduce the value of what I bring?&lt;/p&gt;

&lt;p&gt;AI absolutely compresses certain types of work. Boilerplate generation, repetitive refactors, scaffolding, and documentation drafts. The cost of iteration is dropping fast.&lt;/p&gt;

&lt;p&gt;But that does not eliminate engineering.&lt;/p&gt;

&lt;p&gt;It shifts the bottleneck.&lt;/p&gt;

&lt;p&gt;When code generation becomes easier, the constraint moves to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Architecture decisions&lt;/li&gt;
&lt;li&gt;Systems integration&lt;/li&gt;
&lt;li&gt;Tradeoff analysis&lt;/li&gt;
&lt;li&gt;Failure modeling&lt;/li&gt;
&lt;li&gt;Product alignment&lt;/li&gt;
&lt;li&gt;Accountability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The floor lowers.&lt;/p&gt;

&lt;p&gt;The ceiling rises.&lt;/p&gt;

&lt;p&gt;Engineers who compete only on raw output will struggle.&lt;/p&gt;

&lt;p&gt;Engineers who operate at the level of systems and constraints will gain leverage.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Turning Point in My Workflow
&lt;/h2&gt;

&lt;p&gt;The shift for me happened when I stopped using AI as a replacement and started using it as a thinking partner.&lt;/p&gt;

&lt;p&gt;Instead of asking it to “write this feature,” I began asking it to stress test my ideas.&lt;/p&gt;

&lt;p&gt;I would describe a multitenant billing system with evolving discount rules and ask it to critique the domain model. I would have it enumerate failure modes in webhook handling, idempotency guarantees, and concurrency boundaries. I would ask it to propose alternative abstractions and then interrogate them.&lt;/p&gt;

&lt;p&gt;Suddenly, it was not competing with me.&lt;/p&gt;

&lt;p&gt;It was expanding my cognitive bandwidth.&lt;/p&gt;

&lt;p&gt;Instead of spending an hour drafting a single architecture direction, I could explore multiple viable approaches quickly and focus on the harder question:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Which one survives contact with reality?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;My job became less about producing code from scratch and more about defining constraints, filtering options, and owning decisions.&lt;/p&gt;

&lt;p&gt;That is higher leverage work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reading AI 2041 Put This in Context
&lt;/h2&gt;

&lt;p&gt;While navigating this shift, I started reading &lt;a href="https://a.co/d/01NkYYVm" rel="noopener noreferrer"&gt;AI 2041: Ten Visions for Our Future by Kai-Fu Lee and Chen Qiufan&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The book pairs speculative stories set in 2041 with grounded technical analysis of where AI is realistically heading. It does not frame AI as magic. It frames it as infrastructure.&lt;/p&gt;

&lt;p&gt;In one story, AI assistants optimise daily decisions so effectively that they begin shaping behaviour itself. In another, hyper-realistic synthetic media challenges society’s ability to distinguish truth from fabrication. Other chapters explore personalised AI education, autonomous systems, and algorithmic governance.&lt;/p&gt;

&lt;p&gt;The consistent pattern is this:&lt;/p&gt;

&lt;p&gt;AI expands capability.&lt;br&gt;
Humans remain accountable.&lt;/p&gt;

&lt;p&gt;The systems amplify us. They do not originate purpose.&lt;/p&gt;

&lt;p&gt;That mirrors what I see in engineering. AI can generate options. It cannot decide what is aligned with the product vision, the business model, or long-term technical health.&lt;/p&gt;

&lt;p&gt;Those decisions remain human.&lt;/p&gt;




&lt;h2&gt;
  
  
  The AI Stack I’m Using
&lt;/h2&gt;

&lt;p&gt;This shift is not abstract. It is operational.&lt;/p&gt;

&lt;p&gt;Here are the tools currently shaping how I work.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Copilot Pro+
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/features/copilot" rel="noopener noreferrer"&gt;GitHub Copilot Pro+&lt;/a&gt; lives directly in my editor. It handles low-level friction extremely well. Boilerplate, repetitive patterns, test scaffolding, and interface implementations. It uses local context to generate code that aligns with the file and project structure.&lt;/p&gt;

&lt;p&gt;The fundamental benefit is cognitive offloading.&lt;/p&gt;

&lt;p&gt;Instead of spending working memory on syntactic repetition, I stay focused on architecture and intent. Copilot accelerates expression. It does not replace direction.&lt;/p&gt;




&lt;h3&gt;
  
  
  Variant
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://variant.com/" rel="noopener noreferrer"&gt;Variant&lt;/a&gt; is closer to an AI-native website design tool than a coding assistant. Conceptually, it sits nearer to design platforms like &lt;a href="https://www.figma.com/make/" rel="noopener noreferrer"&gt;Figma make&lt;/a&gt; or emerging AI-enhanced layout builders.&lt;/p&gt;

&lt;p&gt;Instead of focusing on implementation details, Variant accelerates layout generation, design system consistency, and structured UI exploration.&lt;/p&gt;

&lt;p&gt;For engineers who touch frontend systems, this matters.&lt;/p&gt;

&lt;p&gt;It shortens the loop between concept and visual artefact. Instead of manually iterating on layout structures or styling hierarchies, AI can generate structured starting points that you refine.&lt;/p&gt;

&lt;p&gt;This shifts attention from pixel pushing to experience reasoning.&lt;/p&gt;




&lt;h3&gt;
  
  
  ChatGPT Codex
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://chatgpt.com/codex" rel="noopener noreferrer"&gt;ChatGPT Codex&lt;/a&gt; is where I go for higher-level reasoning.&lt;/p&gt;

&lt;p&gt;When I need to explore design tradeoffs, simulate adversarial critiques, or prototype alternative abstractions, this is the thinking layer.&lt;/p&gt;

&lt;p&gt;I use it to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stress test system boundaries&lt;/li&gt;
&lt;li&gt;Model edge case behaviour&lt;/li&gt;
&lt;li&gt;Generate multiple architecture directions quickly&lt;/li&gt;
&lt;li&gt;Challenge assumptions before implementation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fundamental benefit is parallel cognition.&lt;/p&gt;

&lt;p&gt;It feels less like delegation and more like expanding thought capacity. I remain accountable. But I am no longer thinking alone.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Real Difference Between Humans and AI
&lt;/h2&gt;

&lt;p&gt;AI responds to framed problems.&lt;/p&gt;

&lt;p&gt;Humans decide which problems matter.&lt;/p&gt;

&lt;p&gt;AI can expand the solution space. It can generate variations at scale. It can surface blind spots.&lt;/p&gt;

&lt;p&gt;It cannot choose meaning.&lt;br&gt;
It cannot care about consequences.&lt;br&gt;
It cannot take responsibility when systems fail.&lt;/p&gt;

&lt;p&gt;Engineering is not just code generation.&lt;/p&gt;

&lt;p&gt;It is ownership.&lt;/p&gt;

&lt;p&gt;That part is not automated.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where This Is Headed
&lt;/h2&gt;

&lt;p&gt;AI will continue compressing certain tasks.&lt;/p&gt;

&lt;p&gt;Entry-level work will change. Expectations for productivity will rise. Teams will likely stay leaner.&lt;/p&gt;

&lt;p&gt;But the demand for people who can define systems, reason about complexity, and own outcomes is not disappearing.&lt;/p&gt;

&lt;p&gt;If anything, it becomes more valuable.&lt;/p&gt;

&lt;p&gt;The engineers who thrive will not be the fastest typists.&lt;/p&gt;

&lt;p&gt;They will be the ones who:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Define the right problems&lt;/li&gt;
&lt;li&gt;Set clear constraints&lt;/li&gt;
&lt;li&gt;Interrogate AI outputs critically&lt;/li&gt;
&lt;li&gt;Integrate tools into disciplined workflows&lt;/li&gt;
&lt;li&gt;Take responsibility for results&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI is not replacing engineers entirely.&lt;/p&gt;

&lt;p&gt;It is reshaping engineering.&lt;/p&gt;

&lt;p&gt;The work becomes less about proving you can produce code alone and more about orchestrating intelligence effectively.&lt;/p&gt;

&lt;p&gt;That is not a downgrade.&lt;/p&gt;

&lt;p&gt;It is a shift in leverage.&lt;/p&gt;

&lt;p&gt;And the sooner we adapt to that reality, the better positioned we will be in the decade ahead.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>software</category>
    </item>
    <item>
      <title>From Gridsome to Astro: Rebuilding My Personal Site for the Next Phase</title>
      <dc:creator>Lewis kori</dc:creator>
      <pubDate>Fri, 23 Jan 2026 19:42:14 +0000</pubDate>
      <link>https://forem.com/lewiskori/from-gridsome-to-astro-rebuilding-my-personal-site-for-the-next-phase-1a24</link>
      <guid>https://forem.com/lewiskori/from-gridsome-to-astro-rebuilding-my-personal-site-for-the-next-phase-1a24</guid>
      <description>&lt;p&gt;Six years ago, I rebuilt my personal website using Gridsome and Vue.js. At the time, it felt modern, fast, and perfectly aligned with how I wanted to publish technical writing.&lt;/p&gt;

&lt;p&gt;That rebuild became this article:&lt;br&gt;&lt;br&gt;
👉 &lt;em&gt;&lt;a href="https://lewiskori.com/blog/building-my-new-site-with-gridsome-vue-js/" rel="noopener noreferrer"&gt;Building my new site with Gridsome &amp;amp; Vue.js&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Fast forward to today, that same site still exists at &lt;strong&gt;&lt;a href="https://lewiskori.netlify.app" rel="noopener noreferrer"&gt;lewiskori.netlify.app&lt;/a&gt;&lt;/strong&gt;, but it had started to feel like a time capsule. The tooling had aged, the ecosystem had slowed down, and more importantly, my own work and interests had expanded far beyond what the site was originally designed to represent.&lt;/p&gt;

&lt;p&gt;So I decided to rebuild again.&lt;br&gt;&lt;br&gt;
This time with &lt;strong&gt;&lt;a href="https://astro.build/" rel="noopener noreferrer"&gt;Astro&lt;/a&gt;&lt;/strong&gt;, hosted on &lt;strong&gt;&lt;a href="https://pages.cloudflare.com/" rel="noopener noreferrer"&gt;Cloudflare Pages&lt;/a&gt;&lt;/strong&gt;, with a stronger content model, better performance, and a structure that reflects where I am today as an engineer and advisor.&lt;/p&gt;

&lt;p&gt;This post is about why I made that move, what changed, and what I learned along the way.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Move Away from Gridsome?
&lt;/h2&gt;

&lt;p&gt;To be clear, Gridsome served me well. It gave me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Static site generation with Vue&lt;/li&gt;
&lt;li&gt;Markdown-based content&lt;/li&gt;
&lt;li&gt;A smooth authoring experience for the time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The ecosystem is largely &lt;strong&gt;inactive&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Plugin maintenance has slowed significantly&lt;/li&gt;
&lt;li&gt;Modern web tooling has moved on&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In short, it became &lt;strong&gt;harder to evolve the site than to rebuild it&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For a personal site that I actually want to use as a thinking space, that is not a good place to be.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Astro Felt Like the Right Fit
&lt;/h2&gt;

&lt;p&gt;Astro hit a very specific sweet spot for what I wanted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Content-first architecture&lt;/strong&gt; (Markdown, MDX, collections)&lt;/li&gt;
&lt;li&gt;Minimal client-side JavaScript by default&lt;/li&gt;
&lt;li&gt;Framework-agnostic components when needed&lt;/li&gt;
&lt;li&gt;Excellent performance out of the box&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But more than features, Astro feels aligned with how the web is trending again:&lt;br&gt;&lt;br&gt;
static where possible, dynamic only where necessary.&lt;/p&gt;

&lt;p&gt;That matches how I write and publish. Most of my content does not need hydration, interactivity, or heavy client logic.&lt;/p&gt;

&lt;p&gt;It just needs to load instantly and read well.&lt;/p&gt;




&lt;h2&gt;
  
  
  Multiple Themes: Light, Dark, and Sepia
&lt;/h2&gt;

&lt;p&gt;One of the things I really wanted in this rebuild was &lt;strong&gt;intentional reading modes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The new site supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Light mode&lt;/li&gt;
&lt;li&gt;Dark mode&lt;/li&gt;
&lt;li&gt;Sepia mode (surprisingly great for long-form reading)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Theme preference is persisted, and switching is instant.&lt;/p&gt;

&lt;p&gt;This seems like a small detail, but it changes how comfortable long sessions on the site feel, especially for essays and technical deep dives.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Broader Representation of My Work
&lt;/h2&gt;

&lt;p&gt;Previously, the site was mostly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Blog posts&lt;/li&gt;
&lt;li&gt;A simple “About” section&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That no longer reflects what I actually do.&lt;/p&gt;

&lt;p&gt;The new structure includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://lewiskori.com/advisory" rel="noopener noreferrer"&gt;Advisory&lt;/a&gt;&lt;/strong&gt; – work with startups and private equity-backed firms&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://lewiskori.com/projects" rel="noopener noreferrer"&gt;Projects&lt;/a&gt;&lt;/strong&gt; – long-running technical efforts and experiments&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://lewiskori.com/resources" rel="noopener noreferrer"&gt;Resources&lt;/a&gt;&lt;/strong&gt; – books I am reading, tools I use, desk setup, and stack&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://lewiskori.com/operating-notes" rel="noopener noreferrer"&gt;Operating Notes&lt;/a&gt;&lt;/strong&gt; – practical reflections on building companies and systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This was intentional.&lt;/p&gt;

&lt;p&gt;I wanted the site to capture not just what I build, but &lt;strong&gt;how I think about building&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Better Content Discovery: Series and Tags
&lt;/h2&gt;

&lt;p&gt;As my writing has improved, so has the structure of my content.&lt;/p&gt;

&lt;p&gt;Now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Posts can belong to &lt;strong&gt;series&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Tags are actually useful for navigation&lt;/li&gt;
&lt;li&gt;Related content is easier to surface&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes the site more usable for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;New readers&lt;/li&gt;
&lt;li&gt;People landing from search&lt;/li&gt;
&lt;li&gt;Anyone following a specific topic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also forces me to be more disciplined about how I publish.&lt;/p&gt;




&lt;h2&gt;
  
  
  Moving from Netlify to Cloudflare Pages
&lt;/h2&gt;

&lt;p&gt;This move was both practical and strategic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Practically:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;GitHub-based CI/CD is still there&lt;/li&gt;
&lt;li&gt;Builds are fast&lt;/li&gt;
&lt;li&gt;Edge delivery is excellent&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Strategically:
&lt;/h3&gt;

&lt;p&gt;Cloudflare &lt;a href="https://astro.build/blog/joining-cloudflare/" rel="noopener noreferrer"&gt;has acquired Astro&lt;/a&gt;, which gives me far more confidence in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Long-term platform stability&lt;/li&gt;
&lt;li&gt;Investment in performance tooling&lt;/li&gt;
&lt;li&gt;Tight integration with edge services&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is rare to see hosting, CDN, and framework alignment at this level, and it made the decision straightforward.&lt;/p&gt;




&lt;h2&gt;
  
  
  Performance: Actually, Genuinely Fast
&lt;/h2&gt;

&lt;p&gt;Astro already helps here, but I was deliberate about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zero unnecessary hydration&lt;/li&gt;
&lt;li&gt;Minimal client-side scripts&lt;/li&gt;
&lt;li&gt;Aggressive static generation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is a site that is, frankly, boring in how fast it loads. Which is exactly what I want.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://pagespeed.web.dev/analysis/https-lewiskori-com/oaofktzutt?form_factor=mobile" rel="noopener noreferrer"&gt;Lighthouse test scores speak for themselves&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhxxauqdxyafz45nx6sxp.jpg" 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%2Fhxxauqdxyafz45nx6sxp.jpg" alt="Lighthouse performance results showing near-perfect mobile scores across performance, accessibility, best practices, and SEO" width="800" height="519"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Performance is consistently near perfect&lt;/li&gt;
&lt;li&gt;No layout shift&lt;/li&gt;
&lt;li&gt;Immediate content paint&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This matters not just for SEO but also for reading comfort.&lt;/p&gt;




&lt;h2&gt;
  
  
  Newsletter Support with Beehiiv
&lt;/h2&gt;

&lt;p&gt;Writing is something I am taking seriously again.&lt;/p&gt;

&lt;p&gt;The new site integrates directly with &lt;a href="https://www.beehiiv.com?via=Lewis-Kori" rel="noopener noreferrer"&gt;Beehiiv&lt;/a&gt; for newsletter subscriptions, which allows me to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Publish long-form essays&lt;/li&gt;
&lt;li&gt;Reach readers outside of social platforms&lt;/li&gt;
&lt;li&gt;Own the distribution channel&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This also aligns with my decision over the last year to step away from algorithm-driven platforms and focus on slower, more intentional publishing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Open Sourcing the Entire Codebase
&lt;/h2&gt;

&lt;p&gt;This part matters to me.&lt;/p&gt;

&lt;p&gt;The full site is now open source.&lt;/p&gt;

&lt;p&gt;Not because the code is groundbreaking, but because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Personal sites are great learning material&lt;/li&gt;
&lt;li&gt;People often ask how certain layouts or systems work&lt;/li&gt;
&lt;li&gt;Transparency aligns with how I learned early in my career&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If something in the site helps another developer ship their own, that is a win.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/lewis-kori/astro-portfolio-v3" rel="noopener noreferrer"&gt;Open Source Code is available on my Github&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And a special thank you to &lt;a href="https://github.com/EvansRobbie" rel="noopener noreferrer"&gt;Evans Robbie &lt;/a&gt;for all the help with the frontend styling.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Rebuild Actually Represents
&lt;/h2&gt;

&lt;p&gt;On the surface, this is a framework migration.&lt;/p&gt;

&lt;p&gt;In reality, it reflects something deeper:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I write differently now&lt;/li&gt;
&lt;li&gt;I work across engineering, business, and strategy&lt;/li&gt;
&lt;li&gt;I care more about clarity than cleverness&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This site is not just a portfolio anymore.&lt;br&gt;&lt;br&gt;
It is an operating system for how I think, learn, and share.&lt;/p&gt;

&lt;p&gt;Astro just happened to be the best tool to support that.&lt;/p&gt;




&lt;h2&gt;
  
  
  What’s Next
&lt;/h2&gt;

&lt;p&gt;Now that the foundation feels solid, the focus shifts to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Writing more consistently&lt;/li&gt;
&lt;li&gt;Publishing deeper technical and business essays&lt;/li&gt;
&lt;li&gt;Sharing practical experiences from advisory and operating work&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The stack is finally out of the way.&lt;/p&gt;

&lt;p&gt;Now the real work, the thinking and the writing, can take centre stage again.&lt;/p&gt;




&lt;p&gt;If you are curious, the previous version of the site still lives here:&lt;br&gt;&lt;br&gt;
👉 &lt;strong&gt;&lt;a href="https://lewiskori.netlify.app" rel="noopener noreferrer"&gt;https://lewiskori.netlify.app&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And the new one is live at:&lt;br&gt;&lt;br&gt;
👉 &lt;strong&gt;&lt;a href="https://lewiskori.com" rel="noopener noreferrer"&gt;https://lewiskori.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is to rebuilding, again, but this time with much clearer intent.&lt;/p&gt;

</description>
      <category>astro</category>
      <category>vue</category>
      <category>webdev</category>
      <category>portfolio</category>
    </item>
    <item>
      <title>Smarter Apps with PostHog: Feature Flags &amp; Analytics</title>
      <dc:creator>Lewis kori</dc:creator>
      <pubDate>Thu, 11 Sep 2025 12:52:20 +0000</pubDate>
      <link>https://forem.com/lewiskori/smarter-apps-with-posthog-feature-flags-analytics-pf5</link>
      <guid>https://forem.com/lewiskori/smarter-apps-with-posthog-feature-flags-analytics-pf5</guid>
      <description>&lt;p&gt;Building an app, whether it's a quick side project or a full-fledged SaaS solution, is an exciting adventure. You eagerly ship a new feature, hoping for enthusiastic feedback, but often find yourself waiting in silence. The reality is, without clear insights, we often fly blind. We move fast, but sometimes we end up going in circles.&lt;/p&gt;

&lt;p&gt;That's where analytics and feature flags come in. They aren't just for the major players anymore. These tools are essential for understanding what's working, what's not, and how users are &lt;em&gt;truly&lt;/em&gt; engaging with your product.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why Django Developers Should Care
&lt;/h4&gt;

&lt;p&gt;As a Django developer, I love the "batteries-included" philosophy. It lets us build robust, secure backends at incredible speed. But when it comes to product-led growth—understanding user behaviour, rolling out features safely, and running experiments—we often have to piece together solutions. Integrating separate analytics packages, building a feature toggle system from scratch... it can feel like a puzzle.&lt;/p&gt;

&lt;p&gt;This is why I've been so impressed with &lt;a href="https://posthog.com/" rel="noopener noreferrer"&gt;PostHog&lt;/a&gt;. It’s a platform built for product engineers that combines analytics, feature flags, and session replay into a single, cohesive solution. For a Django developer, this means you can spend less time integrating tools and more time building what matters, backed by real data.&lt;/p&gt;

&lt;p&gt;Let’s dive into how PostHog can make your Django app smarter, safer and more data-driven.&lt;/p&gt;




&lt;h3&gt;
  
  
  Pre-requisites
&lt;/h3&gt;

&lt;p&gt;Before following along, make sure you have the basics in place:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python 3.10+&lt;/strong&gt; and &lt;strong&gt;Django 4.0+&lt;/strong&gt; (the examples assume a modern Django stack).&lt;/li&gt;
&lt;li&gt;A working &lt;strong&gt;multi-tenant setup&lt;/strong&gt; (e.g. using &lt;a href="https://django-tenants.readthedocs.io/" rel="noopener noreferrer"&gt;django-tenants&lt;/a&gt; or a similar package), since group identification is demonstrated with &lt;code&gt;request.tenant&lt;/code&gt;. - Optional&lt;/li&gt;
&lt;li&gt;An existing &lt;strong&gt;PostHog account&lt;/strong&gt; with a project API key. You can sign up for free at &lt;a href="https://posthog.com/" rel="noopener noreferrer"&gt;posthog.com&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Familiarity with &lt;strong&gt;Django middleware&lt;/strong&gt; and how to add custom classes to the &lt;code&gt;MIDDLEWARE&lt;/code&gt; list.&lt;/li&gt;
&lt;li&gt;Optional but recommended: &lt;strong&gt;Docker&lt;/strong&gt; if you prefer to self-host PostHog instead of using their cloud service.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With these in place, you’ll be able to plug in PostHog confidently and follow every step without hitting blockers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting Started: A Production-Ready Setup
&lt;/h3&gt;

&lt;p&gt;Before you can use feature flags or track events, you need to get PostHog configured. Instead of using global variables, let's build a robust, &lt;a href="https://refactoring.guru/design-patterns/singleton" rel="noopener noreferrer"&gt;singleton client&lt;/a&gt; and a &lt;a href="https://docs.djangoproject.com/en/5.2/topics/http/middleware/" rel="noopener noreferrer"&gt;context-aware middleware&lt;/a&gt;. This pattern is safer for concurrent requests and much easier to manage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Install the Library&lt;/strong&gt;&lt;br&gt;
First, add the official Python library to your project:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: Configure Your &lt;code&gt;settings.py&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
Add your PostHog API key to your Django settings. It's best practice to load this from an environment variable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# in settings.py
&lt;/span&gt;&lt;span class="n"&gt;POSTHOG_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POSTHOG_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;POSTHOG_HOST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POSTHOG_HOST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://app.posthog.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;POSTHOG_ENABLED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POSTHOG_ENABLED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3: Create a Centralised PostHog Client&lt;/strong&gt;&lt;br&gt;
This is the core of a robust integration. A &lt;a href="https://refactoring.guru/design-patterns/singleton" rel="noopener noreferrer"&gt;singleton client&lt;/a&gt; encapsulates the setup logic and ensures the client is initialised only once for your entire application. Create a new file for it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# In a new file, e.g., myapp/integrations/posthog_client.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;posthog&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.conf&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;

&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostHogClient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    A singleton client for PostHog to provide a central, configured
    instance for the entire application.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;_instance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__new__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_instance&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_instance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PostHogClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;__new__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POSTHOG_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POSTHOG_API_KEY not found. PostHog will be disabled.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;posthog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;project_api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POSTHOG_HOST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://app.posthog.com&lt;/span&gt;&lt;span class="sh"&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="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_instance&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_feature_enabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;feature_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

        &lt;span class="n"&gt;groups&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;organization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="n"&gt;group_properties&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;organization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;plan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt; &lt;span class="k"&gt;else&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;feature_enabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;feature_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;distinct_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;groups&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;group_properties&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;group_properties&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;logger&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Failed to check PostHog feature flag &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;feature_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

&lt;span class="c1"&gt;# Create the shared singleton instance for your project to import
&lt;/span&gt;&lt;span class="n"&gt;posthog_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PostHogClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4: Create the Identification Middleware&lt;/strong&gt;&lt;br&gt;
For PostHog to work correctly, it needs to know who the user is on every request. This middleware uses our new client and wraps each request in a &lt;code&gt;new_context&lt;/code&gt; to ensure user data doesn't leak between concurrent requests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# In a new file, e.g., myapp/integrations/posthog_middleware.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;.posthog_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;posthog_client&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostHogMiddleware&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;get_response&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_response&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;posthog_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__call__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Use a new context for each request to ensure thread-safety
&lt;/span&gt;        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_context&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;hasattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_authenticated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;
                &lt;span class="n"&gt;organization&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tenant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;distinct_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_full_name&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="c1"&gt;# Optionally attach group (e.g., tenant/organization)
&lt;/span&gt;                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;organization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group_identify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="n"&gt;group_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;organization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;group_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;organization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;organization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;plan&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;organization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 5: Activate the Middleware&lt;/strong&gt;&lt;br&gt;
Finally, add the middleware to your &lt;code&gt;settings.py&lt;/code&gt;. It must come &lt;strong&gt;after&lt;/strong&gt; Django's &lt;code&gt;AuthenticationMiddleware&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# in settings.py
&lt;/span&gt;&lt;span class="n"&gt;MIDDLEWARE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c1"&gt;# ... other middleware ...
&lt;/span&gt;    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;django.contrib.auth.middleware.AuthenticationMiddleware&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;myapp.integrations.posthog_middleware.PostHogMiddleware&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# ... other middleware ...
&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 setup complete, your integration is now robust, safe, and ready for advanced use cases.&lt;/p&gt;




&lt;h4&gt;
  
  
  Use Case 1: Rolling Out Features with Smarter Feature Flags
&lt;/h4&gt;

&lt;p&gt;While you can target individual users, the real power in a SaaS app comes from targeting &lt;strong&gt;groups&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Pro-Level Use Case: Gating Features by Subscription Plan&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine you have different plans (&lt;code&gt;BASIC&lt;/code&gt;, &lt;code&gt;PRO&lt;/code&gt;) and want to give a company on the &lt;code&gt;BASIC&lt;/code&gt; plan a free trial of a &lt;code&gt;PRO&lt;/code&gt; feature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: The Centralised Feature Checker&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Create a central function that first checks your app's internal logic (the plan) and then asks PostHog for any overrides.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# In a central permissions.py file
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;.integrations.posthog_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;posthog_client&lt;/span&gt;

&lt;span class="n"&gt;FEATURE_AI_SEARCH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ai_search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;PLAN_FEATURES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BASIC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PRO&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;FEATURE_AI_SEARCH&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;has_feature_access&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;organization&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;feature_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# 1. Check the plan first (fast, local check)
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;feature_name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;PLAN_FEATURES&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="n"&gt;organization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="c1"&gt;# 2. If not, ask PostHog for an override using our client.
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;posthog_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_feature_enabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;feature_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;organization&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: Protecting Your Views&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now, you can protect your views cleanly using a custom mixin.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;.permissions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;has_feature_access&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FEATURE_AI_SEARCH&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FeatureFlagMixin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;feature_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;feature_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;has_feature_access&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;feature_name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;PermissionDenied&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;This feature is not enabled for your account.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AiSearchView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FeatureFlagMixin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TemplateView&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;feature_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FEATURE_AI_SEARCH&lt;/span&gt;
    &lt;span class="n"&gt;template_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ai_search.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern lets you manage base permissions with your subscription plans and use PostHog to dynamically grant trial access, driving upgrades.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Use Case 2: Understanding Behavior with Proper Identification&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;The middleware we set up handles identification automatically, ensuring every event is correctly associated with the user and their organization.&lt;/p&gt;

&lt;p&gt;When you capture a custom event from the backend, it's now context-aware:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# In a view, after a user uploads an article
&lt;/span&gt;&lt;span class="n"&gt;posthog_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;capture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;distinct_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;article_listed&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;article_id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;new_article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;new_article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;groups&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;organization&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&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;h4&gt;
  
  
  &lt;strong&gt;Bonus: Session Replay&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Want to see exactly how users interact with your app? Enable &lt;a href="https://posthog.com/docs/session-replay" rel="noopener noreferrer"&gt;session replay&lt;/a&gt; in PostHog. Watching real user sessions is one of the fastest ways to find UI bugs and UX friction points—it’s like looking over your user's shoulder.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Best Practices &amp;amp; Lessons Learned&lt;/strong&gt;
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Think in Groups, Not Just Users:&lt;/strong&gt; For any SaaS or multi-tenant app, group analytics are more powerful than user analytics. Always identify the company, team or organization the user belongs to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Centralise Your Logic:&lt;/strong&gt; Don't sprinkle PostHog calls all over your codebase. Create a central client singleton and use middleware for identification.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep Flags Tidy:&lt;/strong&gt; Feature flags are powerful but can become technical debt. Regularly review and remove flags for features that are fully rolled out.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Conclusion: The Swiss Army Knife for Django Product Teams&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;With PostHog, you’re not just tracking clicks—you’re building a feedback loop. Feature flags de-risk your deployments, analytics tell you what’s working, and session replay shows you the "why" behind the numbers. For Django developers looking to build smarter, safer, and more responsive applications, it's an essential tool. You can finally ship, learn and improve—without flying blind.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>analytics</category>
      <category>python</category>
    </item>
    <item>
      <title>What Makes a Good Website? Lessons From the Edge</title>
      <dc:creator>Lewis kori</dc:creator>
      <pubDate>Tue, 26 Aug 2025 16:03:36 +0000</pubDate>
      <link>https://forem.com/lewiskori/what-makes-a-good-website-lessons-from-the-edge-3aoa</link>
      <guid>https://forem.com/lewiskori/what-makes-a-good-website-lessons-from-the-edge-3aoa</guid>
      <description>&lt;p&gt;Over the past few years, I’ve had the privilege of working across fintech, blockchain, healthcare, and a variety of ambitious ventures through my consultancy work and &lt;a href="https://inflectionstudio.io" rel="noopener noreferrer"&gt;Inflection Studio&lt;/a&gt;. Somewhere along the way, I started to notice a pattern. Projects that thrived weren’t always the ones with the biggest budgets or flashiest designs. They were the ones that got the fundamentals right.&lt;/p&gt;

&lt;p&gt;This post is an attempt to capture those lessons. Not as “best practices” in the abstract, but as principles I’ve seen play out in the wild. Consider this my working definition of what makes a &lt;strong&gt;good website&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Clarity Before Cleverness
&lt;/h2&gt;

&lt;p&gt;A good website doesn’t confuse you. The value proposition is clear from the first glance. You know what the business is about, who it’s for, and what to do next.&lt;/p&gt;

&lt;p&gt;Too many projects get caught up in aesthetics, burying the message under animations, gradients, and jargon. I’ve been guilty of this too. At &lt;a href="https://inflectionstudio.io" rel="noopener noreferrer"&gt;Inflection Studio&lt;/a&gt;, we’ve learned to strip it back. Design should guide, not distract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test:&lt;/strong&gt; If a stranger can’t explain what your site does after 10 seconds, you’ve missed the mark.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Speed is Strategy
&lt;/h2&gt;

&lt;p&gt;Nobody waits for a slow site. Performance is the invisible handshake between your product and the user. It communicates professionalism, care, and respect for their time.&lt;/p&gt;

&lt;p&gt;We’ve adopted the principle that &lt;strong&gt;performance is design&lt;/strong&gt;. It shapes everything from tech stack decisions (Next.js, caching, CDNs) to how we think about content delivery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test:&lt;/strong&gt; If your site feels instant, you’re not just winning on UX — you’re improving SEO, conversions, and user trust.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Responsive by Default
&lt;/h2&gt;

&lt;p&gt;This isn’t 2010. Your site isn’t being viewed only on a desktop monitor in a quiet office. It’s being pulled up on the go, in poor lighting, on shaky networks.&lt;/p&gt;

&lt;p&gt;At &lt;a href="https://inflectionstudio.io" rel="noopener noreferrer"&gt;Inflection Studio&lt;/a&gt;, we design for the smallest screen first. This isn’t just mobile responsiveness — it’s about empathy. Meeting users where they are, not where it’s convenient for us.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test:&lt;/strong&gt; If your mobile experience feels like an afterthought, the site isn’t done yet.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Brand as Experience
&lt;/h2&gt;

&lt;p&gt;A logo and color palette don’t make a brand. The real test is whether a website feels like an extension of the company’s voice and ethos.&lt;/p&gt;

&lt;p&gt;I’ve seen projects where the site looked good but felt hollow — like a template with someone else’s clothes. What sticks is consistency: visuals, copy, interactions, all reinforcing the same story.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test:&lt;/strong&gt; If you stripped the logo off your site, would people still recognize it as yours?&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Measurement Over Myth
&lt;/h2&gt;

&lt;p&gt;Here’s a hard truth: what feels like a “beautiful” website to you might be irrelevant to users. That’s why measurement matters.&lt;/p&gt;

&lt;p&gt;Tools like &lt;a href="https://posthog.com/" rel="noopener noreferrer"&gt;PostHog&lt;/a&gt;, &lt;a href="https://clarity.microsoft.com/" rel="noopener noreferrer"&gt;Microsoft Clarity&lt;/a&gt; and &lt;a href="https://developers.google.com/analytics" rel="noopener noreferrer"&gt;Google Analytics&lt;/a&gt; give us a window into reality. We’ve built a discipline around &lt;strong&gt;tracking what success looks like&lt;/strong&gt; — not just visits, but engagement, conversions, drop-offs. It keeps us honest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test:&lt;/strong&gt; If you can’t point to a metric that validates a design choice, it’s just opinion.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Evolution, Not Perfection
&lt;/h2&gt;

&lt;p&gt;The best websites aren’t “launched” — they’re alive. They adapt as the business grows, as customers shift, as technology changes.&lt;/p&gt;

&lt;p&gt;This is why at &lt;a href="https://inflectionstudio.io" rel="noopener noreferrer"&gt;Inflection Studio&lt;/a&gt; we design systems, not just sites. Modular content structures, scalable design languages, and workflows that make iteration easy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test:&lt;/strong&gt; If updating your site feels like starting from scratch, it wasn’t designed to last.&lt;/p&gt;




&lt;h3&gt;
  
  
  Final Thought
&lt;/h3&gt;

&lt;p&gt;A “good website” is less about aesthetics and more about &lt;strong&gt;alignment&lt;/strong&gt;. Alignment between what you say and what users need. Between the technology and the business model. Between ambition and execution.&lt;/p&gt;

&lt;p&gt;These are lessons I’m still refining in my consultancy and through the work we do at &lt;a href="https://inflectionstudio.io" rel="noopener noreferrer"&gt;Inflection Studio&lt;/a&gt;. And if there’s one thing I’ve learned, it’s this: a website is never really finished. It’s a living reflection of the business — and when it’s done right, it doesn’t just tell your story. It moves it forward.&lt;/p&gt;

</description>
      <category>career</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Execution Is the Real Differentiator — and the Multiplier</title>
      <dc:creator>Lewis kori</dc:creator>
      <pubDate>Tue, 26 Aug 2025 16:00:05 +0000</pubDate>
      <link>https://forem.com/lewiskori/execution-is-the-real-differentiator-and-the-multiplier-4d9e</link>
      <guid>https://forem.com/lewiskori/execution-is-the-real-differentiator-and-the-multiplier-4d9e</guid>
      <description>&lt;p&gt;People talk a lot about big ideas, but the thing that actually changes outcomes is much smaller: showing up when you said you would. Whether it’s a 6 a.m. run or a product launch, execution is what earns trust and trust is what compounds.&lt;/p&gt;

&lt;p&gt;A few months ago, I invested in a gym. One of my goals was to build a sense of community, so we started a Saturday morning running club. Six a.m., every week.&lt;/p&gt;

&lt;p&gt;I’ve been a runner for years, but lately I’d been letting “too tired” and “too busy” win. The running club changed that. When you’ve promised people you’ll be there, you show up — even if your bed feels like it has diplomatic immunity. And when you do, you realise the showing up &lt;em&gt;is&lt;/em&gt; the work. The enthusiasm fluctuates, sure, but that’s part of the game. Week by week, more people join. The momentum builds.&lt;/p&gt;

&lt;p&gt;That same principle is at the core of my work as a software engineer. In tech, execution isn’t just about ticking boxes. It’s about delivering on what you said you would, because trust is currency. And when things don’t go to plan (which they won’t, often spectacularly), the next best thing to delivering is communicating early. Silence erodes trust faster than failure.&lt;/p&gt;

&lt;p&gt;In both running and work, follow-through compounds. Each time you do what you said you’d do — or own up when you can’t — you build a reputation that opens doors. That reputation doesn’t come from big heroic sprints. It comes from the quiet, boring, unglamorous act of showing up.&lt;/p&gt;

&lt;p&gt;The Saturday club has reclaimed my weekends. It’s also reminded me that whether you’re trying to run five kilometres or ship a feature, success multiplies when you close the gap between what you promise and what you deliver.&lt;/p&gt;

</description>
      <category>career</category>
      <category>beginners</category>
    </item>
    <item>
      <title>The Quiet Power of Starting From Scratch</title>
      <dc:creator>Lewis kori</dc:creator>
      <pubDate>Mon, 16 Jun 2025 11:30:00 +0000</pubDate>
      <link>https://forem.com/lewiskori/the-quiet-power-of-starting-from-scratch-349k</link>
      <guid>https://forem.com/lewiskori/the-quiet-power-of-starting-from-scratch-349k</guid>
      <description>&lt;p&gt;A few Sundays ago, I found myself on the rooftop of a mall restaurant, catching up with friends over a lazy lunch. As we laughed and lingered, my attention kept drifting to an elderly man seated alone a few tables away. He wasn’t eating—he was sketching. Head bowed slightly, pen gliding over paper with a quiet confidence, he was fully immersed in his work.&lt;/p&gt;

&lt;p&gt;Curiosity got the better of me. After my meal, I walked over and introduced myself. I told him I used to draw and that I’ve recently been trying to rediscover that part of myself. He smiled and told me he only started drawing four years ago—self-taught through YouTube. What started as curiosity became a daily practice, nudged by online art communities. Now, he keeps a visual diary: drawings and notes documenting his travels and thoughts, all created for his daughter to someday read.&lt;/p&gt;

&lt;p&gt;What struck me most wasn’t the beauty of his sketches (though they were beautiful). It was his boldness to begin something new at his age, without shame or hesitation.&lt;/p&gt;

&lt;p&gt;That stuck with me. Because I believe one of the most underrated superpowers in life is the willingness to look foolish when you're starting out.&lt;/p&gt;

&lt;p&gt;I work in tech, where nothing stands still. Tools change. Frameworks evolve. Entire paradigms shift overnight. You fall behind if you’re too proud to ask questions or afraid to look like a beginner. The people I admire most in this space are the ones who keep learning—who dive into new challenges headfirst, even if they’re clumsy at first.&lt;/p&gt;

&lt;p&gt;That’s been my approach too. Whether it’s exploring new tools, starting a creative hobby, or just asking “dumb” questions in a meeting, I’ve learned to lean into the discomfort of not knowing. Because on the other side of that discomfort is growth.&lt;/p&gt;

&lt;p&gt;So here’s to starting small, to asking questions, and to not pretending you have it all figured out. Whether you’re writing your first line of code or sketching your first tree, stay curious. Stay a beginner.&lt;/p&gt;

&lt;p&gt;It’s how we move forward.&lt;/p&gt;

</description>
      <category>career</category>
      <category>psychology</category>
      <category>tutorial</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Why I Haven’t Blogged in Years (and How I’m Overcoming the Fear)</title>
      <dc:creator>Lewis kori</dc:creator>
      <pubDate>Sun, 23 Mar 2025 12:00:00 +0000</pubDate>
      <link>https://forem.com/lewiskori/why-i-havent-blogged-in-years-and-how-im-overcoming-the-fear-31fa</link>
      <guid>https://forem.com/lewiskori/why-i-havent-blogged-in-years-and-how-im-overcoming-the-fear-31fa</guid>
      <description>&lt;p&gt;It’s been over three years since I last wrote on my engineering blog. Three years of growth, new experiences, and lessons learned, but also three years of silence. Not because I had nothing to say but because every time I thought about writing, an invisible wall of anxiety and self-doubt stopped me.&lt;/p&gt;

&lt;p&gt;I’ve grown greatly as an engineer. Tackling big projects, learning new technologies, and advancing in my career, but putting those learnings into words? That felt terrifying. I want to share why I’ve been so reluctant to write, in the hope that being open about it will resonate with others who face the same hesitations. This is a raw look at the mix of &lt;em&gt;imposter syndrome&lt;/em&gt;, fear of failure, and perfectionism that held me back, and how I’m finally learning to move past these blocks. By reflecting on this struggle and the psychology behind it, I hope to reclaim my voice and help you do the same if you’ve felt similarly stuck.&lt;/p&gt;

&lt;h2&gt;
  
  
  Imposter Syndrome: The Voice That Says “You’re Not Enough”
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://stackoverflow.blog/2023/09/11/what-we-talk-about-when-we-talk-about-imposter-syndrome/" rel="noopener noreferrer"&gt;Impostor syndrome&lt;/a&gt; — doubting your abilities to the point where you feel like a fraud — has been my constant companion whenever I considered blogging. Despite my accomplishments, I’d hear a nagging voice: &lt;em&gt;"What right do I have to write about this? Surely everyone else knows this stuff. Someone more qualified should be doing this, not me."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I’m &lt;a href="https://product.hubspot.com/blog/engineering-challenge-impostor-syndrome#:~:text=That%27s%20an%20insane%20amount%20of,to%20time%20was%20actually%20%E2%80%9Cnormal%E2%80%9D" rel="noopener noreferrer"&gt;not alone in this feeling&lt;/a&gt;. In fact, &lt;strong&gt;88% of developers&lt;/strong&gt; said they’ve experienced imposter syndrome, including many with 10+ years of experience. Psychologists define imposter syndrome as an internalized fear of being exposed as a “fraud” despite evidence of your success. It often leads you to attribute your achievements to luck or others, and to &lt;a href="https://www.visualcapitalist.com/are-you-suffering-from-impostor-syndrome/#:~:text=People%20suffering%20from%20impostor%20syndrome,to%20luck%20or%20good%20fortune" rel="noopener noreferrer"&gt;downplay your expertise&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The irony is that I was robbing myself of the chance to contribute by giving in to these thoughts. I’ve learned that &lt;em&gt;almost everyone&lt;/em&gt; who creates something faces this imposter voice at some point. Realizing how common these feelings are has been a crucial first step in stopping believing my imposter's voice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fear of Failure and Judgment: When Sharing Feels Risky
&lt;/h2&gt;

&lt;p&gt;Closely tied to imposter syndrome was a fear of failure and being judged. Hitting “Publish” on a blog post felt like standing on a stage in a spotlight, inviting the world to critique me. What if I explained something wrong and got called out? What if my ideas were dismissed or ridiculed?&lt;/p&gt;

&lt;p&gt;The fear of negative feedback or public mistakes can be paralyzing. Psychologically, this is rooted in a very normal desire to be accepted and competent. We dread doing something that might make others lose respect for us.&lt;/p&gt;

&lt;p&gt;I felt this as a &lt;em&gt;fear of visibility&lt;/em&gt;: I couldn't hide my imperfections if I put my thoughts out there. Over time, I’ve learned that most readers are not sitting there waiting to pounce on my mistakes. Many are probably appreciative of finding honest, human voices. And those who would judge harshly? They’re often not in the arena themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Perfectionism Trap: Waiting for the “Perfect” Post
&lt;/h2&gt;

&lt;p&gt;Perhaps the most sneaky barrier of all has been &lt;strong&gt;perfectionism&lt;/strong&gt;. I convinced myself that if I was going to write at all, it had to be &lt;em&gt;flawless&lt;/em&gt;. In theory, having high standards sounds like a good thing. In reality, my perfectionism became a clever form of procrastination.&lt;/p&gt;

&lt;p&gt;I would start drafts and then abandon them because they weren’t turning out as comprehensive or insightful as I imagined. I’d endlessly tweak wording, research extra details, or plan huge multi-part tutorials that I never finished. I was waiting for the perfect idea, the perfect mood, the perfect phrasing and as a result, I posted nothing at all.&lt;/p&gt;

&lt;p&gt;Over time I learned an important truth: perfectionism is often a &lt;strong&gt;&lt;a href="https://warriorhabits.com/it-has-to-be-perfect-perfectionism-as-a-form-of-procrastination-and-tips-to-overcome-it/#:~:text=I%20didn%27t%20realize%20it%20at,fear%20of%20failure" rel="noopener noreferrer"&gt;fear of failure in disguise&lt;/a&gt;&lt;/strong&gt;. I’ve started to break this trap by embracing the mantra “&lt;em&gt;done is better than perfect&lt;/em&gt;.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Realizing I’m Not Alone (and Neither Are You)
&lt;/h2&gt;

&lt;p&gt;For a long time, I thought my reluctance to write was a personal quirk or weakness. But I’ve since learned these feelings are extremely common, especially in the tech community. Knowing this has made a huge difference.&lt;/p&gt;

&lt;p&gt;Hearing others open up about their fears has been inspiring and instructive for me. One developer wrote candidly: &lt;em&gt;“I rarely blog about technical topics... The prospect has always scared me,”&lt;/em&gt; listing thoughts like &lt;em&gt;“major case of Imposter Syndrome”&lt;/em&gt; and &lt;em&gt;“What if I’m wrong?!”&lt;/em&gt; Her solution was to confront those fears by setting a modest goal (one blog post a month) and adopting a growth mindset.&lt;/p&gt;

&lt;p&gt;That reframed writing from a performance to &lt;strong&gt;an act of service&lt;/strong&gt;. Similarly, another engineer admitted he didn’t feel “qualified” to write tutorials until he realized that teaching others also reinforced his own knowledge — the &lt;em&gt;fastest way to learn is in public&lt;/em&gt;. These insights started chipping away at the pedestal I’d put writing on.&lt;/p&gt;

&lt;p&gt;It doesn’t have to be perfect or completely original; it just has to be &lt;em&gt;useful&lt;/em&gt; or &lt;em&gt;authentic&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I’m Starting to Write Again: Mindset Shifts
&lt;/h2&gt;

&lt;p&gt;Identifying these mental blocks is important, but the real question is: how do we move past them? Here are a few shifts that have helped me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Embrace continuous learning:&lt;/strong&gt; You don’t need to be an expert. Share what you’re learning as you go.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remember the value of your perspective:&lt;/strong&gt; Your unique experience matters and your way of explaining something might help someone else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shift from performance to service:&lt;/strong&gt; Focus on helping just &lt;em&gt;one&lt;/em&gt; person.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accept imperfection as the price of growth:&lt;/strong&gt; “B+” work shared is more impactful than “A+” work never published.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Strategies for Consistent, Fearless Writing
&lt;/h2&gt;

&lt;p&gt;Some practical tips that I’ve started using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Start small:&lt;/strong&gt; Set a manageable writing cadence (e.g., one short monthly post).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep an idea list:&lt;/strong&gt; Capture even the smallest insights or bugs you solved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time-box writing:&lt;/strong&gt; Separate drafting from editing to reduce overthinking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use templates or frameworks:&lt;/strong&gt; Scaffolding makes writing easier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Share drafts with friends:&lt;/strong&gt; Safe feedback helps you feel supported.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Celebrate small wins:&lt;/strong&gt; Reward yourself for hitting "Publish."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconnect with your “why” regularly:&lt;/strong&gt; Remember why you wanted to share in the first place.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion: From Self-Doubt to Sharing
&lt;/h2&gt;

&lt;p&gt;Writing this post is my way of breaking a 3 years-long silence. By understanding the psychology behind my imposter syndrome, fear of failure, and perfectionism, I’ve begun to take away their power.&lt;/p&gt;

&lt;p&gt;No, I haven’t vanquished my self-doubt completely, and maybe I never will, but I’m learning to act &lt;em&gt;despite&lt;/em&gt; it. Each new post I share is a step toward reclaiming my confidence and authentic voice.&lt;/p&gt;

&lt;p&gt;If you’ve struggled with similar blocks, know you’re not alone. Your insights have value. The community benefits when more real, diverse voices contribute. I’m finally adding mine, and I hope you’ll add yours too.&lt;/p&gt;

&lt;p&gt;After all, the second best time to start writing is right now.&lt;/p&gt;

</description>
      <category>writing</category>
      <category>career</category>
      <category>psychology</category>
    </item>
    <item>
      <title>Tools to supercharge work productivity</title>
      <dc:creator>Lewis kori</dc:creator>
      <pubDate>Mon, 21 Mar 2022 16:39:22 +0000</pubDate>
      <link>https://forem.com/lewiskori/tools-to-supercharge-work-productivity-29ji</link>
      <guid>https://forem.com/lewiskori/tools-to-supercharge-work-productivity-29ji</guid>
      <description>&lt;p&gt;Freelancing is becoming a popular trend in the modern work environment. A lot of people go into freelance expecting smooth sailing. The dream is to choose their projects and do what they love when they want to. Unshackled from bureaucratic corporations. That’s the whole point of freelancing right? Well true, but in a lot of ways, it’s like starting your own business. This brings a raft of challenges and nobody prepares you for the multitude of admin tasks ahead. If not careful, this can hinder you from focusing on what you love and consequently, interfere with your productivity.&lt;/p&gt;

&lt;p&gt;Keeping organized, staying on top of deadlines, bringing new clients, creating contracts, and chasing invoices to ensure that you get paid on time are just some of the things that you have to do by yourself. Fortunately, there’s a wide range of digital tools to help you complete these important tasks with ease. I work as a software engineer in fintech and here are some game-changers for me when it comes to performance, dedication, and smart use of work time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonsai: Contracts and Invoicing
&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.amazonaws.com%2Fuploads%2Farticles%2Fuvk61o4d1xxex8igdkar.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%2Fuvk61o4d1xxex8igdkar.png" alt="bonsai.png" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.hellobonsai.com/?fp_ref=lewis45" rel="noopener noreferrer"&gt;Bonsai&lt;/a&gt; is an all-in-one tool for digital freelancers and self-employed professionals to manage their work and money. Bonsai has everything you need, from proposals and contracts to invoicing and online payments to client CRM and forms to accounting and tax tools. Since everything is in one app, you can save time and money and stay even more organized.&lt;/p&gt;

&lt;p&gt;One thing I really like with Bonsai is the seamless integration with contracts and invoices. With this, clients are sent invoices automatically with regular payment reminders. You can connect this with a payment processor of your choice such as stripe, PayPal and their platform will automatically flag an invoice as paid and stop the reminders.&lt;/p&gt;

&lt;p&gt;Some other benefits I’ve got from Bonsai are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keeping organized by keeping all my projects in one place.&lt;/li&gt;
&lt;li&gt;Time-saving as I can utilize their beautiful and intuitive proposal maker.&lt;/li&gt;
&lt;li&gt;Built a professional image due to the seamless and easy integration between bonsai apps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this sounds appealing to you, they have a &lt;a href="https://www.hellobonsai.com/pricing?fp_ref=lewis45" rel="noopener noreferrer"&gt;14-day free trial&lt;/a&gt; and two other plans. Workflow for $19 per month which should suffice for most individuals and workflow plus for $29 per month. Should you decide to give Bonsai a trial, sign up using &lt;a href="https://www.hellobonsai.com/invite?fp_ref=lewis45" rel="noopener noreferrer"&gt;my referral link&lt;/a&gt; to support my work.  &lt;/p&gt;

&lt;h2&gt;
  
  
  Wise: Payments
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://wise.com" rel="noopener noreferrer"&gt;Wise&lt;/a&gt;, formerly Transferwise, is an international money transfer service. Wise has been a true game-changer in cross-border money transfer and I cannot recommend it enough. Wise essentially allows you to have a bank account in your client’s country and they can easily send money to that account as they normally would to any other local account. Once you have these funds in your wise account, you can easily convert that balance to your local currency and deposit it into your local bank account. Their exchange rate(a mid-market rate) is often better than the bank’s exchange rate so you get your funds at a premium 😃.&lt;/p&gt;

&lt;p&gt;Receiving funds is free as of now so you should receive the entire amount in your invoices. They make money by taking a small percentage of the transaction from the sender.&lt;/p&gt;

&lt;p&gt;Using &lt;a href="https://wise.prf.hn/click/camref:1100l7sx3" rel="noopener noreferrer"&gt;my invite link&lt;/a&gt; to join wise, you’ll get a free transfer of up to 500 GBP! How awesome is that?&lt;/p&gt;

&lt;h2&gt;
  
  
  Grammarly: Checking writing errors
&lt;/h2&gt;

&lt;p&gt;It should come as no surprise that in business, you need impeccable communication skills. Having well-crafted sentences with minimal errors can do wonders for your brand. No matter how much of a pro you are, we all make mistakes and that’s where &lt;a href="https://www.grammarly.com/" rel="noopener noreferrer"&gt;Grammarly&lt;/a&gt; comes in. I use this tool to scan my writing for grammatical errors and tonal inference; emails, blog posts. Almost everything! They have a browser extension compatible with almost all popular browsers. The best thing is that their free plan is enough for most people!&lt;/p&gt;

&lt;h2&gt;
  
  
  Reclaim: Time management and scheduling
&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.amazonaws.com%2Fuploads%2Farticles%2Feekjyfbghg01y3y1efyr.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%2Feekjyfbghg01y3y1efyr.png" alt="reclaim.png" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://reclaim.ai" rel="noopener noreferrer"&gt;Reclaim&lt;/a&gt; is a smart calendar assistant that uses flexible time blocking for your calendar to make time for your important priorities, routines and tasks. If you’re competing with 50–60% of your calendar being taken up by meetings, or if you’re constantly having to context switch from one thing to another throughout the day, Reclaim is for you.&lt;/p&gt;

&lt;p&gt;With reclaim, I can easily sync my work and personal calendar to avoid overbookings. One thing I really use this for is my workout time and lunch breaks. With reclaim, I can set these as high priority so anyone trying to book my time within those time blocks gets instantly notified that they’d have to reschedule. Their AI is smart enough to auto-schedule my low priority tasks to a later free time-block should anyone book a high priority meeting within a time-block allocated a less priority task. I'll get a notification of this re-scheduling. Here’s a &lt;a href="https://reclaim.ai/blog/10-google-calendar-issues-you-didnt-know-you-had" rel="noopener noreferrer"&gt;more detailed article&lt;/a&gt; of their value proposition.&lt;/p&gt;

&lt;p&gt;Reclaim is entirely free for now and will continue being so forever. However, they’ll add some premium packages (pro and team) starting March 28th 2022.&lt;/p&gt;

&lt;p&gt;Using &lt;a href="https://reclaim.ai/r/s/r2Cru" rel="noopener noreferrer"&gt;my invite link&lt;/a&gt; to register to reclaim will greatly help support this blog.&lt;/p&gt;

&lt;h2&gt;
  
  
  Asana: project management
&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.amazonaws.com%2Fuploads%2Farticles%2Fno3szd22fzax7fdlxobj.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%2Fno3szd22fzax7fdlxobj.png" alt="asana.png" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I’ve been using Asana for a long time to manage my projects. &lt;a href="https://asana.com/" rel="noopener noreferrer"&gt;Asana&lt;/a&gt; helps me organize and visualize any sort of project that I may be working on.&lt;/p&gt;

&lt;p&gt;While I may not take many projects at a time, it always helps to have structure and I find their kanban boards super appealing and intuitive.&lt;/p&gt;

&lt;p&gt;They have a basic plan which is free and suited for individuals getting started with project management. Check out &lt;a href="https://asana.com/pricing" rel="noopener noreferrer"&gt;their pricing page&lt;/a&gt; for the premium and business plan fees.&lt;/p&gt;

&lt;h3&gt;
  
  
  Coda
&lt;/h3&gt;

&lt;p&gt;Last but not least, it’s important to note that these tools will not necessarily make your business successful as a solo entrepreneur. The most important thing I’ve found is to have a solid reputation, network, work ethic and quality work. These tools enhance your work and bring somewhat of a work-life balance. You can always do without them.&lt;/p&gt;

&lt;p&gt;Know any other cool apps I don’t know about? Let me know in the comments and I’ll include them in this list.&lt;/p&gt;

</description>
      <category>career</category>
      <category>productivity</category>
      <category>freelancing</category>
      <category>consulting</category>
    </item>
    <item>
      <title>Deploying Next.js apps to a VPS using Github actions and Docker</title>
      <dc:creator>Lewis kori</dc:creator>
      <pubDate>Fri, 17 Dec 2021 09:18:54 +0000</pubDate>
      <link>https://forem.com/lewiskori/deploying-nextjs-apps-to-a-vps-using-github-actions-and-docker-564n</link>
      <guid>https://forem.com/lewiskori/deploying-nextjs-apps-to-a-vps-using-github-actions-and-docker-564n</guid>
      <description>&lt;p&gt;Recently, I had to deploy a project to a DigitalOcean droplet. One of the features I really wanted for this particular project was a Continuous Delivery pipeline.&lt;/p&gt;

&lt;p&gt;The continuous delivery website defines this as&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;the ability to get changes of all types—including new features, configuration changes, bug fixes and experiments—into production, or into the hands of users, safely and quickly in a sustainable way.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The goal is to make deployments—whether of a large-scale distributed system, a complex production environment, an embedded system, or an app—predictable, routine affairs that can be performed on demand.&lt;/p&gt;

&lt;p&gt;For my case I wanted the web app to auto-deploy to the VPS whenever I pushed changes to the main Github branch. This would consequently save a lot of development time in the process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alternative solutions
&lt;/h2&gt;

&lt;p&gt;There are alternative and hustle-free solutions to this such as &lt;a href="https://vercel.com/" rel="noopener noreferrer"&gt;Vercel&lt;/a&gt; and &lt;a href="https://www.digitalocean.com/products/app-platform/" rel="noopener noreferrer"&gt;DigitalOcean app platform&lt;/a&gt;. However one may take my route if:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You want to better understand Github actions&lt;/li&gt;
&lt;li&gt;Learn more about docker&lt;/li&gt;
&lt;li&gt;For Vercel's case, your client or organization may want to keep their apps in a central platform for easier management.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Please note that some of the links below are affiliate links and at no additional cost to you. Know that I only recommend products, tools and learning services I've personally used and believe are genuinely helpful. Most of all, I would never advocate for buying something you can't afford or that you aren't ready to implement.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A &lt;a href="https://github.com" rel="noopener noreferrer"&gt;Github account&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;A virtual private server. I used a DigitalOcean droplet running Ubuntu 20.04 LTS. Sign up with &lt;a href="https://www.digitalocean.com/?refcode=2282403be01f&amp;amp;utm_campaign=Referral_Invite&amp;amp;utm_medium=Referral_Program" rel="noopener noreferrer"&gt;my referral link&lt;/a&gt; and get $100 in credit valid for 60 days.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Create next.js app
&lt;/h2&gt;

&lt;p&gt;We'll use npx to create a standard next.js app&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 meta-news &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;meta-news
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once we're inside the project directory, we'll install a few dependencies for demonstration purposes&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn add @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^4 axios
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll also declare environment variables inside the &lt;code&gt;.env.local&lt;/code&gt; file. We can then reference these variables from our app like so &lt;code&gt;process.env.NEXT_PUBLIC_VARIABLE_NAME&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;NEXT_PUBLIC_BACKEND_URL=http://localhost:8000/api
NEXT_PUBLIC_META_API_KEY=your_api_key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These variables are for demonstration purposes only. So we won't really be referencing them within our app. An example of a place you'd call them is when instantiating an axios instance or setting a google analytics id and you don't want to commit that to the version control system.&lt;/p&gt;

&lt;p&gt;Let's do a quick test run. The app should be running on &lt;code&gt;localhost:3000&lt;/code&gt; if everything is setup properly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Dockerizing the app
&lt;/h2&gt;

&lt;p&gt;Docker is an open-source tool that automates the deployment of an application inside a software container. which are like virtual machines, only more portable, more resource-friendly, and more dependent on the host operating system. for detailed information on the workings of docker, I'd recommend reading &lt;a href="https://dev.to/django_stars/what-is-docker-and-how-to-use-it-with-python-tutorial-87a"&gt;this article&lt;/a&gt; and for those not comfortable reading long posts, &lt;a href="https://www.youtube.com/playlist?list=PLhW3qG5bs-L99pQsZ74f-LC-tOEsBp2rK" rel="noopener noreferrer"&gt;this tutorial series on youtube&lt;/a&gt; was especially useful in introducing me to the concepts of docker.&lt;/p&gt;

&lt;p&gt;We'll add a Dockerfile to the project root by running&lt;br&gt;
&lt;code&gt;touch Dockerfile&lt;/code&gt; within the CLI.&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;# Install dependencies only when needed&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;node:alpine&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&lt;/span&gt;
&lt;span class="c"&gt;# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apk update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; libc6-compat &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apk add git
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package.json yarn.lock ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;yarn &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--immutable&lt;/span&gt;


&lt;span class="c"&gt;# Rebuild the source code only when needed&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;node:alpine&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&lt;/span&gt;
&lt;span class="c"&gt;# add environment variables to client code&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; NEXT_PUBLIC_BACKEND_URL&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; NEXT_PUBLIC_META_API_KEY&lt;/span&gt;


&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; NEXT_PUBLIC_BACKEND_URL=$NEXT_PUBLIC_BACKEND_URL&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; NEXT_PUBLIC_META_API_KEY=$NEXT_PUBLIC_META_API_KEY&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=deps /app/node_modules ./node_modules&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; NODE_ENV=production&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NODE_ENV&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nv"&gt;NODE_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NODE_ENV&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; yarn build

&lt;span class="c"&gt;# Production image, copy all the files and run next&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;node:alpine&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&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;addgroup &lt;span class="nt"&gt;-g&lt;/span&gt; 1001 &lt;span class="nt"&gt;-S&lt;/span&gt; nodejs
&lt;span class="k"&gt;RUN &lt;/span&gt;adduser &lt;span class="nt"&gt;-S&lt;/span&gt; nextjs &lt;span class="nt"&gt;-u&lt;/span&gt; 1001

&lt;span class="c"&gt;# You only need to copy next.config.js if you are NOT using the default configuration. &lt;/span&gt;
&lt;span class="c"&gt;# Copy all necessary files used by nex.config as well otherwise the build will fail&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/next.config.js ./next.config.js&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/public ./public&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder --chown=nextjs:nodejs /app/.next ./.next&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/node_modules ./node_modules&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/package.json ./package.json&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/pages ./pages&lt;/span&gt;

&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; nextjs&lt;/span&gt;

&lt;span class="c"&gt;# Expose&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;

&lt;span class="c"&gt;# Next.js collects completely anonymous telemetry data about general usage.&lt;/span&gt;
&lt;span class="c"&gt;# Learn more here: https://nextjs.org/telemetry&lt;/span&gt;
&lt;span class="c"&gt;# Uncomment the following line in case you want to disable telemetry.&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; NEXT_TELEMETRY_DISABLED 1&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["yarn", "start"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We're running a &lt;a href="https://docs.docker.com/develop/develop-images/multistage-build/" rel="noopener noreferrer"&gt;multi-stage build&lt;/a&gt; for this deployment.&lt;br&gt;
Notice the ARG and ENV keywords? That's how we pass our environment variables to the client code since we won't have access to any &lt;code&gt;.env&lt;/code&gt; files within the container. More on this later.&lt;/p&gt;

&lt;p&gt;We'll then build and tag our image&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;--build-arg&lt;/span&gt; &lt;span class="nv"&gt;NEXT_PUBLIC_BACKEND_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8000/api &lt;span class="nt"&gt;--build-arg&lt;/span&gt; &lt;span class="nv"&gt;NEXT_PUBLIC_META_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your_api_key &lt;span class="nt"&gt;-t&lt;/span&gt; meta-news &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This may take a while depending on your internet connection and hardware specs.&lt;br&gt;
Once everything checks out run the container&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 3000:3000 meta-news
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Launch your browser and your app should be accessible at '&lt;a href="http://localhost:3000" rel="noopener noreferrer"&gt;http://localhost:3000&lt;/a&gt;' 🎉&lt;/p&gt;

&lt;h2&gt;
  
  
  Set up Github actions
&lt;/h2&gt;

&lt;p&gt;GitHub Actions is a continuous integration and continuous delivery (CI/CD) platform that allows you to automate your build, test, and deployment pipeline. You can create workflows that build and test every pull request to your repository, or deploy merged pull requests to production.&lt;/p&gt;

&lt;p&gt;For more about this wonderful platform, head over to their &lt;a href="https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions" rel="noopener noreferrer"&gt;official tutorial page&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We'll create our first workflow by running the following commands in the CLI. You can use the GUI if you aren't comfortable with the command line 🤗.&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;mkdir&lt;/span&gt; .github &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;mkdir&lt;/span&gt; ./github/workflow &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;touch&lt;/span&gt; ./github/workflows/deploy.yml &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; nano ./github/workflows/deploy.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Populate the deploy.yml file with the following values.&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and Deploy&lt;/span&gt;

&lt;span class="c1"&gt;# Controls when the action will run. Triggers the workflow on push or pull request&lt;/span&gt;
&lt;span class="c1"&gt;# events but only for the master branch&lt;/span&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;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;logLevel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Log&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;level'&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;warning'&lt;/span&gt;

&lt;span class="c1"&gt;# A workflow run is made up of one or more jobs that can run sequentially or in parallel&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# This workflow contains a single job called "build"&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# The type of runner that the job will run on&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;container&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node:14&lt;/span&gt;

    &lt;span class="c1"&gt;# Steps represent a sequence of tasks that will be executed as part of the job&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it&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;actions/checkout@v2&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and Publish to Github Packages Registry&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;elgohr/Publish-Docker-Github-Action@master&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;NEXT_PUBLIC_BACKEND_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APP_NEXT_PUBLIC_BACKEND_URL }}&lt;/span&gt;
          &lt;span class="na"&gt;NEXT_PUBLIC_META_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APP_NEXT_PUBLIC_META_API_KEY }}&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my_github_username/my_repository_name/my_image_name&lt;/span&gt;
          &lt;span class="na"&gt;registry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.USERNAME }}&lt;/span&gt;
          &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets. GITHUB_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Dockerfile&lt;/span&gt;
          &lt;span class="na"&gt;buildargs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NEXT_PUBLIC_BACKEND_URL,NEXT_PUBLIC_META_API_KEY&lt;/span&gt;
          &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;latest&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy package to digitalocean&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;appleboy/ssh-action@master&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;GITHUB_USERNAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.USERNAME }}&lt;/span&gt;
          &lt;span class="na"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets. GITHUB_TOKEN }}&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;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DEPLOY_HOST }}&lt;/span&gt;
          &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DEPLOY_PORT }}&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DEPLOY_USER }}&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DEPLOY_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;envs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GITHUB_USERNAME, GITHUB_TOKEN&lt;/span&gt;
          &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;docker login ghcr.io -u $GITHUB_USERNAME -p $GITHUB_TOKEN&lt;/span&gt;
            &lt;span class="s"&gt;docker pull ghcr.io/my_github_username/my_repository_name/my_image_name:latest&lt;/span&gt;
            &lt;span class="s"&gt;docker stop containername&lt;/span&gt;
            &lt;span class="s"&gt;docker system prune -f&lt;/span&gt;
            &lt;span class="s"&gt;docker run --name containername -dit -p 3000:3000 ghcr.io/my_github_username/my_repository_name/my_image_name:latest&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;You may have noticed our actions are very secretive 😂. Worry not, this is deliberately done to protect your sensitive information from prying eyes. They're encrypted environment variables that you(repo owner) creates for a repo that uses Github actions.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;One thing to note is that the &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; secret is &lt;a href="https://docs.github.com/en/actions/security-guides/automatic-token-authentication" rel="noopener noreferrer"&gt;automatically created&lt;/a&gt; for us when running the action.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To create secrets go to your repository &amp;gt; settings &amp;gt; left-sidebar &amp;gt; secrets&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%2Firquu1m5hsxg0lau54y7.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%2Firquu1m5hsxg0lau54y7.png" alt="secrets_creation" width="800" height="78"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For an in-depth walkthrough, see &lt;a href="https://docs.github.com/en/actions/security-guides/encrypted-secrets" rel="noopener noreferrer"&gt;this guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The expected Github secrets are&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;APP_NEXT_PUBLIC_BACKEND_URL - live backend server url
APP_NEXT_PUBLIC_META_API_KEY - prod api key to thirdparty integration
DEPLOY_HOST - IP to Digital Ocean (DO) droplet
DEPLOY_KEY - SSH secret (pbcopy &amp;lt; ~/.ssh/id_rsa) and the public key should be added to `.ssh/authorized_keys` in server
DEPLOY_PORT - SSH port (22)
DEPLOY_USER  - User on droplet
USERNAME - Your Github username
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Lift Off 🚀
&lt;/h3&gt;

&lt;p&gt;Push to the main branch&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add &lt;span class="nt"&gt;-A&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Initial commit"&lt;/span&gt;
git push origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If everything runs as expected, you should see a green checkmark in your repository with the build steps complete.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fts6y2zrz339m3vlenqsj.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%2Fts6y2zrz339m3vlenqsj.png" alt="Github_actions_deploy" width="800" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From there, you can setup a reverse proxy such as nginx within your server and point the host to "&lt;a href="http://localhost:3000" rel="noopener noreferrer"&gt;http://localhost:3000&lt;/a&gt;".&lt;/p&gt;

&lt;p&gt;Yay!🥳 we have successfully created a continuous delivery pipeline and hopefully, now you'll concentrate on code instead of infrastructure.&lt;/p&gt;

&lt;p&gt;Should you have any questions, please do not hesitate to reach out to me on &lt;a href="https://twitter.com/lewis_kihiu" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;.&lt;br&gt;
Comment below if you have feedback or additional input.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shameless plug
&lt;/h2&gt;

&lt;p&gt;Do you need to do a lot of data mining?&lt;/p&gt;

&lt;p&gt;Scraper API is a startup specializing in strategies that'll ease the worry of your IP address from being blocked while web scraping.They utilize IP rotation so you can avoid detection. Boasting over 20 million IP addresses and unlimited bandwidth.&lt;/p&gt;

&lt;p&gt;In addition to this, they provide CAPTCHA handling for you as well as enabling a headless browser so that you'll appear to be a real user and not get detected as a web scraper. It has integration for popular platforms such as python ,node.js, bash, PHP and ruby. All you have to do is concatenate your target URL with their API endpoint on the HTTP get request then proceed as you normally would on any web scraper. Don't know how to webscrape? &lt;br&gt;
Don't worry, I've covered that topic extensively on the &lt;a href="https://lewiskori.com/series/web-scraping-techniques-with-python/" rel="noopener noreferrer"&gt;webscraping series&lt;/a&gt;. All entirely free!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.scraperapi.com?fpr=lewiskori" 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%2Fd2gdx5nv84sdx2.cloudfront.net%2Fuploads%2Fssvxh57a%2Fmarketing_asset%2Fbanner%2F2670%2F069-ScraperAPI-GIF-320x50-v1.gif" alt="scraperapi" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Using &lt;a href="https://www.scraperapi.com?via=lewis93" rel="noopener noreferrer"&gt;my scraperapi referrall link&lt;/a&gt; and the promo code lewis10, you'll get a 10% discount on your first purchase!! You can always start on their generous free plan and upgrade when the need arises.&lt;/p&gt;

</description>
      <category>github</category>
      <category>javascript</category>
      <category>nextjs</category>
      <category>todayilearned</category>
    </item>
    <item>
      <title>How to programatically unzip files uploaded to google cloud storage buckets</title>
      <dc:creator>Lewis kori</dc:creator>
      <pubDate>Thu, 29 Jul 2021 12:30:04 +0000</pubDate>
      <link>https://forem.com/lewiskori/how-to-programatically-unzip-files-uploaded-to-google-cloud-storage-buckets-47l5</link>
      <guid>https://forem.com/lewiskori/how-to-programatically-unzip-files-uploaded-to-google-cloud-storage-buckets-47l5</guid>
      <description>&lt;p&gt;I've been using google cloud services over the last year and I thoroughly enjoy their range of services. In this walkthrough, I'll guide you through the process of unzipping files uploaded to Google cloud storage buckets. But first, let me give you some context.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Over the course of this week, I've had the challenge of ensuring that zip files uploaded from a web app interface are immediately unzipped once they're uploaded to a storage service. Zip files are a convenient way of uploading bulk data as they're minified, hence save a lot of bandwidth while they're being uploaded.&lt;/p&gt;

&lt;p&gt;However, you may need to access individual files located in the zip files once uploaded to your asset holding service. In my case, I needed to access a plethora of pdf statements.&lt;/p&gt;

&lt;p&gt;I have been using several services in the google cloud suite in conjunction with &lt;a href="https://cloud.google.com/storage" rel="noopener noreferrer"&gt;google cloud storage&lt;/a&gt;&lt;br&gt;
These are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://cloud.google.com/run" rel="noopener noreferrer"&gt;Google cloud run&lt;/a&gt; - their managed serverless allowing developers to develop and deploy highly scalable containerized applications.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cloud.google.com/secret-manager" rel="noopener noreferrer"&gt;Google cloud secret manager&lt;/a&gt; - Stores API keys, passwords, certificates, and other sensitive data.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cloud.google.com/products/databases" rel="noopener noreferrer"&gt;Google cloud databases&lt;/a&gt; mainly Postgres&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Having come from using traditional Virtual machines (VMs), I had to change my mindset in regards to hosting each service in the same machine. Having offloaded the storage of objects to the GS bucket, I simply couldn't traverse the local file system and unzip the files directly. After a lot of pain and pulling hair 😂, I finally managed to accomplish this seemingly impossible task.&lt;/p&gt;

&lt;p&gt;Let's get to it.&lt;/p&gt;
&lt;h2&gt;
  
  
  The solution
&lt;/h2&gt;

&lt;p&gt;For this walkthrough, we'll write some python scripts that'll automate the unzipping process.&lt;/p&gt;

&lt;p&gt;If you're not familiar with google cloud services, &lt;a href="https://cloud.google.com/docs" rel="noopener noreferrer"&gt;this tutorial&lt;/a&gt; will help you get started with the basics. In case you're feeling extra curious, here's &lt;a href="https://codelabs.developers.google.com/codelabs/cloud-run-django/#0" rel="noopener noreferrer"&gt;a complete guide to deploy a django app to cloud run&lt;/a&gt;. This will equip you with the knowledge and hands-on experience of most of the services mentioned above.&lt;/p&gt;
&lt;h3&gt;
  
  
  Project set up
&lt;/h3&gt;

&lt;p&gt;Since this is a python project, it's recommended to use virtual environments to manage and separate dependencies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;virtualenv unzipperEnv &lt;span class="nt"&gt;-p&lt;/span&gt; python3.9 // create virtualenv
&lt;span class="nb"&gt;source &lt;/span&gt;unzipperEnv/bin/activate // activate virtualenv

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

&lt;/div&gt;



&lt;p&gt;Once activated, we'll have to install a few libraries to interact with the google cloud suite.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://googleapis.dev/python/storage/latest/index.html" rel="noopener noreferrer"&gt;google-cloud-storage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://google-auth.readthedocs.io/en/master/" rel="noopener noreferrer"&gt;google-auth&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://django-storages.readthedocs.io/en/latest/" rel="noopener noreferrer"&gt;django-storages&lt;/a&gt; - just in case you are using Django. We won't necessarily need this here but it's highly useful in a production app.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;google-auth google-cloud-storage

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

&lt;/div&gt;



&lt;p&gt;Let's jump right to it by creating a directory to house our project. I'll be using Unix commands but you can use the UI or PowerShell in case you're on windows.&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;mkdir &lt;/span&gt;unzipper &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;unzipper // create and enter a directory
&lt;span class="nb"&gt;touch &lt;/span&gt;storages.py // create a script to house our code
code &lt;span class="nb"&gt;.&lt;/span&gt; // open code editor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Great, so far, so good. We'll jump right to the crux of our walkthrough.&lt;/p&gt;

&lt;h2&gt;
  
  
  working with google storage buckets
&lt;/h2&gt;

&lt;p&gt;Before getting started, we'll need to create a google cloud storage bucket. I won't cover that today as it has been widely &lt;a href="https://cloud.google.com/storage/docs/cloud-console#_creatingbuckets" rel="noopener noreferrer"&gt;documented in their official docs&lt;/a&gt;.&lt;br&gt;
Once, you've created a bucket, you'll get a bucket id, we'll need that going forward.&lt;/p&gt;

&lt;p&gt;In the process of creating the bucket, ensure you get a service account and download the JSON file provided by Google.&lt;br&gt;
This will help us authenticated the requests while testing locally on our machines. I've saved mine as &lt;code&gt;credentials.json&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;zipfile&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ZipFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_zipfile&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.cloud&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.oauth2&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;service_account&lt;/span&gt;

&lt;span class="c1"&gt;# declare unzipping function
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;zipextract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;zipfilename_with_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="c1"&gt;# auth config
&lt;/span&gt;    &lt;span class="n"&gt;SERVICE_ACCOUNT_FILE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;credentials.json&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="n"&gt;credentials&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;service_account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_service_account_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;SERVICE_ACCOUNT_FILE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;bucketname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;your-bucket-id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

    &lt;span class="n"&gt;storage_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;storage_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bucketname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;destination_blob_pathname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;zipfilename_with_path&lt;/span&gt;

    &lt;span class="n"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;destination_blob_pathname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;zipbytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BytesIO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;download_as_string&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;is_zipfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;zipbytes&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;ZipFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;zipbytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;myzip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;contentfilename&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;myzip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;namelist&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                &lt;span class="n"&gt;contentfile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;myzip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;contentfilename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                &lt;span class="c1"&gt;# unzip pdf files only, leave out if you don't need this.
&lt;/span&gt;                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.pdf&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;contentfilename&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;casefold&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;

                    &lt;span class="n"&gt;output_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;./&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;contentfilename&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
                    &lt;span class="n"&gt;outfile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;outfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;contentfile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;outfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

                    &lt;span class="n"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;zipfilename_with_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.zip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;contentfilename&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
                    &lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;my_pdf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="n"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upload_from_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;my_pdf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                    &lt;span class="c1"&gt;# make the file publicly accessible
&lt;/span&gt;                    &lt;span class="n"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;make_public&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;done running function&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;zipfilename_with_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;enter the zipfile path: &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;zipextract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;zipfilename_with_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Looking at the code above, what we're doing is declaring a function that takes in the zipfile location within our bucket. This can be &lt;code&gt;documents/reports/2021/January.zip&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;In order to access our storage buckets in this script, we have to authenticate the request through the &lt;code&gt;credentials.json&lt;/code&gt; (service account details). Under the hood, the google cloud libraries use the requests module.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://googleapis.dev/python/storage/latest/blobs.html" rel="noopener noreferrer"&gt;Blobs&lt;/a&gt; are google cloud's concept of an object. This path to the object(zip) is what we provide to our function. From there, we download the zip file as a string and convert its representation to bytes through the io standard python library.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;is_zipfile&lt;/code&gt; checks our byte representation of the zip file to ensure what we want to unzip is an actual zip file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;ZipFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;zipbytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;myzip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;contentfilename&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;myzip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;namelist&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                &lt;span class="n"&gt;contentfile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;myzip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;contentfilename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;From the snippet above, we'll be reading in the zipbytes and loop through its content.&lt;/p&gt;

&lt;p&gt;For my use case, I wanted to unzip pdf files, but you can be creative with this. I used the pattern in the name, checking if there are filenames with &lt;code&gt;.pdf&lt;/code&gt; extension. It's probably not the best method as the pdf may not be accurate. &lt;a href="https://stackoverflow.com/questions/6186980/determine-if-a-byte-is-a-pdf-file" rel="noopener noreferrer"&gt;This StackOverflow question&lt;/a&gt; offers some interesting solutions. In my case, however, I was content with the workaround.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.pdf&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;contentfilename&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;casefold&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;

                    &lt;span class="n"&gt;output_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;./&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;contentfilename&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
                    &lt;span class="n"&gt;outfile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;outfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;contentfile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;outfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

                    &lt;span class="n"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;zipfilename_with_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.zip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;contentfilename&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
                    &lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;my_pdf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="n"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upload_from_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;my_pdf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                    &lt;span class="c1"&gt;# make the file publicly accessible
&lt;/span&gt;                    &lt;span class="n"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;make_public&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;We then extract the bytes and write them to a file within our project directory with the same name as what's available in the bucket. Immediately after, we take this file and upload it to the newly extracted zip folder. The &lt;code&gt;rstrip()&lt;/code&gt; function ensures the extracted folder doesn't have &lt;code&gt;.zip&lt;/code&gt; extension in its name.&lt;/p&gt;

&lt;p&gt;Finally, we ensure the newly extracted files are publicly accessible via URL so that users can easily download them from an app or website interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  conclusion and gotchas
&lt;/h2&gt;

&lt;p&gt;The snippet above is very useful when testing on your own machine, however, if your app is running in any of google's serverless environments, namely, app engine and cloud run, it's recommended to write files to a directory named &lt;code&gt;/tmp&lt;/code&gt;.&lt;br&gt;
All files in this directory are stored in the instance's RAM, therefore writing to &lt;code&gt;/tmp&lt;/code&gt; takes up system memory. In addition, files in the &lt;code&gt;/tmp&lt;/code&gt; directory are only available to the app instance that created the files. When the instance is deleted, the temporary files are deleted. This will ensure the files we're writing and re-uploading are deleted as soon as possible since we no longer need them.&lt;/p&gt;

&lt;p&gt;We'll modify our script slighty to accomodate this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="c1"&gt;# change path here 👇🏽
&lt;/span&gt;    &lt;span class="n"&gt;output_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/tmp/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;contentfilename&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
                    &lt;span class="n"&gt;outfile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;outfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;contentfile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;outfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;additionally, we won't be needing the service account credentials in that environment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
this line changes and there&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s no need for auth module imports
storage_client = storage.Client(credentials=credentials)
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="c1"&gt;# no credential requirements
&lt;/span&gt;&lt;span class="n"&gt;storage_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Thanks for your time, if you want more of this, &lt;a href="https://mailchi.mp/c42286076bd8/lewiskori" rel="noopener noreferrer"&gt;subscribe to my newsletter&lt;/a&gt; to get notified whenever I make new posts.&lt;/p&gt;

&lt;p&gt;That's it from me.&lt;/p&gt;

&lt;p&gt;If you have any questions or issues, leave a comment below or contact me via &lt;a href="https://twitter.com/lewis_kihiu" rel="noopener noreferrer"&gt;twitter&lt;/a&gt; and I'll get to you as soon as I can.&lt;/p&gt;

</description>
      <category>cloudskills</category>
      <category>googlecloud</category>
      <category>python</category>
      <category>todayilearned</category>
    </item>
  </channel>
</rss>
