<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Manjula Liyanage on Medium]]></title>
        <description><![CDATA[Stories by Manjula Liyanage on Medium]]></description>
        <link>https://medium.com/@mliyanage?source=rss-176ddb7f2678------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/2*WboEP7Zowx-mb079WUj0GQ.png</url>
            <title>Stories by Manjula Liyanage on Medium</title>
            <link>https://medium.com/@mliyanage?source=rss-176ddb7f2678------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Thu, 11 Jun 2026 08:21:40 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@mliyanage/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Don’t Outsource Your Intuition to AI]]></title>
            <link>https://mliyanage.medium.com/dont-outsource-your-intuition-to-ai-49bf8e5ab6e1?source=rss-176ddb7f2678------2</link>
            <guid isPermaLink="false">https://medium.com/p/49bf8e5ab6e1</guid>
            <category><![CDATA[entrepreneurship]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[personal-development]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <dc:creator><![CDATA[Manjula Liyanage]]></dc:creator>
            <pubDate>Fri, 10 Apr 2026 19:15:51 GMT</pubDate>
            <atom:updated>2026-04-10T19:28:49.549Z</atom:updated>
            <content:encoded><![CDATA[<p>I’ve been thinking about this lately.</p><p>Human intuition is one of the oldest tools we have. Before books, before systems, before frameworks — we had instinct. It helped us survive. Knowing when something feels off. Making quick decisions without overthinking. That was an evolutionary advantage.</p><p>In today’s world, we still use intuition every day. Sometimes you just <em>know</em> what to do. You can’t fully explain it, but your judgment feels right. That’s experience compressed into a feeling.</p><p>But not all intuition is useful anymore.</p><p>Some of it is still wired for survival in the savanna, not for modern life. We overreact, we fear things that are not real threats, we make biased decisions. So intuition is powerful, but not always correct. We need to know when to trust it — and when to pause and think properly.</p><p>Tools like ChatGPT are everywhere. And honestly, they are incredible. You can ask anything — work problems, life decisions, technical questions, even emotional stuff.</p><p>But I’m starting to see a pattern.</p><p>We ask AI for everything.</p><p>Instead of deciding, we validate.<br>Instead of acting, we research more.<br>Instead of trusting ourselves, we double-check with AI.</p><p>And slowly, we get stuck.</p><p>We feel like we are being smart — gathering more data, more opinions — but sometimes we are just avoiding the decision. Avoiding responsibility. Avoiding the discomfort of being wrong.</p><p>That’s where intuition still matters.</p><p>Not as the only tool — but as one of the tools.</p><p>Use AI when you need knowledge, perspective, or structure. But don’t forget — you already have something powerful inside you. Your intuition is built from your experiences, your failures, your wins.</p><p>Sometimes, it’s enough.</p><p>So maybe the goal is not choosing between AI and intuition.</p><p>It’s knowing when to switch.</p><p>Sometimes you ask AI.<br>Sometimes you trust yourself.</p><p>And sometimes — you just move.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=49bf8e5ab6e1" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Forget Courses. Start Skillmaxxing.]]></title>
            <link>https://mliyanage.medium.com/forget-courses-start-skillmaxxing-15bd8fd8b760?source=rss-176ddb7f2678------2</link>
            <guid isPermaLink="false">https://medium.com/p/15bd8fd8b760</guid>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[productivity]]></category>
            <category><![CDATA[self-improvement]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[startup]]></category>
            <dc:creator><![CDATA[Manjula Liyanage]]></dc:creator>
            <pubDate>Fri, 20 Mar 2026 23:54:44 GMT</pubDate>
            <atom:updated>2026-03-20T23:54:44.504Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SnmSwOouSux6bwmTI2umvQ.png" /><figcaption>Skillmaxxing</figcaption></figure><p>I see this pattern again and again.</p><p>People want to learn tech.<br> They start with a course.</p><p>They open Udemy, YouTube, maybe even Coursera.<br>Watch few videos. Feel productive. Take some notes.</p><p>But after few weeks… nothing.</p><p>No product.<br>No real understanding.<br>Just more “I think I know this”.</p><p>This is the problem.</p><p><strong>Courses make you feel like you’re learning.<br>But building is what actually teaches you.</strong></p><p>That’s where I think this idea of <em>Skillmaxxing</em> fits really well.</p><p>Not learning everything.<br>Not becoming an expert.</p><p>Just going hard on <strong>one skill, with real output</strong>.</p><h3>What is Skillmaxxing (in our world)?</h3><p>For me, Skillmaxxing means:</p><ul><li>Pick one skill (not 10 things)</li><li>Use AI as your assistant</li><li>Learn by building something real</li><li>Repeat until it clicks</li></ul><p>That’s it.</p><p>No long theory.<br>No perfect roadmap.</p><p>Just focused, practical learning.</p><h3>Let’s take a real example: System Design</h3><p>Now this is where many beginners get scared.</p><p>“System design is for senior engineers”<br>“I need years of experience”<br>“I should first learn backend, frontend, databases…”</p><p>No.</p><p>You don’t need to master everything.</p><p>You just need to understand <strong>how things connect</strong>.</p><p>That’s system design.</p><p>Not diagrams.<br>Not fancy words.</p><p>Just answering simple questions:</p><ul><li>Where does my data go?</li><li>How does the user interact with my system?</li><li>What happens when things scale?</li><li>What can break?</li></ul><h3>How to Skillmaxx System Design (with AI)</h3><p>Let’s say you want to build something simple.</p><p>Example:<br> A “founder task tracker” (like a lightweight Jira for founders)</p><p>Now, instead of watching a course, do this:</p><h3>Step 1 — Ask AI to design it</h3><p>Open ChatGPT and ask:</p><blockquote><em>“Design a simple system architecture for a task tracking app for founders. Keep it simple, explain components.”</em></blockquote><p>You’ll get something like:</p><ul><li>Frontend (web app)</li><li>Backend API</li><li>Database</li><li>Authentication</li><li>Notifications</li></ul><p>Good.</p><p>Don’t try to understand everything.</p><p>Just get familiar.</p><h3>Step 2 — Go deeper, one piece at a time</h3><p>Now pick one part.</p><p>For example: <strong>Database</strong></p><p>Ask:</p><blockquote><em>“What database should I use for this and why?”</em></blockquote><p>Then:</p><blockquote><em>“Show me simple schema for tasks”</em></blockquote><p>Then:</p><blockquote><em>“How does this scale if I have 10,000 users?”</em></blockquote><p>You are not memorising.</p><p>You are <strong>exploring</strong>.</p><h3>Step 3 — Draw your own version</h3><p>Now take a paper (or Miro).</p><p>Draw:</p><ul><li>User → Frontend → Backend → Database</li></ul><p>Add arrows.</p><p>Add small notes.</p><p>This step is very important.</p><p>Because now you are thinking, not just reading.</p><h3>Step 4 — Build a tiny version</h3><p>Use any AI coding tool.</p><p>Doesn’t matter if it’s messy.</p><p>Just build:</p><ul><li>Create task</li><li>Save task</li><li>Show task list</li></ul><p>Now suddenly everything becomes real.</p><p>Database is not theory anymore.<br>API is not theory anymore.</p><h3>Step 5 — Break it (this is where real learning happens)</h3><p>Ask:</p><ul><li>What if 1,000 users login at same time?</li><li>What if database is slow?</li><li>What if notification fails?</li></ul><p>Then ask AI:</p><blockquote><em>“How do I improve this system for scale?”</em></blockquote><p>Now you are doing system design.</p><p>Not watching.</p><p>Not memorising.</p><h3>The mindset shift</h3><p>Courses try to give you <strong>complete knowledge</strong>.</p><p>But real skill comes from:</p><ul><li>Partial understanding</li><li>Repeated exposure</li><li>Real problems</li></ul><p>You don’t need to know everything.</p><p>You need to know enough to move forward.</p><p>That’s Skillmaxxing.</p><h3>A Simple 4–8 Week Skillmaxxing Plan (System Design)</h3><p>If you want something practical, follow this.</p><h3>Week 1–2: Foundations (but lightweight)</h3><ul><li>Learn what is frontend, backend, database (use AI explanations)</li><li>Ask AI to show 3–4 simple architectures</li><li>Draw them in your own way</li></ul><h3>Week 3–4: First system</h3><ul><li>Pick one idea (task app, booking system, notification app)</li><li>Ask AI to design it</li><li>Break it into components</li><li>Build a basic version (very simple)</li></ul><h3>Week 5–6: Go deeper</h3><ul><li>Add one feature (auth, notifications, file upload)</li><li>Ask “how this works internally?”</li><li>Learn only what you need</li></ul><h3>Week 7–8: Scale thinking</h3><ul><li>Ask “what happens with 10k users?”</li><li>Learn caching, queues (just basics)</li><li>Improve your design</li></ul><h3>Final thought</h3><p>You don’t become good by finishing courses.</p><p>You become good by:</p><ul><li>Trying</li><li>Breaking</li><li>Fixing</li><li>Repeating</li></ul><p>AI just makes this faster.</p><p>So yeah…</p><p><strong>Forget courses.<br>Start Skillmaxxing.</strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=15bd8fd8b760" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How I Keep My MVP Running for Free by Hopping Cloud Accounts (And Why You Can Too)]]></title>
            <link>https://mliyanage.medium.com/how-i-keep-my-mvp-running-for-free-by-hopping-cloud-accounts-and-why-you-can-too-f2f3b6e66c55?source=rss-176ddb7f2678------2</link>
            <guid isPermaLink="false">https://medium.com/p/f2f3b6e66c55</guid>
            <category><![CDATA[founders]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[fractional-cto]]></category>
            <category><![CDATA[saas]]></category>
            <dc:creator><![CDATA[Manjula Liyanage]]></dc:creator>
            <pubDate>Tue, 27 Jan 2026 12:50:04 GMT</pubDate>
            <atom:updated>2026-01-28T01:36:37.999Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*c3CrAxDOr-r3mieW4x-obw.png" /></figure><p>T<strong>he cloud providers want you to succeed</strong> — so badly that they’ll give you hundreds of dollars in free credits just to try their platform.</p><p>Google Cloud gives you $300 for 90 days. AWS gives you a free tier for 12 months. Azure gives you $200 for 30 days. And nothing is stopping you from creating a new account when those credits run out.</p><p>I’ve been running <a href="https://staging.omnitrackr.dev/">OmniTrackr</a>, a file monitoring and SLA tracking platform, on Google Cloud Platform’s free trial credits. When my first $300 ran out, I didn’t reach for my credit card. I created a new Google account, activated a fresh free trial, and migrated my entire non-production infrastructure over in a single afternoon.</p><p>No data lost. No extended downtime. No panic.</p><p><strong>But here’s the catch:</strong> this only works if you set up your infrastructure the right way from day one. If you hardcoded project IDs, manually configured servers through the console, or stored secrets in your codebase, you’d be looking at days of painful migration instead of hours.</p><p>This post breaks down exactly how I structured my infrastructure to make cloud account hopping trivial, and how you can do the same. Even if you never plan to hop accounts, these practices will save you when you need to spin up a new environment, recover from a disaster, or hand off your project to a real DevOps team.</p><h3><strong>Why This Matters for Bootstrapped Founders</strong></h3><p>Let’s do the math.</p><p>A minimal non-prod environment on GCP with a Cloud SQL database, a Cloud Run service, a load balancer, and some storage costs roughly <strong>$80–100/month</strong>. That’s $960–1200/year before you’ve made a single dollar.</p><p>For a bootstrapped founder validating product-market fit, that’s money better spent on marketing, user research, or keeping the lights on.</p><p>With account hopping, I’ve run my demo environment for months at zero cost. That’s a real runway. And I’m not cutting corners — I’m running a full production-grade setup with:</p><p>- PostgreSQL database with automated backups</p><p>- API service with auto-scaling on Cloud Run</p><p>- Three background worker jobs on scheduled triggers</p><p>- A global HTTPS load balancer with managed SSL</p><p>- Frontend SPA hosting on Cloud Storage</p><p>- CI/CD pipelines deploying automatically on every push</p><p>- Secrets managed through a dedicated secrets manager</p><p>All of it migrated to a new account by changing three values in my Terraform files and running a handful of commands.</p><h3><strong>The Foundation: Infrastructure as Code from Day One</strong></h3><p>The single decision that makes account hopping possible is <strong>**Infrastructure as Code (IaC)**</strong>. If you build your MVP on a cloud platform and you’re clicking around the console to set things up, stop. Go learn the basics of Terraform. It will be the highest-ROI hour you spend on your entire project.</p><p><strong>What Terraform Actually Does</strong></p><p>Terraform lets you describe your entire cloud infrastructure in plain text files. Instead of clicking “Create Database” in the GCP console, you write:</p><pre>resource &quot;google_sql_database_instance&quot; &quot;postgres&quot; {<br>  name             = &quot;omnitrackr-staging-db&quot;<br>  database_version = &quot;POSTGRES_15&quot;<br>  region           = &quot;us-central1&quot;<br><br>  settings {<br>    tier            = &quot;db-f1-micro&quot;<br>    disk_size       = 10<br>    disk_type       = &quot;PD_SSD&quot;<br>    disk_autoresize = true<br>  }<br>}</pre><p>Run `terraform apply`, and the database exists. Run it again on a different account, and an identical database exists there too. Run `terraform destroy`, and it’s all gone — cleanly, completely, no orphaned resources silently burning your credits.</p><p><strong>My Terraform Setup</strong></p><p>Here’s what OmniTrackr’s Terraform configuration manages:</p><pre>infrastructure/terraform/staging/<br>  main.tf              # Provider config, backend state, API enablement<br>  variables.tf         # Parameterized values (project ID, region, etc.)<br>  terraform.tfvars     # Environment-specific values<br>  cloud_sql.tf         # PostgreSQL database<br>  cloud_run.tf         # API service<br>  cloud_run_jobs.tf    # Background workers + Cloud Scheduler triggers<br>  secrets.tf           # Secret Manager (DB password, JWT secret)<br>  iam.tf               # Service accounts and permissions<br>  storage.tf           # Frontend hosting bucket<br>  load_balancer.tf     # HTTPS load balancer with managed SSL<br>  outputs.tf           # Export IPs, URLs, connection strings</pre><p>Every resource is defined in code. Every permission, every firewall rule, every cron schedule. Nothing is manually configured.</p><h3><strong>The Key: Parameterize Everything</strong></h3><p>The magic is in `variables.tf`, `terraform.tfvars`, and a technique called <strong>partial backend configuration</strong>. Every value that could change between accounts is either a variable or a CLI flag — nothing is hardcoded.</p><p>Here’s the catch most people miss: Terraform’s `backend` block <strong>does not support variables</strong>. You can’t write `bucket = var.state_bucket`. So if you hardcode the state bucket name in `main.tf`, switching between projects means editing source files — which is fragile and error-prone.</p><p>The solution is to leave the bucket empty and pass it at init time:</p><pre>```hcl<br># main.tf — partial backend config<br>terraform {<br>  backend &quot;gcs&quot; {<br>    prefix = &quot;staging&quot;<br>    # bucket provided via: terraform init -backend-config=&quot;bucket=BUCKET_NAME&quot;<br>  }<br>}<br>```<br><br>```hcl<br># variables.tf — parameterized project ID<br>variable &quot;project_id&quot; {<br>  description = &quot;GCP Project ID&quot;<br>  default     = &quot;omnitrackr-staging-v2&quot;<br>}<br>```</pre><p>Then keep separate `.tfvars` files for each target:</p><pre>```hcl<br># terraform.tfvars — active project<br>project_id  = &quot;omnitrackr-staging-v2&quot;<br>region      = &quot;us-central1&quot;<br>environment = &quot;staging&quot;<br>```<br><br>```hcl<br># terraform.tfvars.old — old project (kept for cleanup)<br>project_id  = &quot;omnitrackr-staging&quot;<br>region      = &quot;us-central1&quot;<br>environment = &quot;staging&quot;<br>```</pre><p>When I migrate, I create the new `.tfvars`, update the default in `variables.tf`, and pass the new bucket at init:</p><pre>```bash<br>terraform init -backend-config=&quot;bucket=omnitrackr-terraform-state-v2&quot;<br>terraform apply # uses terraform.tfvars by default<br>```</pre><p>Every resource — database, services, load balancer, secrets — gets created in the new project. No manual configuration. And when it’s time to tear down the old project, I don’t edit a single file:</p><pre>```bash<br>terraform init -backend-config=&quot;bucket=omnitrackr-terraform-state&quot; -reconfigure<br>terraform destroy -var-file=&quot;terraform.tfvars.old&quot;<br>```</pre><p>Clean setup, clean teardown, using the same Terraform files throughout.</p><p><strong>Environment Separation Isn’t Optional</strong></p><p>In my previous article about <a href="https://mliyanage.com/5-critical-mistakes-non-technical-founders-make-when-building-mvps-with-ai-and-how-to-avoid-them-5231c25131fc">mistakes non-technical founders make when building with AI</a>, I wrote about Mistake #3: ignoring multi-environment configuration from the start. That lesson is central to what makes account hopping work.</p><p>If your staging and production environments aren’t cleanly separated, migrating one without breaking the other is a nightmare. Here’s what proper separation looks like:</p><p><strong>Separate Everything</strong></p><p><strong>Separate GCP projects:</strong></p><pre>- `omnitrackr-staging` (or `omnitrackr-staging-v2` after migration)<br>- `omnitrackr-prod` (untouched during staging migration)</pre><p><strong>Separate GitHub Actions secrets:</strong></p><pre>- `GCP_PROJECT_ID_STAGING` / `GCP_PROJECT_ID_PROD`<br>- `GCP_SA_KEY_STAGING` / `GCP_SA_KEY_PROD`<br>- `DB_HOST_STAGING` / `DB_HOST_PROD`</pre><p><strong>Separate CI/CD workflows:</strong></p><pre>- `deploy-staging.yml` triggers on push to `develop`<br>- `deploy-prod.yml` triggers on push to `main`</pre><p><strong>Separate Terraform state:</strong></p><pre>- Staging: `terraform init -backend-config=&quot;bucket=omnitrackr-terraform-state-v2&quot;` + `terraform.tfvars`<br>- Production: `terraform init -backend-config=&quot;bucket=omnitrackr-terraform-state-prod&quot;` + `terraform.tfvars.prod`</pre><p>When I migrated staging to a new GCP account, production wasn’t affected at all. Different project, different secrets, different workflows, different state. The two environments have zero coupling.</p><h3><strong>Secrets Management Done Right</strong></h3><p>This is where I see a lot of founders cut corners. They hardcode database passwords in environment files, commit API keys to Git, or share secrets across environments.</p><p>OmniTrackr uses Google Secret Manager for all sensitive values. Terraform creates the secrets automatically:</p><pre>```<br>resource &quot;random_password&quot; &quot;db_password&quot; {<br>  length  = 32<br>  special = true<br>}<br><br>resource &quot;google_secret_manager_secret&quot; &quot;db_password&quot; {<br>  secret_id = &quot;db-password&quot;<br>  replication { auto {} }<br>}<br><br>resource &quot;google_secret_manager_secret_version&quot; &quot;db_password&quot; {<br>  secret      = google_secret_manager_secret.db_password.id<br>  secret_data = random_password.db_password.result<br>}<br>```</pre><p>When I create a new project, Terraform generates <strong>fresh</strong> passwords and secrets. I never copy passwords between accounts. I never store them in files that could leak. The Cloud Run services pull secrets at runtime from Secret Manager with least-privilege IAM permissions.</p><p>The only place secrets live outside GCP is in GitHub Actions secrets — and those are updated manually during migration, one at a time, through the GitHub UI.</p><h3><strong>The Actual Migration: What It Looks Like</strong></h3><p>Here’s the condensed version of what an account hop involves. The full migration plan lives in my <a href="https://github.com/mliyanage/omnitrackr/blob/develop/docs/GCP_STAGING_MIGRATION_PLAN.md">GCP Staging Migration Plan</a>, but this is the essence.</p><p><strong>Step 1: Backup Your Data</strong></p><pre>```<br># Export database<br>pg_dump &quot;postgresql://user:pass@old-host:5432/omnitrackr&quot; \<br>  --format=custom \<br>  --file=backup.dump<br>```</pre><p>This is the only manual step that involves actual data. Everything else is infrastructure that gets recreated from code.</p><p><strong>Step 2: Set Up the New Account</strong></p><p>Create a new Google account. Activate the free trial. Then run a few `gcloud` commands to set up the project shell:</p><pre>```<br>gcloud projects create omnitrackr-staging-v2<br>gcloud services enable run.googleapis.com sqladmin.googleapis.com ...<br>gsutil mb gs://omnitrackr-terraform-state-v2<br>```</pre><p><strong>Step 3: Update the tfvars and Init Against the New Backend</strong></p><pre>```bash<br># Copy old tfvars for later cleanup, update the active one<br>cp terraform.tfvars terraform.tfvars.old<br># Edit terraform.tfvars: project_id = &quot;omnitrackr-staging-v2&quot;<br># Edit variables.tf default to match<br>```</pre><p>No need to touch `main.tf` — the backend bucket is passed via CLI, not hardcoded.</p><p><strong>Step 4: Apply</strong></p><pre>```bash<br>terraform init -backend-config=&quot;bucket=omnitrackr-terraform-state-v2&quot;<br>terraform apply<br>```</pre><p>Terraform creates the entire infrastructure from scratch in the new project: database, services, load balancer, secrets, IAM, storage bucket, scheduler jobs — everything.</p><p><strong>Step 5: Restore Data and Update DNS</strong></p><pre>```bash<br># Restore database<br>pg_restore --host=NEW_IP --dbname=omnitrackr backup.dump<br><br># Update DNS to point to new load balancer IP<br>gcloud dns record-sets update staging.omnitrackr.dev. \<br>  --type=A --rrdatas=NEW_LB_IP<br>```</pre><p><strong>Step 6: Update GitHub Secrets and Deploy</strong></p><p>Update the handful of secrets in GitHub (new project ID, new service account key), push to `develop`, and your CI/CD pipeline deploys everything to the new account.</p><p><strong>Step 7: Tear Down the Old Account</strong></p><pre>```bash<br># Point Terraform at the old state bucket and destroy using the old tfvars<br>terraform init -backend-config=&quot;bucket=omnitrackr-terraform-state&quot; -reconfigure<br>terraform destroy -var-file=&quot;terraform.tfvars.old&quot;<br>```</pre><p>Clean. Complete. No orphaned resources.</p><h3><strong>What Makes This Work (And What Breaks It)</strong></h3><p><strong>This works because:</strong></p><p>1. <strong>Every resource is defined in Terraform.</strong> Nothing was clicked into existence through the console. If it’s not in `.tf` files, it doesn’t exist.</p><p>2. <strong>All configuration is parameterized.</strong> Project IDs, regions, and environment names are variables, not hardcoded strings scattered across 20 files.</p><p>3. <strong>Environments are fully isolated.</strong> Staging and production share nothing — no projects, no secrets, no state files, no CI/CD triggers.</p><p>4. <strong>Secrets are generated, not copied.</strong> Terraform creates fresh passwords for each environment. No password reuse, no secret files floating around.</p><p>5. <strong>CI/CD reads from environment-specific secrets.</strong> GitHub Actions workflows reference `GCP_PROJECT_ID_STAGING` (not a hardcoded project name), so updating one secret propagates everywhere.</p><p>6. <strong>DNS is the only external dependency. </strong>The domain registrar and DNS zone are the only things that bridge old and new accounts. Everything else is self-contained.</p><p><strong>This breaks if:</strong></p><p>1. <strong>You configured resources manually through the cloud console.</strong> If you added a firewall rule by clicking buttons, Terraform doesn’t know about it, and it won’t exist in the new project.</p><p>2. <strong>You hardcoded project IDs or bucket names in source files.</strong> If your `main.tf` has `bucket = “my-specific-bucket”` or your API has `project: “omnitrackr-staging”` buried in a service file, you’ll have to edit files every time you switch targets.</p><p>3. <strong>You share secrets across environments.</strong> If staging and production use the same JWT secret, rotating one breaks the other.</p><p>4. <strong>Your Terraform state is local.</strong> If your `.tfstate` file lives on your laptop instead of a cloud bucket, you can’t collaborate, and you can’t cleanly re-initialize for a new project.</p><h3><strong>Lessons from the Trenches</strong></h3><p><strong>Start with Terraform Even If You Don’t Plan to Migrate</strong></p><p>Even if you never hop accounts, Terraform pays for itself:</p><p>- <strong>Disaster recovery.</strong> Your entire infrastructure can be recreated from code. If something breaks catastrophically, `terraform apply` gets you back.</p><p>- <strong>Documentation.</strong> Your `.tf` files are living documentation of exactly what infrastructure exists and how it’s configured.</p><p>- <strong>Onboarding.</strong> When you hire a DevOps engineer, they can read your Terraform files and understand your entire setup in minutes instead of spelunking through the cloud console.</p><p>- <strong>Cost visibility. </strong>You can see every resource that exists and what it costs, because it’s all in your codebase.</p><p><strong>Keep DNS Separate</strong></p><p>I made a deliberate choice to manage DNS through the GCP Console rather than Terraform. This means `terraform destroy` doesn’t touch my DNS zone. During migration, I just update the A record to point to the new load balancer IP.</p><p>DNS zones are cheap (around $0.20/month on Google Cloud DNS), so keeping the zone in the old account while everything else moves to the new one is fine as a transitional strategy. You can migrate DNS to a registrar like GoDaddy or Cloudflare later at your convenience.</p><p><strong>Accept the Brief Downtime</strong></p><p>During migration, there’s a window where DNS is pointing to the new load balancer but the SSL certificate hasn’t provisioned yet. For staging, this is a non-issue. For production, you’d want to plan this during a maintenance window and set DNS TTLs low (60 seconds) beforehand.</p><p>For a pre-revenue MVP, an hour of downtime on staging is nothing. Don’t over-engineer the zero-downtime migration if you have five users.</p><p><strong>Document the Migration Process</strong></p><p>I wrote a detailed <a href="https://github.com/mliyanage/omnitrackr/blob/develop/docs/GCP_STAGING_MIGRATION_PLAN.md">migration plan</a> before touching anything. Every command, every file change, every secret to update. This isn’t just for the current migration — it’s the playbook for the next one. When this free trial runs out, I’ll spend half the time because the process is documented.</p><p>This connects back to my earlier point about <a href="https://mliyanage.com/5-critical-mistakes-non-technical-founders-make-when-building-mvps-with-ai-and-how-to-avoid-them-5231c25131fc">documenting architectural decisions </a>— the same discipline that helps you build maintainable code helps you build maintainable infrastructure.</p><p><strong>The Broader Point: Build Your MVP Like It’s Going to Move</strong></p><p>Cloud account hopping is just one application of a deeper principle: <strong>build your infrastructure to be portable from day one.</strong></p><p>You might need to move because:</p><p>- Free credits ran out (the account hopping scenario)</p><p>- You’re switching cloud providers entirely (GCP to AWS)</p><p>- A client requires infrastructure in a specific region or cloud</p><p>- You need to spin up isolated environments for enterprise customers</p><p>- You’re handing off to a DevOps team that has their own standards</p><p>In every case, the founders who invested in Infrastructure as Code, environment separation, and proper secrets management will adapt in days. The founders who clicked through consoles and hardcoded values will be stuck for weeks.</p><p><strong>Quick-Start Checklist for New MVPs</strong></p><p>If you’re starting a new project today and want to keep your options open:</p><ul><li><strong>Use Terraform (or Pulumi/CDK) from day one. </strong>Even a basic setup is better than clicking through consoles.</li><li><strong>Parameterize your project ID, region, and environment name.</strong> Never hardcode these values.</li><li><strong>Use partial backend configuration. </strong>Don’t hardcode your state bucket in `main.tf` — pass it via `terraform init -backend-config=”bucket=…”`. This lets you switch between projects and environments without editing source files.</li><li><strong>Use separate `.tfvars` files per environment</strong> (e.g., `terraform.tfvars`, `terraform.tfvars.prod`). Load them with `terraform apply -var-file=”…”`. One Terraform directory, multiple environments — no copy-pasting `.tf` files.</li><li><strong>Store Terraform state in a cloud bucket </strong>, not on your laptop. Use a separate bucket per environment.</li><li><strong>Create separate secrets for each environment</strong> in your CI/CD system (`_STAGING` / `_PROD` suffixes).</li><li><strong>Use your cloud’s secrets manager</strong> for passwords and API keys. Never commit secrets to Git.</li><li><strong>Generate fresh secrets per environment.</strong> Don’t copy passwords between accounts.</li><li><strong>Keep DNS management separate</strong> from your main infrastructure so it survives teardowns.</li><li><strong>Document your infrastructure.</strong> Future you (or your future DevOps hire) will thank you.</li><li><strong>Write a migration playbook</strong> the first time you set things up, while the process is fresh.</li></ul><h3><strong>Final Thoughts</strong></h3><p>There’s a certain irony in using engineering best practices to save money on cloud bills. The same infrastructure discipline that makes your MVP production-ready also makes it free-trial-ready.</p><p>But that’s the thing about doing things right: it pays off in ways you don’t expect. I set up Terraform because I wanted reproducible deployments. I separated environments because I didn’t want staging bugs in production. I used Secret Manager because I didn’t want credentials in Git.</p><p>And as a side effect, I can migrate my entire staging environment to a fresh GCP account in an afternoon and keep building without spending a dollar on infrastructure.</p><p>That’s the kind of leverage bootstrapped founders need.</p><p><strong>Need Help Getting Your Technical Foundation Right?</strong></p><p>I’ve been through the pain of setting up cloud infrastructure, making architectural decisions, and building an MVP from scratch — so you don’t have to learn these lessons the hard way.</p><p>If you’re a non-technical founder building a SaaS product and you need help with:</p><p>- <strong>Cloud infrastructure setup</strong> — Terraform, CI/CD pipelines, environment separation</p><p>- <strong>Architecture decisions</strong> — database design, API structure, secrets management</p><p>- <strong>Technical strategy</strong>— choosing the right stack, planning for scale, avoiding costly mistakes early</p><p>- <strong>AI-assisted development</strong> — getting the most out of tools like Claude and Cline without accumulating technical debt</p><p>I offer a <strong>Fractional CTO Service </strong>— helping founders make the right technical decisions from day one so they can focus on finding product-market fit instead of fighting infrastructure fires.</p><p><a href="https://cto-as-a-service.breely.com/form/14013"><strong>Book a free consultation</strong></a><strong> a</strong>nd let’s talk about your project.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f2f3b6e66c55" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[5 Critical Mistakes Non-Technical Founders Make When Building MVPs with AI (And How to Avoid Them)]]></title>
            <link>https://mliyanage.medium.com/5-critical-mistakes-non-technical-founders-make-when-building-mvps-with-ai-and-how-to-avoid-them-5231c25131fc?source=rss-176ddb7f2678------2</link>
            <guid isPermaLink="false">https://medium.com/p/5231c25131fc</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[founders]]></category>
            <category><![CDATA[startup-lessons]]></category>
            <category><![CDATA[vibe-coding]]></category>
            <dc:creator><![CDATA[Manjula Liyanage]]></dc:creator>
            <pubDate>Tue, 25 Nov 2025 22:17:29 GMT</pubDate>
            <atom:updated>2026-01-27T12:59:57.790Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*ac1OvMOFUd_IaQZz" /><figcaption>Photo by <a href="https://unsplash.com/@johnmoeses?utm_source=medium&amp;utm_medium=referral">John Moeses Bauan</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><h3>Introduction</h3><p>You’ve got a brilliant SaaS idea. You’re ready to build. But there’s one problem: you don’t have a technical co-founder, and hiring a development team would burn through your runway before you even validate product–market fit.</p><p>Enter AI dev tools like Claude, GitHub Copilot, and ChatGPT. They promise to democratize software development, letting anyone build production-ready applications with the right prompts. And they deliver — to a point.</p><p>I recently built <a href="https://staging.omnitrackr.dev/">OmniTrackr</a>, a file monitoring and SLA tracking platform, using Claude as my primary development partner. The AI helped me ship a full-stack application with a React frontend, Express API, PostgreSQL database, background workers, and automated CI/CD pipelines — all without writing most of the code myself.</p><p>But here’s what nobody tells you: <strong>the quality of your AI-built MVP depends entirely on how well you prompt it</strong>. Make the wrong assumptions early, and you’ll spend days (and thousands of tokens) refactoring. Skip critical architectural decisions, and you’ll hand off a codebase that real developers will want to rewrite from scratch.</p><p>After building OmniTrackr and accumulating over 300KB of documentation, countless refactoring sessions, and valuable lessons learned, I’ve identified the 5 most critical mistakes non-technical founders make when “vibe coding” with AI.</p><p>These aren’t just theoretical pitfalls — they’re real mistakes I made, complete with the technical debt they created and how I fixed them. Whether you’re building a proof-of-concept or an MVP you plan to scale, avoiding these mistakes will save you time, money, and your sanity.</p><h3>Mistake #1: Skipping Architecture Documentation and Design Decisions</h3><h3>The Mistake</h3><p>When you start building with AI, it’s tempting to jump straight into feature development. You type “build me a user dashboard” and watch the code materialize. It feels like magic.</p><p>But here’s the trap: <strong>AI generates code that works now, but you need a codebase that can be understood and maintained later</strong> — preferably by a team of real developers you’ll hire once you’ve validated your idea.</p><p>Without proper documentation of your architectural decisions, database schema design, and system design, you’re creating a mystery box that even you won’t understand in six months.</p><h3>Why This Matters</h3><p>You’re not building a throwaway prototype. You’re building the foundation for a company. Future developers need to understand:</p><ul><li><strong>Why</strong> certain architectural decisions were made</li><li><strong>What</strong> trade-offs were considered</li><li><strong>How</strong> different components interact</li><li><strong>When</strong> to use specific patterns or approaches</li></ul><h3>What I Did Wrong</h3><p>Initially, I focused solely on getting features working. The AI would suggest an approach, I’d approve it, and we’d move on. No documentation, no decision logs.</p><p>Three weeks later, when I wanted to refactor the file source connection logic, I couldn’t remember why we chose a certain database schema. Did we consider alternatives? Were there trade-offs? I had no idea.</p><h3>The Fix</h3><p>I started instructing the AI to document every major decision.</p><p><strong>Prompt template:</strong></p><pre>Before implementing [feature], create a design document that covers:<br>1. Problem statement and requirements<br>2. Proposed solution with alternatives considered<br>3. Database schema design with justification<br>4. API contract specifications<br>5. Security considerations<br>6. Testing strategy<br>7. Deployment plan</pre><h3>Real Examples from OmniTrackr</h3><p>Some of the design docs that ended up saving me countless hours:</p><ul><li><a href="https://github.com/mliyanage/omnitrackr/blob/develop/docs/SCHEMA_SPLIT_DESIGN.md"><strong>Schema Split Design</strong></a> — Documents the decision to split a monolithic file_sources table into normalized tables (source_connections, schedules, watchers). Includes alternatives considered, migration strategy, and trade-off analysis.</li><li><a href="https://github.com/mliyanage/omnitrackr/blob/develop/docs/CREDENTIAL_STORAGE_ARCHITECTURE.md"><strong>Credential Storage Architecture</strong></a> — Details the multi-layered approach to storing AWS credentials (Secrets Manager + encrypted database fallback), including security considerations and key rotation strategy.</li><li><a href="https://github.com/mliyanage/omnitrackr/blob/develop/docs/DATA_MODEL_AND_API_PLAN.md"><strong>Data Model and API Plan</strong></a> — Comprehensive overview of the entire data model with entity relationships, API endpoints, and business logic.</li><li><a href="https://github.com/mliyanage/omnitrackr/blob/develop/docs/S3_FILE_SOURCE_DESIGN.md"><strong>S3 File Source Design</strong></a> — A deep-dive into handling S3 file sources, including credential validation, bucket access patterns, and error handling strategies.</li><li><a href="https://github.com/mliyanage/omnitrackr/blob/develop/docs/REPOSITORY_STRUCTURE.md"><strong>Repository Structure</strong> </a>— Documents the monorepo structure, package dependencies, and architectural patterns.</li></ul><h3>Action Items</h3><p>✅ Create a /docs folder from day one<br> ✅ Document before building major features<br> ✅ Include alternatives considered to show thought process<br> ✅ Explain trade-offs so future developers understand constraints<br> ✅ Update docs during refactoring to reflect learnings</p><h3>Mistake #2: Not Thinking About Component Reusability and Organization</h3><h3>The Mistake</h3><p>You prompt the AI: “Build a page where users can create, read, update, and delete file watchers.”</p><p>The AI delivers. It creates a beautiful feature with inline forms to create schedules, departments, and connections right from the watcher creation flow. Everything works perfectly.</p><p><strong>But here’s the problem:</strong> the AI puts all the code in /src/components/watchers/ because that’s where you told it to build the feature.</p><p>Now you need those same schedule, department, and connection components on other pages. But they’re tightly coupled to the watcher workflow, nested three folders deep in watcher-specific code.</p><h3>Why This Matters</h3><p>Features you build in isolation often need to be reused elsewhere. When you don’t plan for component reusability upfront:</p><ul><li>Components become <strong>tightly coupled</strong> to specific workflows</li><li>You end up <strong>duplicating code</strong> across different pages</li><li><strong>Refactoring costs</strong> multiply (time, tokens, bugs)</li><li>You introduce <strong>inconsistent UX</strong> when duplicate components diverge</li></ul><h3>What I Did Wrong</h3><p>I asked the AI to build inline creation for schedules, connections, and departments within the watcher creation flow — before building standalone pages for these entities.</p><p>The result? All the components ended up in /src/components/watchers/:</p><pre>src/components/watchers/<br>├── ConnectionCombobox.tsx<br>├── ConnectionForm.tsx<br>├── DepartmentCombobox.tsx<br>├── DepartmentForm.tsx<br>├── ScheduleCombobox.tsx<br>├── ScheduleForm.tsx<br>├── WatcherForm.tsx<br>└── WatcherSheet.tsx</pre><p>These should have been in:</p><ul><li>/src/components/connections/</li><li>/src/components/departments/</li><li>/src/components/schedules/</li></ul><p>When I later needed a standalone Connections page, I had to refactor everything. This created:</p><ul><li><strong>Token cost</strong> for extensive refactoring</li><li><strong>Time waste</strong> re-generating similar code</li><li><strong>Bugs</strong> from missed references during the move</li><li><strong>Inconsistencies</strong> between old and new versions</li></ul><h3>The Fix</h3><p><strong>Think through your entire feature set BEFORE building individual features.</strong></p><p><strong>Better prompt strategy:</strong></p><pre>I need to build a watcher management system that will eventually include:<br>1. Standalone pages for Connections, Schedules, and Departments<br>2. Inline creation of these entities within other workflows<br>3. Reusable components across multiple pages</pre><pre>Please structure the codebase with:<br>- Shared components in /src/components/[entity]/<br>- Each entity gets: ComboBox, Form, Sheet, Table components<br>- Components should accept onSuccess callbacks for flexibility<br>- Keep business logic separate from UI components</pre><h3>Proper Component Structure</h3><p>Here’s how OmniTrackr should have been structured from the start:</p><pre>src/components/<br>├── connections/<br>│   ├── ConnectionCombobox.tsx<br>│   ├── ConnectionForm.tsx<br>│   └── ConnectionSheet.tsx<br>├── departments/<br>│   ├── DepartmentCombobox.tsx<br>│   ├── DepartmentForm.tsx<br>│   └── DepartmentSheet.tsx<br>├── schedules/<br>│   ├── ScheduleCombobox.tsx<br>│   ├── ScheduleForm.tsx<br>│   └── ScheduleSheet.tsx<br>└── watchers/<br>    ├── WatcherForm.tsx<br>    └── WatcherSheet.tsx</pre><p>Action Items</p><p>✅ Map your domain model before building features<br> ✅ Identify reusable entities (users, departments, settings, etc.)<br> ✅ Build shared components first, before feature-specific workflows<br> ✅ Use composition with callbacks rather than tight coupling<br> ✅ Review component organization after each major feature</p><h3>Mistake #3: Ignoring Multi-Environment Configuration from the Start</h3><h3>The Mistake</h3><p>You’re building locally. Everything works. The AI hardcodes database credentials, API endpoints, and configuration values directly in the code.</p><p>Then you want to deploy to staging. Suddenly, you need different database credentials, different AWS buckets, and different secrets. You start doing find-and-replace across your codebase.</p><p><strong>This is a nightmare.</strong></p><h3>Why This Matters</h3><p>Modern applications need to run in multiple environments:</p><ul><li><strong>Development</strong> — Your local machine with test data</li><li><strong>Staging</strong> — Cloud environment that mirrors production (I use this for testing, though. Another mistake: not naming the environment correctly)</li><li><strong>Production</strong> — Real users with real data</li><li><strong>CI/CD</strong> — Automated testing and deployment pipelines</li></ul><p>Each environment needs different:</p><ul><li>Database connections</li><li>API keys and secrets</li><li>Feature flags</li><li>Logging levels</li><li>Third-party service endpoints</li></ul><p>Hardcoding these values or handling them inconsistently leads to:</p><ul><li>Accidentally using production data in development</li><li>Secrets committed to Git</li><li>Broken deployments</li><li>Security vulnerabilities</li></ul><h3>What I Did Wrong</h3><p>Initially, I had a single .env file with hardcoded values. When deploying to Google Cloud Run, I realized:</p><ul><li>The staging database shouldn’t be the same as dev</li><li>Environment variables needed to be injected via Cloud Run configuration</li><li>Local development needed Cloud SQL Proxy for secure database access</li><li>Different environments needed different logging levels and error handling</li></ul><h3>The Fix</h3><p>I restructured the configuration system with environment-specific files and validation.</p><p><strong>Key pattern:</strong></p><pre>// packages/api/knexfile.ts<br>// Load environment-specific .env files with priority:<br>// .env.{NODE_ENV}.local &gt; .env.{NODE_ENV} &gt; .env</pre><pre>const nodeEnv = process.env.NODE_ENV || &#39;development&#39;;</pre><pre>dotenv.config({ path: path.resolve(__dirname, &#39;.env&#39;) });<br>dotenv.config({ path: path.resolve(__dirname, `.env.${nodeEnv}`) });<br>dotenv.config({ path: path.resolve(__dirname, `.env.${nodeEnv}.local`) });</pre><p><strong>Environment files:</strong></p><pre>.env                    # Default values, safe to commit<br>.env.development        # Dev-specific values<br>.env.development.local  # Your personal overrides (gitignored)<br>.env.staging            # Staging configuration<br>.env.production         # Production configuration</pre><p><strong>Validation at startup:</strong></p><pre>function validateEnvVars(requiredVars: string[]): void {<br>  const missing = requiredVars.filter((varName) =&gt; !process.env[varName]);<br>  if (missing.length &gt; 0) {<br>    throw new Error(<br>      `Missing required environment variables: ${missing.join(&#39;, &#39;)}\n` +<br>      `Make sure you have created .env.${nodeEnv}.local file`<br>    );<br>  }<br>}</pre><pre>if (nodeEnv === &#39;staging&#39; || nodeEnv === &#39;production&#39;) {<br>  validateEnvVars([&#39;DB_HOST&#39;, &#39;DB_NAME&#39;, &#39;DB_USER&#39;, &#39;DB_PASSWORD&#39;]);<br>}</pre><p><strong>Cloud SQL Proxy support:</strong></p><pre>connection: (() =&gt; {<br>  const useProxy = process.env.USE_CLOUD_SQL_PROXY === &#39;true&#39;;<br>  if (useProxy) {<br>    return {<br>      host: &#39;127.0.0.1&#39;,<br>      port: parseInt(process.env.CLOUD_SQL_PROXY_PORT || &#39;5433&#39;),<br>      // Proxy handles encryption, no SSL needed<br>    };<br>  } else {<br>    return {<br>      host: process.env.DB_HOST,<br>      ssl: { rejectUnauthorized: false }<br>    };<br>  }<br>})()</pre><h3>Real-World Example</h3><p>The <a href="https://staging.omnitrackr.dev/">staging environment</a> now runs with:</p><ul><li>A separate PostgreSQL database</li><li>Google Cloud Run deployment</li><li>Cloud SQL Proxy for secure connections</li><li>Environment-specific secrets in Google Secret Manager</li><li>Automated deployments on push to the develop branch</li></ul><h3>Prompt Strategy</h3><p>From the beginning, tell your AI:</p><pre>Structure this application to support multiple environments (dev, staging, production):</pre><pre>1. Use environment variables for ALL configuration<br>2. Create .env.example with all required variables<br>3. Support layered config: .env → .env.{NODE_ENV} → .env.{NODE_ENV}.local<br>4. Validate required environment variables at startup<br>5. Never commit actual secrets to Git<br>6. Support both local development and cloud deployment<br>7. Use Cloud SQL Proxy for secure database connections in development</pre><h3>Action Items</h3><p>✅ Use environment variables from day one<br> ✅ Create .env.example documenting all required variables<br> ✅ Implement config validation that fails fast with helpful errors<br> ✅ Add .env*.local to .gitignore<br> ✅ Support Cloud SQL Proxy for safe remote database access<br> ✅ Document environment setup in your README</p><h3>Mistake #4: Not Designing Your Database Schema Properly Upfront</h3><h3>The Mistake</h3><p>You tell the AI: “Create a database table for file sources that stores S3 connection info, file patterns, and scheduling configuration.”</p><p>The AI creates a single file_sources table with 30+ columns mixing:</p><ul><li>Connection credentials</li><li>Bucket configuration</li><li>File patterns and filters</li><li>Scheduling settings</li><li>Monitoring configuration</li><li>Status and tracking data</li></ul><p>It works! You ship the feature.</p><p>Two weeks later, you realize you need to:</p><ul><li>Reuse the same S3 connection across multiple file sources</li><li>Create schedules that apply to multiple resources</li><li>Track file changes separately from configuration</li></ul><p><strong>Now you’re in trouble.</strong> You need to split one monolithic table into 5 normalized tables, migrate existing data, update all your API endpoints, refactor the frontend, and update documentation.</p><p>This is exactly what happened to OmniTrackr.</p><h3>Why This Matters</h3><p>Database schema is the <strong>foundation of your application</strong>. Poor database design leads to:</p><ul><li><strong>Data duplication</strong> — Same S3 credentials stored in multiple rows</li><li><strong>Update anomalies</strong> — Changing a schedule requires updating multiple records</li><li><strong>Deletion problems</strong> — Can’t delete a schedule without losing file source config</li><li><strong>Query complexity</strong> — Retrieving related data requires massive JOINs or multiple queries</li><li><strong>Scaling issues</strong> — Table grows unwieldy with mixed concerns</li><li><strong>Migration nightmares</strong> — Refactoring requires complex data migrations</li></ul><h3>What I Did Wrong</h3><p>I started with a monolithic file_sources table that mixed everything:</p><pre>CREATE TABLE file_sources (<br>  id SERIAL PRIMARY KEY,<br>  name TEXT,<br>  type TEXT,</pre><pre>  -- Connection config (should be separate table)<br>  aws_access_key_id TEXT,<br>  aws_secret_access_key TEXT,<br>  aws_region TEXT,<br>  bucket_name TEXT,</pre><pre>  -- Scheduling (should be separate table)<br>  schedule_type TEXT,<br>  schedule_value TEXT,<br>  schedule_timezone TEXT,</pre><pre>  -- File patterns (should be separate table)<br>  file_patterns JSONB,<br>  file_filters JSONB,</pre><pre>  -- Monitoring (should be separate table)<br>  sla_max_age_hours INTEGER,<br>  sla_alert_recipients JSONB,</pre><pre>  -- Status tracking (should be separate table)<br>  last_checked_at TIMESTAMPTZ,<br>  last_file_detected_at TIMESTAMPTZ,<br>  status TEXT</pre><pre>  -- ... 15 more columns<br>);</pre><p><strong>Problems this created:</strong></p><ul><li>Creating multiple watchers for the same S3 bucket meant duplicating credentials</li><li>Changing a schedule from “daily at 2 AM” to “every 6 hours” required updating multiple file sources</li><li>Couldn’t share departments or tags across different file sources</li><li>Couldn’t track file-level changes independently from configuration</li><li>The table had 30+ columns and kept growing</li></ul><h3>The Fix</h3><p>I had to perform a major schema redesign, splitting the monolithic table into a normalized structure.</p><p><strong>New normalized schema:</strong></p><pre>-- Reusable connection configuration<br>CREATE TABLE source_connections (<br>  id SERIAL PRIMARY KEY,<br>  name TEXT NOT NULL,<br>  type TEXT NOT NULL,  -- &#39;s3&#39;, &#39;azure&#39;, &#39;gcp&#39;<br>  connection_config JSONB NOT NULL,  -- Credentials and config<br>  created_by INTEGER REFERENCES users(id)<br>);</pre><pre>-- Reusable schedules<br>CREATE TABLE schedules (<br>  id SERIAL PRIMARY KEY,<br>  name TEXT NOT NULL,<br>  type TEXT NOT NULL,  -- &#39;daily&#39;, &#39;interval&#39;, &#39;cron&#39;<br>  config JSONB NOT NULL,  -- { hour: 14, minute: 30 } or { interval_hours: 6 }<br>  timezone TEXT DEFAULT &#39;UTC&#39;<br>);</pre><pre>-- Specific file watchers<br>CREATE TABLE watchers (<br>  id SERIAL PRIMARY KEY,<br>  name TEXT NOT NULL,<br>  connection_id INTEGER REFERENCES source_connections(id),<br>  schedule_id INTEGER REFERENCES schedules(id),<br>  department_id INTEGER REFERENCES departments(id),</pre><pre>  -- Watcher-specific config<br>  file_pattern TEXT NOT NULL,<br>  file_filters JSONB,<br>  sla_max_age_hours INTEGER,</pre><pre>  status TEXT DEFAULT &#39;active&#39;<br>);</pre><pre>-- File-level tracking<br>CREATE TABLE file_tracking (<br>  id SERIAL PRIMARY KEY,<br>  watcher_id INTEGER REFERENCES watchers(id),<br>  file_key TEXT NOT NULL,<br>  file_size BIGINT,<br>  last_modified TIMESTAMPTZ,<br>  first_seen_at TIMESTAMPTZ DEFAULT NOW(),<br>  last_seen_at TIMESTAMPTZ DEFAULT NOW()<br>);</pre><pre>-- Audit logs<br>CREATE TABLE watcher_logs (<br>  id SERIAL PRIMARY KEY,<br>  watcher_id INTEGER REFERENCES watchers(id),<br>  event_type TEXT NOT NULL,  -- &#39;check&#39;, &#39;error&#39;, &#39;sla_breach&#39;<br>  message TEXT,<br>  details JSONB,<br>  created_at TIMESTAMPTZ DEFAULT NOW()<br>);</pre><p><strong>Benefits of normalized design:</strong></p><p>✅ Reusability — one S3 connection used by multiple watchers<br> ✅ Consistency — update a schedule in one place<br> ✅ Flexibility — easy to add new connection types or schedule patterns<br> ✅ Performance — smaller tables with better indexes<br> ✅ Clarity — each table has a single, clear purpose</p><h3>The Migration Nightmare</h3><p>Refactoring required:</p><ol><li>Creating new tables with migration scripts</li><li>Migrating data from the old table to the new normalized structure</li><li>Updating API endpoints to handle new relationships</li><li>Refactoring frontend components to work with separate entities</li><li>Rewriting queries to use JOINs</li><li>Updating tests for the new data structure</li><li>Dropping old tables after validation</li></ol><p>It cost multiple days of refactoring, thousands of tokens, introduced bugs, and delayed feature development.</p><h3>How to Avoid This</h3><p>Prompt your AI to design a proper schema <strong>before</strong> coding:</p><pre>I&#39;m building a file monitoring system. Before writing any code,<br>create a database schema design document that:</pre><pre>1. Lists all entities and their relationships<br>2. Shows entity-relationship diagrams<br>3. Identifies what should be normalized vs denormalized<br>4. Considers future extensibility (new connection types, schedule patterns)<br>5. Plans for audit logging and change tracking<br>6. Includes proper indexes for query performance<br>7. Documents why each design decision was made</pre><pre>Entities to consider:<br>- Users and authentication<br>- Source connections (S3, Azure, GCP) - REUSABLE<br>- Schedules (daily, interval, cron) - REUSABLE<br>- Departments/teams - REUSABLE<br>- File watchers (specific file patterns to monitor)<br>- File tracking (individual file metadata)<br>- Audit logs and events<br>- SLA definitions and breach tracking</pre><p>Ask the AI to explain trade-offs:</p><ul><li>“Why did you choose this design over alternative X?”</li><li>“What are the pros/cons of normalizing this data?”</li><li>“How will this schema handle [specific future requirement]?”</li></ul><h3>Action Items</h3><p>✅ Design your schema <strong>before</strong> coding features<br> ✅ Identify reusable entities vs feature-specific tables<br> ✅ Normalize appropriately — avoid massive tables mixing concerns<br> ✅ Plan for extensibility — new types, patterns, configurations<br> ✅ Document your schema with ERD diagrams and explanations<br> ✅ Use migrations properly — never alter production schema manually<br> ✅ Add proper indexes based on expected queries</p><h3>Mistake #5: Neglecting Type Safety and Input Validation from Day One</h3><h3>The Mistake</h3><p>You’re building fast with AI. You tell it to create an API endpoint to accept user input. The AI generates the route, the controller, the database query.</p><p>It works! You can create records, update them, and delete them.</p><p>But you didn’t tell the AI to:</p><ul><li>Validate input formats (is that email actually an email?)</li><li>Sanitize user input (hello, SQL injection!)</li><li>Handle edge cases (what if the user sends null? Empty strings? Negative numbers?)</li><li>Provide helpful error messages (just “Invalid input” isn’t helpful)</li><li>Enforce type safety across frontend and backend</li></ul><p><strong>Result:</strong> Your app works in happy-path scenarios but breaks in production when users inevitably send unexpected data.</p><h3>Why This Matters</h3><p>Without proper validation and type safety:</p><ul><li><strong>Security vulnerabilities</strong> — SQL injection, XSS, command injection</li><li><strong>Data corruption</strong> — Invalid data gets into your database</li><li><strong>Cryptic errors</strong> — Users see “500 Internal Server Error” with no context</li><li><strong>Debugging nightmares</strong> — Runtime errors that should have been caught at compile time</li><li><strong>Frontend/backend drift</strong> — Types don’t match, fields are missing or renamed</li><li><strong>Poor user experience</strong> — No helpful validation messages</li></ul><h3>What I Got Right (After Learning the Hard Way)</h3><p>OmniTrackr uses <strong>strict TypeScript</strong> across the entire stack with comprehensive validation at every boundary.</p><h4>1. Strict TypeScript Configuration</h4><p>/tsconfig.json:</p><pre>{<br>  &quot;compilerOptions&quot;: {<br>    &quot;strict&quot;: true,<br>    &quot;noImplicitAny&quot;: true,<br>    &quot;strictNullChecks&quot;: true,<br>    &quot;strictFunctionTypes&quot;: true,<br>    &quot;forceConsistentCasingInFileNames&quot;: true<br>  }<br>}</pre><p>This catches type errors at compile time, prevents undefined/null bugs, and makes refactoring safer.</p><h4>2. Shared Types Package</h4><p>/packages/shared/src/types/:</p><pre>export interface SourceConnection {<br>  id: number;<br>  name: string;<br>  type: SourceType;  // &#39;s3&#39; | &#39;azure&#39; | &#39;gcp&#39;<br>  connection_config: S3ConnectionConfig | AzureConnectionConfig;<br>  status: ConnectionStatus;  // &#39;active&#39; | &#39;inactive&#39; | &#39;error&#39;<br>  created_at: string;<br>  updated_at: string;<br>  created_by: number;<br>}</pre><pre>export enum SourceType {<br>  S3 = &#39;s3&#39;,<br>  Azure = &#39;azure&#39;,<br>  GCP = &#39;gcp&#39;<br>}</pre><pre>export interface S3ConnectionConfig {<br>  aws_access_key_id: string;<br>  aws_secret_access_key: string;<br>  aws_region: string;<br>  bucket_name: string;<br>}</pre><p>Benefits:</p><ul><li>Frontend knows exactly what fields the API returns</li><li>Backend and worker share the same type definitions</li><li>Refactoring a field name updates everywhere</li></ul><h4>3. Joi Validation with Custom Error Messages</h4><p>/packages/api/src/middleware/validation.ts:</p><pre>import Joi from &#39;joi&#39;;</pre><pre>export const createS3ConnectionSchema = Joi.object({<br>  name: Joi.string().trim().min(1).max(100).required()<br>    .messages({<br>      &#39;string.empty&#39;: &#39;Connection name is required&#39;,<br>      &#39;string.max&#39;: &#39;Connection name must be at most 100 characters&#39;,<br>    }),</pre><pre>  aws_access_key_id: Joi.string().trim().required()<br>    .pattern(/^[A-Z0-9]{20}$/)<br>    .messages({<br>      &#39;string.pattern.base&#39;: &#39;Invalid AWS Access Key ID format. Must be 20 uppercase alphanumeric characters.&#39;,<br>      &#39;any.required&#39;: &#39;AWS Access Key ID is required&#39;,<br>    }),</pre><pre>  aws_secret_access_key: Joi.string().trim().required()<br>    .pattern(/^[A-Za-z0-9/+=]{40}$/)<br>    .messages({<br>      &#39;string.pattern.base&#39;: &#39;Invalid AWS Secret Access Key format&#39;,<br>    }),</pre><pre>  aws_region: Joi.string().trim().required()<br>    .valid(&#39;us-east-1&#39;, &#39;us-west-2&#39;, &#39;eu-west-1&#39;, &#39;ap-southeast-1&#39;)<br>    .messages({<br>      &#39;any.only&#39;: &#39;AWS region must be one of: us-east-1, us-west-2, eu-west-1, ap-southeast-1&#39;,<br>    }),</pre><pre>  bucket_name: Joi.string().trim().required()<br>    .pattern(/^[a-z0-9][a-z0-9.-]*[a-z0-9]$/)<br>    .min(3).max(63)<br>    .messages({<br>      &#39;string.pattern.base&#39;: &#39;Invalid S3 bucket name format&#39;,<br>      &#39;string.min&#39;: &#39;Bucket name must be at least 3 characters&#39;,<br>      &#39;string.max&#39;: &#39;Bucket name cannot exceed 63 characters&#39;,<br>    }),<br>}).options({<br>  stripUnknown: true,<br>  abortEarly: false,<br>});</pre><pre>export const validate = (schema: Joi.Schema) =&gt; {<br>  return (req, res, next) =&gt; {<br>    const { error, value } = schema.validate(req.body);</pre><pre>    if (error) {<br>      const errors = error.details.map((detail) =&gt; ({<br>        field: detail.path.join(&#39;.&#39;),<br>        message: detail.message,<br>      }));</pre><pre>      return res.status(400).json({<br>        success: false,<br>        error: {<br>          code: &#39;VALIDATION_ERROR&#39;,<br>          message: &#39;Invalid input data&#39;,<br>          details: errors,<br>        },<br>      });<br>    }</pre><pre>    req.body = value;<br>    next();<br>  };<br>};</pre><p>Used in routes:</p><pre>router.post(<br>  &#39;/connections&#39;,<br>  authenticate,<br>  validate(createS3ConnectionSchema),<br>  async (req, res) =&gt; {<br>    const connection = await connectionService.create(req.body);<br>    res.json({ success: true, data: connection });<br>  }<br>);</pre><h4>4. Connection Validation Before Storage</h4><p>validateS3Connection middleware:</p><pre>export const validateS3Connection = async (req, res, next) =&gt; {<br>  const { aws_access_key_id, aws_secret_access_key, aws_region, bucket_name } = req.body;</pre><pre>  try {<br>    const s3Client = new S3Client({<br>      region: aws_region,<br>      credentials: {<br>        accessKeyId: aws_access_key_id,<br>        secretAccessKey: aws_secret_access_key,<br>      },<br>    });</pre><pre>    await s3Client.send(new ListObjectsV2Command({<br>      Bucket: bucket_name,<br>      MaxKeys: 1,<br>    }));</pre><pre>    next();<br>  } catch (error) {<br>    if (error.name === &#39;NoSuchBucket&#39;) {<br>      return res.status(400).json({<br>        success: false,<br>        error: {<br>          code: &#39;INVALID_BUCKET&#39;,<br>          message: `S3 bucket &#39;${bucket_name}&#39; does not exist or is not accessible`,<br>        },<br>      });<br>    }</pre><pre>    if (error.name === &#39;InvalidAccessKeyId&#39;) {<br>      return res.status(400).json({<br>        success: false,<br>        error: {<br>          code: &#39;INVALID_CREDENTIALS&#39;,<br>          message: &#39;AWS Access Key ID is invalid&#39;,<br>        },<br>      });<br>    }</pre><pre>    return res.status(400).json({<br>      success: false,<br>      error: {<br>        code: &#39;CONNECTION_FAILED&#39;,<br>        message: &#39;Failed to connect to S3 bucket. Please verify credentials and permissions.&#39;,<br>        details: { error: error.message },<br>      },<br>    });<br>  }<br>};</pre><p>This prevents storing invalid credentials and gives immediate, precise feedback.</p><h4>5. Custom Error Classes with Consistent Format</h4><p>/packages/api/src/utils/errors.ts:</p><pre>export class AppError extends Error {<br>  constructor(<br>    public statusCode: number,<br>    public message: string,<br>    public code: string = &#39;INTERNAL_ERROR&#39;,<br>    public details?: Record&lt;string, any&gt;<br>  ) {<br>    super(message);<br>    this.name = this.constructor.name;<br>    Error.captureStackTrace(this, this.constructor);<br>  }<br>}</pre><pre>export class ValidationError extends AppError {<br>  constructor(message: string, details?: Record&lt;string, any&gt;) {<br>    super(400, message, &#39;VALIDATION_ERROR&#39;, details);<br>  }<br>}</pre><pre>export class NotFoundError extends AppError {<br>  constructor(resource: string, identifier?: string | number) {<br>    const message = identifier<br>      ? `${resource} with ID ${identifier} not found`<br>      : `${resource} not found`;<br>    super(404, message, &#39;NOT_FOUND&#39;);<br>  }<br>}</pre><pre>export class UnauthorizedError extends AppError {<br>  constructor(message: string = &#39;Unauthorized&#39;) {<br>    super(401, message, &#39;UNAUTHORIZED&#39;);<br>  }<br>}</pre><p>Global error handler:</p><pre>export const errorHandler = (err, req, res, next) =&gt; {<br>  console.error(&#39;Error occurred:&#39;, {<br>    name: err.name,<br>    message: err.message,<br>    stack: process.env.NODE_ENV === &#39;development&#39; ? err.stack : undefined,<br>    path: req.path,<br>    method: req.method,<br>  });</pre><pre>  if (err instanceof AppError) {<br>    return res.status(err.statusCode).json({<br>      success: false,<br>      error: {<br>        code: err.code,<br>        message: err.message,<br>        details: err.details,<br>      },<br>    });<br>  }</pre><pre>  const message = process.env.NODE_ENV === &#39;production&#39;<br>    ? &#39;Internal server error&#39;<br>    : err.message;</pre><pre>  return res.status(500).json({<br>    success: false,<br>    error: {<br>      code: &#39;INTERNAL_ERROR&#39;,<br>      message,<br>    },<br>  });<br>};</pre><h3>How to Get This Right from the Start</h3><p>Prompt your AI with validation and type safety requirements:</p><pre>Build this feature with comprehensive type safety and validation:</pre><pre>1. Define TypeScript interfaces in /packages/shared/src/types/<br>2. Use strict TypeScript mode (noImplicitAny, strictNullChecks)<br>3. Create Joi (or Zod) validation schemas with:<br>   - Regex patterns for format validation<br>   - Custom error messages that help users<br>   - Input sanitization (trim, stripUnknown)<br>   - Return all errors at once (abortEarly: false)<br>4. Test external connections before accepting credentials<br>5. Use custom error classes (ValidationError, NotFoundError, etc.)<br>6. Implement global error handler with environment-aware messages<br>7. Never return sensitive details in error messages<br>8. Log errors with context for debugging</pre><h3>Action Items</h3><p>✅ Enable strict TypeScript from day one<br> ✅ Create a shared types package for frontend/backend consistency<br> ✅ Use Joi or Zod for runtime validation<br> ✅ Write custom error messages that help users fix issues<br> ✅ Test external connections before accepting credentials<br> ✅ Use custom error classes for consistent error handling<br> ✅ Implement a global error handler with environment-aware logging<br> ✅ Validate at boundaries (API inputs, environment variables, external data)</p><h3>Conclusion: Build Smart, Not Just Fast</h3><p>AI dev tools like Claude can help you build an MVP incredibly fast — but <strong>speed without strategy creates technical debt</strong>.</p><p>The difference between a throwaway prototype and a maintainable codebase comes down to how well you prompt the AI and whether you plan for the future.</p><h3>The 5 Mistakes Recap</h3><ol><li><strong>Skipping Documentation</strong> → Document architecture decisions, database schema, and trade-offs</li><li><strong>Ignoring Reusability</strong> → Plan component structure before building features</li><li><strong>No Environment Strategy</strong> → Support dev/staging/production from day one</li><li><strong>Poor Database Design</strong> → Normalize your schema upfront, avoid monolithic tables</li><li><strong>No Type Safety/Validation</strong> → Use strict TypeScript and comprehensive input validation</li></ol><h3>The Meta-Lesson</h3><p><strong>You’re not just prompting the AI to write code. You’re prompting it to make architectural decisions.</strong></p><p>The better you understand software architecture, the better you can guide the AI. If you don’t know what questions to ask, the AI will make reasonable guesses — but those guesses might not align with your future needs.</p><h3>What to Do Next</h3><p>If you’re building an MVP with AI:</p><ol><li>Start with documentation — design docs, schema design, architecture decisions</li><li>Plan your domain model — what entities exist, and how they relate</li><li>Structure for reusability — think about component organization early</li><li>Support multiple environments — even if you only use dev at first</li><li>Validate everything — type safety and input validation prevent bugs</li><li>Ask the AI to explain — “Why this approach? What are the alternatives?”</li></ol><p><strong>Remember:</strong> Building with AI isn’t about blindly accepting every suggestion. It’s about being a thoughtful product architect who uses AI as a highly capable implementation partner.</p><p>Happy building! 🚀</p><p><strong>Need Help Getting Your Technical Foundation Right?</strong></p><p>I’ve been through the pain of setting up cloud infrastructure, making architectural decisions, and building an MVP from scratch — so you don’t have to learn these lessons the hard way.</p><p>If you’re a non-technical founder building a SaaS product and you need help with:</p><p>- <strong>Cloud infrastructure setup</strong> — Terraform, CI/CD pipelines, environment separation</p><p>- <strong>Architecture decisions </strong>— database design, API structure, secrets management</p><p>- <strong>Technical strategy</strong> — choosing the right stack, planning for scale, avoiding costly mistakes early</p><p>- <strong>AI-assisted development</strong> — getting the most out of tools like Claude and Copilot without accumulating technical debt</p><p>I offer a <strong>Fractional CTO</strong> service — helping founders make the right technical decisions from day one so they can focus on finding product-market fit instead of fighting infrastructure fires.</p><p><a href="https://cto-as-a-service.breely.com/form/14013)"><strong>Book a free consultation</strong></a> and let’s talk about your project.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5231c25131fc" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Vibe Architecture for Vibe Coders: From Idea to Users]]></title>
            <link>https://mliyanage.medium.com/vibe-architecture-for-vibe-coders-from-idea-to-users-00234f482142?source=rss-176ddb7f2678------2</link>
            <guid isPermaLink="false">https://medium.com/p/00234f482142</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[vibe-coding]]></category>
            <category><![CDATA[generative-ai-tools]]></category>
            <category><![CDATA[software-development]]></category>
            <dc:creator><![CDATA[Manjula Liyanage]]></dc:creator>
            <pubDate>Thu, 14 Aug 2025 12:44:39 GMT</pubDate>
            <atom:updated>2025-08-15T23:28:39.346Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NPoccl50yoVoEeSh2AYDQA.jpeg" /><figcaption>Photo by <a href="https://unsplash.com/@lanceanderson?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Lance Anderson</a> on <a href="https://unsplash.com/photos/white-concrete-building-QdAAasrZhdk?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Unsplash</a></figcaption></figure><p>Vibe coding gets your app built fast. Vibe architecture keeps it alive.</p><p>You’ve already proven you can turn ideas into working software without touching a line of traditional code. But once it’s in the hands of real users, the rules change — suddenly you’re thinking about uptime, scaling, bug fixes, and making sure one tweak doesn’t break the whole damn thing. That’s where vibe architecture comes in.</p><p>This post is your roadmap for taking an app from <strong>“just built”</strong> to <strong>“used and loved by real people”</strong> — without burning out or breaking everything.</p><h3>Who this is for</h3><p>If you can build with vibe coding tools (AI-assisted coding) but struggle with <strong>deploying, maintaining, and scaling</strong> your apps, this is for you.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/753/1*fpgXZ5dcf6CKpA9t7zBmwg.png" /><figcaption><a href="https://www.reddit.com/r/replit/comments/1mpxddr/why_your_app_works_for_you_but_fails_for_real/">https://www.reddit.com/r/replit/comments/1mpxddr/why_your_app_works_for_you_but_fails_for_real/</a></figcaption></figure><h3>The Lifecycle Model</h3><p>The simplest way to think about the software journey is:</p><p><strong>Develop → Build → Ship → Maintain → Scale → Improve</strong></p><p>Here’s the model I use:</p><ul><li><strong>Build</strong> → Write, test, and package your app in a way that’s easy to ship.</li><li><strong>Deployment</strong> <strong>Infrastructure</strong> → Get it running reliably for real users.</li><li><strong>Integrations</strong> → Plug in all the useful services that make it more powerful.</li></ul><p>Let’s walk through each capability, with examples and tools you can use today.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/579/1*m8DOCrN145_PJJin4iDsRw.png" /></figure><h3>1. Develop &amp; Build</h3><p>Goal: <strong>Get clean, testable code and repeatable builds.</strong></p><h3>Vibe Coding Tools</h3><p>You don’t need any explanation here; these are the obvious tools such as Claude Code, Windsurf, Lovebale, Base44, Cursor, Replit, GitHub Codespaces, and VS Code + Copilot.</p><p>Start with an LLM‑assisted design doc</p><p>Before you generate a single screen or API, ask ChatGPT (or your favourite LLM) to produce a <strong>detailed design doc</strong> for your app. This becomes the source of truth you’ll feed back into your AI tools while building.</p><p>What to include (in plain English):</p><ul><li><strong>App purpose &amp; problem statement</strong></li><li><strong>User personas</strong> (primary + secondary)</li><li><strong>Top 3 user journeys</strong> (happy path + edge cases)</li><li><strong>Scope &amp; constraints</strong> (MVP vs later)</li><li><strong>Feature list</strong> grouped by “must / should / could”</li><li><strong>Data model</strong> (entities, relationships, example records)</li><li><strong>API surface</strong> (endpoints, request/response examples)</li><li><strong>Integrations</strong> (email, SMS, auth, payments, analytics, LLM use)</li><li><strong>Non‑functional requirements</strong> (security, performance, availability)</li><li><strong>Preferred tech stack</strong> (or “let the tool choose with reasoning”)</li><li><strong>Metrics &amp; logging</strong> (what we will track and why)</li><li><strong>Risks &amp; open questions</strong></li></ul><h3>Your architecture guardrails (tell the LLM to include these explicitly):</h3><ul><li><strong>Code Quality &amp; Standards:</strong> TypeScript, ESLint, Prettier, Conventional Commits, minimal folder structure.</li><li><strong>Unit Tests for critical paths:</strong> auth, payment, scheduling, timezone logic.</li><li><strong>Database Best Practices:</strong> migrations (Prisma/Drizzle), foreign keys, indexes, seed data, pgvector if using embeddings. Use separate databases for Dev, Test, and Production with proper environment config.</li><li><strong>Environment Parity:</strong> dev/staging/prod mirrored configs; env vars not hardcoded.</li><li><strong>Error Handling &amp; Structured Logging:</strong> JSON logs, correlation/request IDs, consistent error shapes.</li><li><strong>Observability:</strong> Sentry for errors, basic request latency metric, uptime checks.</li><li><strong>Security Baseline:</strong> secret manager, security headers, rate limits, input validation.</li><li><strong>Performance &amp; Reliability:</strong> caching where safe, retries with backoff for external APIs, idempotency keys.</li><li><strong>Availability &amp; Backups:</strong> managed Postgres with daily backups, restore drill monthly.</li></ul><h3>Documentation (Just Enough)</h3><p>Save the output of the previous step as /docs/design.md and commit it. Your AI coding tool will keep referencing it while it generates code, tests, and configs.</p><h3>A Reality Check for Non-Programmers Using AI Tools</h3><p>Let me be blunt: <strong>AI will not magically build your app while you sip coffee and watch Netflix.</strong></p><p>If you don’t know programming and you think you can just throw prompts at ChatGPT, copy-paste the output, and end up with a beautiful, maintainable app… you’re in for a rude awakening.</p><p>What you’ll actually get is a <strong>messy, brittle codebase</strong> that works “just enough” for your first demo, but turns into a house of cards the moment you fix one bug or add a new feature.</p><p><strong>You’ve got two real options:</strong></p><ol><li><strong>Learn programming with the AI as your pair programmer.</strong></li></ol><ul><li>Let the AI write code, but read it, ask “why?”, and tweak it yourself.</li><li>Use it as a mentor, not a vending machine.</li><li>This is slower at first, but you’ll gain the skills to maintain and improve your own app without panic.</li></ul><p><strong>2. Admit you’ll need to hire real developers later.</strong></p><ul><li>Focus on validating your idea.</li><li>Keep your scope tight so when you hand it over, you’re not giving a dev team a flaming garbage pile to rewrite from scratch.</li></ul><p>Either way, <strong>stop pretending AI is a push-button “build my business” machine</strong>. It’s a powerful multiplier — but only if you bring knowledge, discipline, and architecture to the table.</p><h3>Unit Tests</h3><p>Unit tests verify that individual components of your code — such as functions, classes, or modules — work exactly as intended in isolation. By testing small, focused units of logic, you can quickly detect errors early in the development process, often before the code is integrated into the larger system. A must-have to avoid this</p><p><strong>Why they’re useful:</strong></p><ul><li><strong>Early Bug Detection:</strong> Issues are caught before they reach users, saving time and reducing costly fixes later.</li><li><strong>Confidence in Changes:</strong> You can safely refactor or extend your code knowing that unit tests will flag any unintended side effects.</li><li><strong>Faster Debugging:</strong> When a test fails, it pinpoints exactly where the issue lies, making troubleshooting faster.</li><li><strong>Improved Code Quality:</strong> Writing unit tests encourages developers to write cleaner, more modular, and more maintainable code.</li></ul><p>In short, unit tests act as a safety net, ensuring your code’s foundation is solid before it’s deployed — preventing small mistakes from turning into big problems for your users.</p><p><strong>Tools:</strong> Jest, Vitest, Pytest.</p><h3>Development Workflow</h3><p>Think of your workflow as the <strong>rules of the road</strong> for how code moves from your laptop to your users. Without a clear process, you’ll end up with “It works on my machine” disasters, broken features in production, and late-night panic fixes.</p><p><strong>Step 1 — Get on GitHub (or GitLab/Bitbucket, but GitHub is easiest)</strong></p><ul><li>Create a repo for your project.</li><li>Commit your /docs/design.md and your initial AI-generated code here.</li><li>Turn on branch protection rules for main so nobody (including you) can push directly without a review.</li></ul><p><strong>Step 2 — Use Branches for Every Change</strong></p><ul><li><strong>main branch</strong> = your production-ready code.</li><li><strong>develop branch</strong> (optional) = where features merge before going to staging.</li><li><strong>Feature branches</strong> = feature/scheduler-ui or fix/timezone-bug.</li><li>Branch naming rule: feature/…, fix/…, or chore/….</li></ul><p><strong>Step 3 — Release Management for Test &amp; Prod</strong></p><ul><li><strong>Staging environment</strong>: a near-copy of production where you test new features with real data (or good fake data) before public release.</li><li><strong>Production environment</strong>: what your real users see.</li><li>Each environment should have its <strong>own database, storage, and API keys</strong> so you can break staging without fear.</li><li>Deploy <strong>staging</strong> on every merge to develop (or main if skipping develop).</li><li>Deploy <strong>production</strong> only when you merge from develop → main or tag a release.</li></ul><p><strong>Step 4 — Automate Everything with CI/CD</strong></p><p>Once you’ve got GitHub and branches sorted, let automation handle the boring (and error-prone) stuff:</p><ol><li><strong>Continuous Integration (CI)</strong></li></ol><ul><li>Runs on every PR or commit.</li><li>Does linting (npm run lint), type checking, and unit tests.</li><li>Builds the app to catch early errors.</li></ul><p><strong>2. Continuous Delivery/Deployment (CD)</strong></p><ul><li>Automatically deploys staging after a successful PR merge.</li><li>Automatically deploys production when you merge to main (or create a GitHub Release).</li><li>Can run <strong>smoke tests</strong> (via Playwright or Cypress) against the deployed URL to confirm nothing is broken.</li></ul><p><strong>Example Flow:</strong></p><pre>feature/new-onboarding → PR → CI checks pass → merge to develop → auto deploy to staging<br>Test staging → merge develop → main → auto deploy to production</pre><p><strong>Recommended Stack for Vibe Coders:</strong></p><ul><li><strong>GitHub Flow</strong> (simple and popular)</li><li><strong>GitHub Actions</strong> (free for small projects)</li><li><strong>Vercel Previews</strong> (instant preview URL for every PR)</li><li><strong>Protected branches + required reviews</strong> (forces good habits)</li></ul><h3>Vulnerability Scanning</h3><p>Every package you install is code you didn’t write — and it can have bugs, security holes, or even malicious updates that put your users (and your data) at risk. Vulnerability scanning is your early warning system.</p><p><strong>Step 1 — Built-in AI Tool Scanners</strong></p><p>Many modern AI coding tools already have security awareness baked in.</p><ul><li><strong>Cursor</strong>, for example, can integrate with <strong>Snyk</strong>, which scans your code and dependencies in real-time as you work.</li><li>Some AI tools will even <strong>warn you before committing</strong> if they detect API keys, passwords, or other secrets in your code.</li><li>Treat these warnings seriously — they’re not nagging you, they’re saving you from embarrassment or a data breach.</li></ul><p><strong>Step 2 — GitHub Security Tools</strong></p><ul><li><strong>Dependabot</strong> automatically checks for outdated or vulnerable packages and opens PRs to update them.</li><li><strong>GitHub Advanced Security</strong> (paid) adds secret scanning, code scanning, and dependency review.</li><li>At a minimum, enable <strong>Dependabot alerts</strong> in your repo settings.</li></ul><p><strong>Step 3 — CI/CD Integration</strong></p><ul><li>Add npm audit (or yarn audit / pnpm audit) to your CI pipeline so builds fail if critical vulnerabilities are found.</li><li>If using Snyk, connect it to your GitHub Actions workflow so every PR gets a vulnerability report before merge.</li></ul><h3>2. Deployment Infrastructure</h3><p>Goal: <strong>Release with confidence, keep prod safe.</strong></p><p>Some platforms (like <strong>Replit Base44</strong>, Vercel, or Render) have <strong>built-in deployment</strong> — push code, and it’s live. Others require you to set up and manage hosting yourself (AWS, Azure, custom VPS). Either way, these are the core pieces you need to understand and configure:</p><h3>Environments (Test &amp; Prod)</h3><p>Separate sandboxes for testing and live users so you can break staging without breaking production.</p><p><strong>Example:</strong> staging.example.com with its own DB.</p><p><strong>Go-to:</strong> Vercel (app) + Supabase (DB), staging + prod.</p><h3>DNS &amp; Certificates</h3><p>Maps your domain name to your app and encrypts traffic with HTTPS.</p><p><strong>Go-to:</strong> Cloudflare DNS + Vercel auto TLS.</p><h3>Security</h3><p>Keeps your app and data safe with secret management, headers, and firewalls.</p><p><strong>Example:</strong> API keys in secret manager, security headers enabled.</p><p><strong>Go-to:</strong> Managed env vars + basic WAF.</p><h3>Remote Access</h3><p>Allows secure, controlled access to servers or containers for debugging.</p><p><strong>Go-to:</strong> Tailscale, a Jump host or AWS SSM Session Manager.</p><h3>Testing in Deployment</h3><p>Runs automated checks on the deployed app to confirm everything works before users see it.</p><p><strong>Example:</strong> Playwright smoke tests run after deploy.</p><h3>Observability</h3><p>Tracks errors, performance, and user behavior so you can spot issues early.</p><p><strong>Example:</strong> Sentry alerts for 500 errors, PostHog for user flows, CloudWatch alerts, etc</p><h3>Storage</h3><p>Handles file and media storage, often with a CDN for speed.</p><p><strong>Go-to:</strong> R2/S3 for files + Cloudflare CDN.</p><h3>Certificates &amp; Backups</h3><p>Ensures secure HTTPS connections and recoverability if data is lost.</p><p><strong>Example:</strong> Daily DB backups with monthly restore test.</p><h3>Performance &amp; Reliability</h3><p>Keeps the app fast and resilient under load with queues, caching, and retries.</p><p><strong>Example:</strong> Queue SMS jobs, retry failed sends.</p><h3>Availability</h3><p>Prevents downtime by removing single points of failure and enabling scaling.</p><p><strong>Example:</strong> Multi-AZ DB, health checks, auto-scaling.</p><h3>3. Integrations (Make It Useful)</h3><ul><li><strong>Email Delivery:</strong> Postmark, Mailjet, SendGrid.</li><li><strong>SMS:</strong> Twilio.</li><li><strong>Push Notifications:</strong> Firebase Cloud Messaging.</li><li><strong>Authentication:</strong> Clerk, Firebase auth or Supabase Auth.</li><li><strong>Payments:</strong> Stripe.</li><li><strong>Analytics:</strong> PostHog.</li><li><strong>CRM:</strong> HubSpot Free.</li><li><strong>LLMs:</strong> OpenAI + pgvector.</li><li><strong>Other Services:</strong> Svix for webhooks, n8n/Zapier for automations.</li></ul><h3>4. Three Starter Stacks You Can Copy</h3><p><strong>Weekend MVP (fastest path)</strong></p><ul><li>Next.js on Vercel or Railway</li><li>Supabase (DB + Auth + storage)</li><li>Postmark, Stripe, PostHog, Sentry</li></ul><p><strong>Indie SaaS (cheap to scale)</strong></p><ul><li>Next.js + Vercel or Railway</li><li>Neon Postgres + Prisma</li><li>Clerk, R2 (assets), PostHog, Sentry, Stripe, BullMQ</li></ul><p><strong>Enterprise-Friendly</strong></p><ul><li>AWS (EC2/EB/ECS), Google Cloud Run, Azure App Service</li><li>RDS Postgres, CloudFront, Route53</li><li>AWS Secrets Manager, CloudWatch, Auth0, SES/SNS, Stripe, Terraform</li></ul><h3>5. Anti-Crash Checklist</h3><ul><li>[ ] Staging environment with separate DB</li><li>[ ] Health check route &amp; smoke test after deploy</li><li>[ ] Error tracking on (Sentry)</li><li>[ ] Daily DB backups + restore test</li><li>[ ] Rate limiting + retries on external APIs</li><li>[ ] Security headers + secret manager</li><li>[ ] Observability dashboard (errors, latency)</li><li>[ ] Feature flags for risky changes</li></ul><p><strong>Final thought:</strong> Vibe coding lets you build in days. Vibe architecture keeps your app alive for years. Use this checklist, pick a starter stack, and start shipping without the fear of breaking everything.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=00234f482142" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A simple RAG Application]]></title>
            <link>https://mliyanage.medium.com/simple-rag-application-e23910275c8f?source=rss-176ddb7f2678------2</link>
            <guid isPermaLink="false">https://medium.com/p/e23910275c8f</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[genai]]></category>
            <category><![CDATA[agentic-ai]]></category>
            <category><![CDATA[rags]]></category>
            <dc:creator><![CDATA[Manjula Liyanage]]></dc:creator>
            <pubDate>Fri, 06 Jun 2025 16:01:18 GMT</pubDate>
            <atom:updated>2025-06-07T11:17:21.998Z</atom:updated>
            <content:encoded><![CDATA[<h3>Simple RAG Application</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hzvAx5-fxvcIVMXcAJXBhw.png" /><figcaption>Architecture</figcaption></figure><p>This simple Retrieval-Augmented Generation (RAG) application would read a large PDF, semantically chunk the content, embed it using OpenAI, and store those embeddings in MongoDB Atlas for efficient vector search. When a user asks a question, relevant context is retrieved and passed to an LLM to generate accurate, grounded answers.</p><p><a href="https://github.com/mliyanage/agentic_ai2/blob/main/rag_app.ipynb">My notebook file</a></p><ol><li>Load the PDF using PyPDFLoader</li></ol><pre>from langchain_community.document_loaders import PyPDFLoader<br>loader=PyPDFLoader(&#39;NET-Microservices.pdf&#39;)<br>pages=loader.load() #Load data into Document objects, a list.<br>pages</pre><p>2. Just checking whether the number of pages is correct by checking the length of the pages list.</p><pre>len(pages)  # Number of pages loaded<br></pre><p>3. Perform semantic chunking</p><p>This will chunk the texts based on semantic breakpoints (e.g., sentence or paragraph ends), and maintain some overlap for context preservation.</p><p>Here, I used the RecursiveCharacterTextSplitter with semantic breakpoints (e.g., sentence or paragraph ends), a 1000-chunk size, and a 200 overlap. I wanted to understand the <a href="https://python.langchain.com/docs/how_to/semantic-chunker/">Greg Kamradt semantic similarity checking</a> method, but I couldn’t do it yet — next one.</p><pre>from langchain.text_splitter import RecursiveCharacterTextSplitter<br><br># Join all pages <br>full_text = &quot; &quot;.join([p.page_content for p in pages])<br><br># Use RecursiveCharacterTextSplitter with sentence-level separators<br>text_splitter = RecursiveCharacterTextSplitter(<br>    separators=[&quot;\n\n&quot;, &quot;\n&quot;, &quot;.&quot;, &quot;!&quot;, &quot;?&quot;, &quot;,&quot;, &quot; &quot;],<br>    chunk_size=1000,<br>    chunk_overlap=200,<br>)<br><br>chunks = text_splitter.create_documents([full_text])</pre><p>Let’s see if the length of the chunks, so we know if that worked.</p><pre>len(chunks)  # Number of chunks created</pre><p>4. Let’s embed the chunks and insert into the MongoDB collection</p><pre>from langchain.embeddings import OpenAIEmbeddings<br>from pymongo import MongoClient<br>from langchain_mongodb.vectorstores import MongoDBAtlasVectorSearch<br><br>import os<br>from dotenv import load_dotenv<br>load_dotenv() <br><br>#get connection string, database name, collection name from the .env file<br><br>client = MongoClient(os.getenv(&quot;MONGODB_CONNECTION_STRING&quot;))<br>collection = client[os.getenv(&quot;MONGODB_NAME&quot;)][os.getenv(&quot;MONGODB_COLLECTION&quot;)]<br><br># setting the embeding model<br>embedding_model = OpenAIEmbeddings()<br><br># Insert the documents in MongoDB Atlas with their embedding<br>docsearch = MongoDBAtlasVectorSearch.from_documents(<br>    chunks, embedding_model, collection=collection, index_name=os.getenv(&quot;MONGODB_INDEX_NAME&quot;)<br>)</pre><p>Now we can go to the Atlas console and see the MongoDB collection. The data should be populated.</p><figure><img alt="Screenshot of the MongoDB Collection where the embeddings are stored" src="https://cdn-images-1.medium.com/max/988/1*DDmbFDbNPGIPSTJvRZM_AQ.png" /><figcaption>MogoDB Collection where embeddings are stored. It got the original text in the text field and embeddings on the embeddings array (1536 elements)</figcaption></figure><p>5. Index the Vector Embeddings</p><p>To index the collection, we need to create a Vector Search Index</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*w5up1sohdla0Wlc2bTCXyQ.png" /></figure><p>Select Vector Search for semantic search and AI applications as the search type. Then select the collection where our embeddings are stored.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/988/1*K9oF5YGQaK10zVYDp0Re4g.png" /></figure><p>Next, we can use the JSON Editor for configuration.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/981/1*Irmu9p64egLeECJLZ9y1wQ.png" /></figure><p>Configuration: The Path is the field name of the collection where we stored the embeddings. The numDimention is the length of the array.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/998/1*eWCQnu0Rthy6HeFpFzdlcw.png" /></figure><p><a href="https://www.pinecone.io/learn/vector-similarity/">Similarity is the function</a> type that will be used to perform the similarity search. <br>This is some math shit that my brain couldn’t comprehent, but I “belive” in maths so that gives me confident it will do the job even though I don’t understand how it works. It’s quite the opposite of the <a href="https://www.laphamsquarterly.org/magic-shows/miscellany/niels-bohrs-lucky-horseshoe"><strong>Niels Bohr</strong> popular horseshoe story</a> 😂</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/628/1*rS6gIeZJCpipdQA___nx8A.png" /></figure><p>6. Create a Retrieve function</p><pre># Insert the documents in MongoDB Atlas with their embedding<br>docsearch = MongoDBAtlasVectorSearch.from_documents(<br>    chunks, embedding_model, collection=collection, index_name=os.getenv(&quot;MONGODB_INDEX_NAME&quot;)<br>)</pre><p>Check if the vector search is working by performing a similarity search.</p><pre># perform a similarity search between the embedding of the query and the embeddings of the documents<br>myquery = &quot;What is docker and how does it work?&quot;<br>context_from_vecotore_store = vectorStore.similarity_search(query=myquery, k=5)<br><br>context_from_vecotore_storeÍ</pre><p>It works</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xvJGT7JaqyczMOkceAntQQ.png" /></figure><p>Now, set up the retriever, output parser, and LLM Model that we are going to use.</p><pre>from langchain_openai import OpenAI<br>from langchain_core.output_parsers import StrOutputParser<br>from langchain_core.runnables import RunnablePassthrough, RunnableLambda<br>from langchain_core.messages.base import BaseMessage<br>from langchain import hub<br><br>llm = OpenAI(model_name=&quot;gpt-3.5-turbo-instruct&quot;)<br><br>retriever = vectorStore.as_retriever()<br>output_parser = StrOutputParser()<br><br>rag_prompt = hub.pull(&quot;rlm/rag-prompt&quot;)</pre><p>Then we have these two functions</p><p>Theformat_docs(docs) takes a list of document objects (where each object has an page_content attribute) and returns a single string.</p><p>The get_question(input) function ensures that, <strong>regardless of how a question is passed</strong>, the system can <strong>extract a clean question string</strong> for downstream processing (like retrieval or prompting).</p><pre>def format_docs(docs):<br>    return &quot;\n\n&quot;.join(doc.page_content for doc in docs)<br><br>def get_question(input):<br>    if not input:<br>        return None<br>    elif isinstance(input,str):<br>        return input<br>    elif isinstance(input,dict) and &#39;question&#39; in input:<br>        return input[&#39;question&#39;]<br>    elif isinstance(input,BaseMessage):<br>        return input.content<br>    else:<br>        raise Exception(&quot;string or dict with &#39;question&#39; key expected as RAG chain input.&quot;)</pre><p>Next, set up the <strong>Retrieval-Augmented Generation (RAG)</strong> pipeline using LangChain’s composable Runnable framework.</p><pre>rag_chain = (<br>        {<br>            &quot;context&quot;: RunnableLambda(get_question) | retriever | format_docs,<br>            &quot;question&quot;: RunnablePassthrough()<br>        }<br>        | rag_prompt<br>        | llm<br>        | output_parser<br>)</pre><p>Here’s a breakdown of what it’s doing:</p><p>{<br> “context”: RunnableLambda(get_question) | retriever | format_docs,<br> “question”: RunnablePassthrough()<br>}</p><p>This sets up a dictionary-based input processor.</p><p>&quot;question&quot;: The original input (e.g. &quot;What is Docker?&quot;) is passed directly via RunnablePassthrough().</p><p>&quot;context&quot;:</p><p>get_question: The function that extracts the input question.</p><p>retriever: Uses the result to retrieve relevant documents from a vector store (e.g., MongoDB Vector Search).</p><p>format_docs: Formats the documents into a string using &quot;\n\n&quot;.join(...).</p><p>| rag_prompt: This combines the context and question into a prompt template (e.g., &quot;Use the following context to answer the question...&quot;).</p><p>| llm: Sends the final prompt to the LLM (e.g., OpenAI GPT-4) for response generation.</p><p>Now, let’s test this. Pass a question and invoke the RAG chain.</p><pre>rag_chain.invoke(&quot;What is Docker?&quot;)</pre><p>It works 🤸‍♂️</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1013/1*Y1QXH2V7hvicA8q3M24wlA.png" /></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e23910275c8f" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A Roadmap to Becoming a Generative AI Engineer]]></title>
            <link>https://mliyanage.medium.com/a-roadmap-to-becoming-a-generative-ai-engineer-7c4802c3d0a2?source=rss-176ddb7f2678------2</link>
            <guid isPermaLink="false">https://medium.com/p/7c4802c3d0a2</guid>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[genai]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[api]]></category>
            <category><![CDATA[chatgpt]]></category>
            <dc:creator><![CDATA[Manjula Liyanage]]></dc:creator>
            <pubDate>Tue, 25 Feb 2025 22:29:37 GMT</pubDate>
            <atom:updated>2025-02-26T00:17:08.346Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*JYZw1Xr7OjqPyG_O" /><figcaption>Photo by <a href="https://unsplash.com/@googledeepmind?utm_source=medium&amp;utm_medium=referral">Google DeepMind</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>You want to become an AI engineer but struggling to find a clear starting point? This guide provides a structured roadmap to help you focus and avoid distractions while mastering Generative AI development.</p><h3>Step 1: Introduction to NLP</h3><p>Natural Language Processing (NLP) is the foundation of Generative AI. Begin by understanding the basics of NLP, including how machines process and interpret human language.</p><h3>Step 2: Master Text Processing in NLP</h3><p>Familiarize yourself with essential text processing concepts:</p><ul><li><strong>Tokenization</strong> — Breaking text into words or subwords.</li><li><strong>Stop Words Removal</strong> — Filtering out common words like “the,” “is,” etc.</li><li><strong>Stemming &amp; Lemmatization</strong> — Reducing words to their base or root forms.</li><li><strong>Lowercasing and Punctuation Removal</strong> — Standardizing text for better processing.</li></ul><h3>Step 3: Learn Part-of-Speech (POS) Tagging</h3><p>Understand how NLP models classify words into their respective parts of speech (nouns, verbs, adjectives, etc.) to improve language understanding.</p><h3>Step 4: Understand Named Entity Recognition (NER)</h3><p>Learn how models extract useful information, such as names of people, locations, and organizations, from text.</p><h3>Step 5: Study Text Vectorization</h3><p>Transform text into numerical representations using:</p><ul><li><strong>Bag of Words (BoW)</strong></li><li><strong>TF-IDF (Term Frequency-Inverse Document Frequency)</strong></li><li><strong>Word Embeddings (e.g., Word2Vec, GloVe, FastText)</strong></li></ul><p>Watch this tutorial to understand text vectorization: <a href="https://www.youtube.com/watch?v=ENLEjGozrio">YouTube Video</a></p><h3>Step 6: Understand How Large Language Models (LLMs) Work</h3><p>LLMs are built on the Transformer architecture. To understand them, watch an explanation of Google’s <em>Attention is All You Need</em> paper: <a href="https://www.youtube.com/watch?v=bCz4OMemCcA">YouTube Video</a></p><h3>Step 7: Experiment with LLMs</h3><p>Try using different models to get hands-on experience:</p><ul><li><strong>Paid Models:</strong> OpenAI’s GPT, Gemini by Google</li><li><strong>Open-Source Models:</strong> Hugging Face, Ollama</li></ul><h3>Step 8: Learn Prompt Engineering</h3><p>Understand how to craft effective prompts to get the best results from LLMs. Check out these resources:</p><ul><li><a href="https://www.youtube.com/watch?v=BDlmT1z2IJE">YouTube Video</a></li><li><a href="https://www.youtube.com/watch?v=_ZvnD73m40o">YouTube Video</a></li></ul><h3>Step 9: Explore Advanced Topics</h3><ul><li><strong>Quantization</strong> — Optimize models for efficiency.</li><li><strong>Fine-Tuning LLMs</strong> — Train models with custom datasets. Follow this guide: <a href="https://www.youtube.com/watch?v=6S59Y0ckTm4&amp;list=PLZoTAELRMXVN9VbAx5I2VvloTtYmlApe3">YouTube Video</a></li></ul><h3>Step 10: Learn LangChain and LangGraph</h3><p>These tools help build applications using LLMs.</p><ul><li><a href="https://www.youtube.com/watch?v=nAmC7SoVLd8">LangChain Guide</a></li><li><a href="https://www.youtube.com/watch?v=y-HBJ2bWRss&amp;list=PLNVqeXDm5tIqUIPQHLk5Xw5mpisruvsac">LangGraph Guide</a></li></ul><h3>Step 11: Build a Basic AI Application</h3><p>Start with a chatbot or another AI-driven project. You’ll need:</p><ul><li><strong>Backend Development:</strong> Learn <strong>FastAPI</strong> for LLM deployments.</li><li>FastAPI Docs &amp; Tutorials: <a href="https://www.youtube.com/watch?v=XnYYwcOfcn8&amp;list=PLqAmigZvYxIL9dnYeZEhMoHcoP4zop8-p">YouTube Playlist</a></li><li><strong>Database Knowledge:</strong> Learn how to store and retrieve data efficiently.</li><li><strong>Authentication Methods:</strong> Understand OAuth, access tokens, and user authentication.</li></ul><h3>Step 12: Build the Frontend (Optional)</h3><p>For a full-stack AI application, learn:</p><ul><li><strong>HTML, CSS, Tailwind</strong> (UI styling)</li><li><strong>JavaScript Frameworks:</strong> React, Angular, Vue</li><li>However, if your primary goal is to become an AI engineer, focus more on backend development.</li></ul><h3>Step 13: Deploy Your AI Application</h3><p>Learn how to host and deploy your project for real-world use.</p><h3>Step 14: Solve Real-World Problems</h3><p>Build AI solutions that address practical challenges. The best way to master AI is to apply it!</p><p>By following this roadmap, you can systematically develop your expertise in Generative AI. Stay focused, avoid distractions, and most importantly, keep experimenting and building!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7c4802c3d0a2" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[AWS Lambda Use Cases]]></title>
            <link>https://mliyanage.medium.com/aws-lambda-use-cases-84448bb7762d?source=rss-176ddb7f2678------2</link>
            <guid isPermaLink="false">https://medium.com/p/84448bb7762d</guid>
            <category><![CDATA[cloud-computing]]></category>
            <category><![CDATA[aws-lambda]]></category>
            <category><![CDATA[aws]]></category>
            <dc:creator><![CDATA[Manjula Liyanage]]></dc:creator>
            <pubDate>Sun, 11 Dec 2022 11:30:36 GMT</pubDate>
            <atom:updated>2022-12-11T11:42:43.215Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="An image with AWS Lambda sign and title of the post, AWS Lambda use case" src="https://cdn-images-1.medium.com/max/1024/1*HWF8dLDoQk8QYds4R2A14A.png" /></figure><h3>What is AWS Lambda and Why?</h3><p><a href="https://aws.amazon.com/lambda/">AWS Lambda</a> is a service provided by Amazon Web Services (AWS) that allows developers to run their code without worrying about the underlying infrastructure. Therefore, developers can focus on writing and deploying their code rather than managing and scaling servers.</p><p>AWS Lambda functions are executed in response to specific events or triggers, such as user requests or data changes in a database. These functions are independent and can be written in various languages, including JavaScript, Python, and Java.</p><p>One advantage of using AWS Lambda is its great flexibility and scalability. Since each function is independent, it can be scaled up or down separately from the rest of the application. Additionally, cloud-native services allow easy integration with other cloud-based services, such as databases and message queues. This can make it easier to build complex, scalable applications.</p><p>So, below are some highlights:</p><ul><li>Removes the need for traditional computing services</li><li>Reduce operational cost and easier operational management</li><li>Faster development</li><li>Auto-scaling — Lambda can scale from a single request to hundreds of thousands per second. <a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html">Read this</a> to learn more about how Lambda handles Scaling.</li><li>Pay as you go</li><li>Lambda is natively microservice; therefore, it gives an easy microservice implementation model.</li></ul><p>Given these benefits, you should still know the <a href="https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html">limitation of Lambda</a>. In addition, take note of known <a href="https://mliyanage.medium.com/55-lambda-pitfalls-198862077b79">Lambda pitfalls</a> and <a href="https://www.linkedin.com/feed/update/urn:li:activity:6896060601446100992/">cost optimization tips</a>.</p><p>Here are some Lambda use-cases</p><h3>Building Serverless web apps</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/831/1*sjBauIGjWtU-YjHwj4h7Pg.jpeg" /><figcaption>An architecture of a Serverless web app</figcaption></figure><p>Lambda functions can be used to build serverless web applications, where the application logic is split into individual tasks executed in response to specific events. This can make it easier to build scalable, flexible applications.</p><p>You can create a web API with an HTTP endpoint for your Lambda function by using <a href="https://docs.aws.amazon.com/lambda/latest/dg/services-apigateway-tutorial.html">Amazon API Gateway</a>. API Gateway provides tools for creating and documenting web APIs that route HTTP requests to Lambda functions.</p><p>Other Serverless services used in this architecture:</p><p>Cognito User Pools provides features to control user sign-up, sign-in, email or SMS verification and user management with more advanced features. Users can also sign in through social identity providers like Google, Facebook, and SAML identity providers.</p><p>Cognito Identity Pool will provide temporary credentials to AWS resources like S3 using the token received on successful login.</p><p>Cognito will authorize the user with the necessary permissions for the IAM role. <a href="https://medium.com/@sumindaniro/user-authentication-and-authorization-with-aws-cognito-d204492dd1d0">Learn more about user authentication and authorization with AWS Cognito.</a></p><p>Route 53 for managing DNS (custom domain names)</p><p>Use CloudFront distribution with S3 to serve the web app. The website must be a client-side web app such as React, Angular, or JavaScript.</p><p>Certificate manager to provision an <a href="https://www.stormit.cloud/post/setup-an-amazon-cloudfront-distribution-with-ssl-custom-domain-and-s3">SSL certificate and associate with CloudFront</a></p><p><a href="https://www.readysetcloud.io/blog/allen.helton/adding-a-custom-domain-to-aws-api-gateway/">Associate the custom domain</a> and the SSL with the API gateway</p><p>Amazon DynamoDB is a fully-managed, high-performance, NoSQL database service that is easy to set up, operate, and scale. It is used to persist session data, such as the shopping cart and the product database. The same architecture can be extended to develop complex applications like serverless e-commerce systems.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/921/1*D58XZcq_Y60KFSbB6jzSqw.jpeg" /><figcaption>An architecture of an e-commerce app</figcaption></figure><p>I’m using the <a href="https://docs.microsoft.com/en-us/dotnet/architecture/cloud-native/introduce-eshoponcontainers-reference-app">Microsoft cloud-native microservice reference application</a> with AWS services.</p><p>The application also uses a related pattern called Backends-for-Frontends (BFF), which recommends creating separate API gateways for each front-end client. The reference architecture demonstrates breaking up the API gateways based on whether the request comes from a web or mobile client.</p><p>Other Serverless services used in this architecture:</p><p><em>Amazon Personalize:</em> Amazon Personalize provides similar item recommendations, search re-ranking based on user preferences, and product recommendations based on user-item interactions.</p><p><em>Amazon Pinpoint:</em> Amazon Pinpoint adds the ability to dynamically send welcome messages, abandoned cart messages, and messages with personalized product recommendations to the customers.</p><p><em>Amazon ElastiCache:</em> This is a session store for volatile data and a caching layer for the product catalogue to reduce I/O (and cost) on DynamoDB.</p><p><em>Amazon Simple Notification Service</em> (Amazon SNS) is a managed service that delivers messages from publishers to subscribers (also known as <em>producers</em> and <em>consumers</em>).</p><p><em>Amazon Simple Queue Service</em> (SQS) lets you send, store, and receive messages between services at any volume without losing messages or requiring other services to be available.</p><p><em>EventBridge</em> enables you to decouple your architectures to make it faster to build and innovate, using routing rules to deliver events to selected targets.</p><p><a href="https://aws.amazon.com/blogs/architecture/architecting-a-highly-available-serverless-microservices-based-ecommerce-site/">How to achieve high availability</a></p><h3>Document conversion</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/711/1*TjtobXTCflbdhwVSlY0U2A.jpeg" /></figure><p>Suppose your application provides documents (invoices, purchase orders, receipts, prescriptions). In that case, your application should be capable of serving those documents in different formats, such as PDF, HTML, and CSV, depending on the requirement and the device. Instead of storing documents in all forms, you can use AWS Lambda to create the desired format of the document and serve it to the customer for a download or a display on a page.</p><h3>Data processing</h3><p>Lambda functions can process large amounts of data, such as logs or events. This can be useful for data transformation, aggregation, or analysis tasks.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/471/1*OEy9ZwPQ629Df5VJbeVYlw.jpeg" /></figure><p>An excellent example of this use case is a Centralized logging system where Lambda is used for logging transformation.</p><p>You can attach a Lambda function to Kinesis to transform data before sending it to the destination. You can use Lambda for Structuring data as per destination accepted format, such as adding metadata and combining data from another source. Lambda can add metadata such as SourceName, SourceType, AWS Account Number and name etc. Also, Lambda can transform the raw logs to JSON format if required. Also, it can read from an S3 bucket to get the logs in the example of Load Balancer logs. Many other sources can be integrated with Kinesis via EventBridge.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pJY_YD-X4yfpffBPstPHgw.jpeg" /><figcaption>AWS Log transformation and ingestion</figcaption></figure><h3>File processing</h3><p>Lambda functions can be used to process files as they are uploaded to the cloud. For example, a Lambda function could be used to resize images, extract text from documents, or transcribe audio files.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/631/1*LIZhLjQ-WQOgtKgp1hw1Hw.jpeg" /></figure><p>Objects are uploaded into Amazon S3. Then the S3 bucket publishes an event notification to an Amazon SNS topic.</p><p>Amazon SNS can fan-out messages to multiple subscribers.</p><p>N number of Lambda functions can be created to process data, all without the need to provision or manage servers.</p><p>After processing, data can be sent to different AWS services for storage, further processing or analytics.</p><p>A famous example of this is how <a href="https://youtu.be/hU25CIRPIJo">Netflix do their video encoding at scale using Lambda.</a></p><p>With Lambda, Netflix can use rules triggered by the movement of video assets to launch and configure the necessary processing to encode 60 different parallel streams and can use the rules and events to aggregate and deploy after all the parts are processed.</p><h3>Automation</h3><p>Lambda functions can be used to automate a wide range of tasks, such as backups, data migration, or data synchronization. These functions can be triggered by a schedule or in response to specific events, making it easy to automate complex processes.</p><p>Sending security alerts and email automation are two common examples.</p><p>Sending security alerts</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/711/1*YraRFvnnUeA7G68zKKlAgA.jpeg" /><figcaption>Automation of security alerts and raising incidents</figcaption></figure><p>You can write a Lambda function to send an alert on a specific event from Cloudwatch/CloudTrail AWS activity logs. It will notify your designated on-call staff via email, or you could even write a code that will trigger the AWS Lambda to call you on your phone.</p><p>Email automation</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/552/1*Hy5XVuL_Ih2thEtciIFBfg.jpeg" /><figcaption>Email automation</figcaption></figure><p>You can automate newsletters, email campaigns and transactional emails using Lambda functions and Simple Email Service SES.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=84448bb7762d" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[5 Lambda pitfalls]]></title>
            <link>https://mliyanage.medium.com/55-lambda-pitfalls-198862077b79?source=rss-176ddb7f2678------2</link>
            <guid isPermaLink="false">https://medium.com/p/198862077b79</guid>
            <category><![CDATA[aws-lambda]]></category>
            <category><![CDATA[cloud-computing]]></category>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[aws-cost-optimization]]></category>
            <dc:creator><![CDATA[Manjula Liyanage]]></dc:creator>
            <pubDate>Fri, 26 Aug 2022 05:30:49 GMT</pubDate>
            <atom:updated>2022-08-26T07:42:50.083Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ibcsg7_HQiDRSYTuC5nFdQ.png" /></figure><h4>1. Lambda Overloading</h4><p>Sometimes you use the same Lambda function for multiple purposes.</p><p>Avoid using one Lambda function for two different tasks because both of these workloads can scale independently and would require different memory configurations.</p><p>Also, it makes monitoring challenging because It will be difficult to understand what&#39;s going on in your Lambda function by looking at the monitoring. Because there is more than one purpose of invocations, you would not able to relate monitoring metrics to different invocations.</p><p>Use <strong>purpose-built and Small Lambda Functions</strong></p><p>One Lambda should do only one thing. Single Responsibility Principle</p><p><strong>Function size is part of the cost</strong></p><p>Lambda runtime would download the code from S3 or Docker image registry on every cold-start, the larger the code, the longer the wait, the more you pay</p><ul><li>Since 2021 Lambda functions are billed in one millisecond increments, you can now <strong>save money for every millisecond your function runs faster</strong>.</li></ul><h4>2. Using CloudWatch PutMetricData API</h4><p><strong>PutMetricData</strong> API Publishes metric data points to Amazon CloudWatch.</p><p>It is a common requirement for you need to collect metrics in your Lambda function. A Lot of people use PutMetricData API from the Lambda function to publish the metrics to CloudWatch.</p><p>Your Lambda will make a synchronous HTTP call to the PutMetricData API and this would put your Lambda on hold and it will cost you money.</p><p>Instead of using Cloudwatch PutMetricData API, use <a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CreateMetricFilterProcedure.html"><strong>metric filters</strong></a>.</p><p>You can simply emit a logline and within that logline, you emit the name of the metric and then the number of counters that you want to pair with that metric.</p><p>Example: You want to count the number of legal argument exceptions in your Lambda. You can put leagalarguments=1</p><p>Then you can set up a metric filter with a rule to parse that format of the log line and that will be published into Cloudwatch metrics.</p><p>This happens offline and it is not part of the lambda execution.</p><h4>3. Chaining lambda functions synchronously</h4><p>In synchronous Lambda calls The first one will wait for the second one to finish, and you’ll be paying for the waiting time.</p><p>Don’t call a lambda from another lambda. Run the second function using a service like Step Functions.</p><h4>4. Using lambda functions as an Orchestrator</h4><p>A lot of people combine Lambda with SQS because it&#39;s an easy way to move messages around from your Lambda and let the output of one function be the input of another.</p><p>This pattern can be very challenging to analyse what is actually going on in your application when your data is bouncing around in many Lambda functions</p><p>Log streams are independent of each lambda function, and it&#39;s difficult to pair all the logs that are related to a particular invocation.</p><p>Use <a href="https://docs.aws.amazon.com/step-functions/latest/dg/sample-lambda-orchestration.html">Step Functions for Lambda Orchestration</a></p><p>Step functions allow you to build workflows, allow fallbacks, error handling, and retries, and it&#39;s very easy to analyse what happened in the step function workflow through the AWS Step Function console.</p><h4>5. Not using batch processing in SQS and Lambda functions for event processing workloads</h4><p>When you have a message in your SQS you can set it up to invoke your lambda with a single message. When you have many messages, it&#39;s a lot of invocations and will become costly.</p><p>The ideal way to do this is to use SQS and Lambda batch processing.</p><p>The Lambda function will pull multiple messages from the SQS once and pass all of those messages to the event body of the lambda function invocation.</p><p>This way you can iterate all the messages in the input and process them independently.</p><p>But you have to be careful in this approach because there would be partial failures. When the failure occurs, you have to put those messages back into the SQS to retry later.</p><p>There is an easy way to <a href="https://betterprogramming.pub/sqs-batch-processing-with-reporting-batch-item-failures-6c405c852401">handle partial failures using failure reporting</a>. You can return failed message ids from your Lambda to the SQS. Return the failed message ids that will indicate SQS to put those messages back into the queue. This way only the failed message will reappear in the Queue for processing and the successful will be deleted from the Queue.</p><p>Check out my Lambda cost optimization tips <a href="https://www.linkedin.com/posts/manjula-gl_aws-lambda-cost-optimization-tips-activity-6896060601446100992-lu0t?utm_source=share&amp;utm_medium=member_desktop">here</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=198862077b79" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Migrating 3 tier web application to AWS (Re-hosting/lift-and-shift)]]></title>
            <link>https://mliyanage.medium.com/migrating-3-tier-web-application-to-aws-re-hosting-lift-and-shift-4bc327567afa?source=rss-176ddb7f2678------2</link>
            <guid isPermaLink="false">https://medium.com/p/4bc327567afa</guid>
            <category><![CDATA[cloud-computing]]></category>
            <category><![CDATA[aws]]></category>
            <dc:creator><![CDATA[Manjula Liyanage]]></dc:creator>
            <pubDate>Tue, 26 Apr 2022 12:59:46 GMT</pubDate>
            <atom:updated>2022-05-19T00:29:34.667Z</atom:updated>
            <content:encoded><![CDATA[<h3>The Current Architecture</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/932/1*MQd22naYznh6NLxAbT6PYQ.jpeg" /></figure><p>Before migrating any workload to the cloud, you need to assess the right <a href="https://aws.amazon.com/blogs/enterprise-strategy/6-strategies-for-migrating-applications-to-the-cloud/">migration strategy</a> for you. I wrote <a href="https://manju.la/migration-q/">some questions</a> that you should ask to evaluate your systems.</p><h3>Deployment Overview</h3><p>The current application will be migrated as it is to the AWS Cloud. There will be no code changes required other than changing the connection config.</p><p>The Oracle database will be deployed in an EC2 instance. (Oracle RDS was not chosen for this migration because code changes might be required. Also, there are possible dependencies on some Oracle services and DB server file system)</p><p>Tape backups will be replaced with scheduled backups and an S3 deep archive. You can use the <a href="http://aws.amazon.com/storagegateway">AWS Storage Gateway file interface</a> to directly back up the database to Amazon S3. AWS Storage Gateway file interface provides an NFS mount for S3 buckets. Oracle Recovery Manager (RMAN) backups written into the Network File System (NFS) mount are automatically copied to S3 buckets by the AWS Storage Gateway instance.</p><p>Backups will be stored in S3 and moved to the S3 deep archive to save costs. An Application Load balancer will replace the on-prem load balancer.</p><h3>New Architecture</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/768/1*hBwPdKzy1MlGyhfqyRtrcQ.jpeg" /></figure><h3>High availability</h3><p>The application will be deployed in two AWS availability zones. This will guarantee the application availability even if one AZ goes down. The primary DB will be syncing with the second instance on the other AZ. This instance will be on standby. If in case of a failure in the AZ 1, the stand by DB will automatically be brought up to serve the requests from the application servers. An application load balancer — ALB will be used to divide the load between two AZs. ALB will ensure the traffic will be sent only to the available and healthy server. It will stop sending traffic to a web server if in case the server health check fails.</p><h3>Performance</h3><p>Auto-scaling cannot be used in the current application because the web application is storing the user session (stateful application). The application needs to be refactored to support auto-scaling.</p><p>AWS Cloud-front can be used to cache static content from different geographical locations to provide faster access.</p><h3>Security</h3><p>Authenticating users: On-prem AD and SSO will authenticate the organisation’s employees to the cloud infra.</p><p>Use IAM policies and best practices to provide only the required amount of access to the users.</p><p>Data Security in transit: All client applications will communicate over HTTPS. SSL/TLS will be deployed on the application load balancer, and only HTTPS traffic will be allowed from the Internet. Certificate manager is used to manager SSL certificates and be associated with the ALB.</p><p>Data security at rest: All EC2 instance volumes will be encrypted. Furthermore, data in EFS and S3 will be encrypted.</p><p>Application security: The web application firewall prevents vulnerabilities such as DDoS attacks, cross-site scripting, cookie poisoning, parameter tampering, etc.</p><p>All application servers will be deployed in a private subnet that allows only inbound traffic from the ALB in the public subnet.</p><p>Moreover, four security groups will enable managing traffic flows securely.</p><ul><li>Web DMZ security group will allow port 443 traffic from the internet.</li><li>In the Web SG — Allow traffic from the Web DMZ SG</li><li>In the App SG — Allow traffic from Web SG</li><li>In the DB SG — Allow traffic from App SG</li></ul><p>A NAT gateway will be used to download security patches for web and app servers. This is required to keep the security and software patches to be updated.</p><p>Application support and maintenance: application engineers can perform code deployments from the corporate network to the servers via the site-to-site VPN. This connectivity will be used for application support and deployments.</p><h3>Monitoring and alerts</h3><p>Cloud watch will be used to monitor system performances and health. If in case of any issue, Cloud watch will trigger alerts to the application support team. Cloud trail can be used to monitor user activities for audit purposes.</p><h3>Backups</h3><p>Scheduled backups will be enabled in the database with an appropriate retention period. After the retention period, the backups will be moved to S3 Glacier deep archive to save costs using S3 life cycle management.</p><p>In addition, manual periodical snapshots of the DB can be taken and moved to S3 Glacier deep archive.</p><h3>Disaster recovery</h3><p>The same infrastructure set can be deployed in another AWS region for disaster recovery purposes. The region should be selected based on corporate compliance policies.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4bc327567afa" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>