Imposter Syndrome in the Tech Space

I started late, struggled with JS, and spent way too long shrinking into self doubt

3 years ago I didn’t know what a component was. Didn’t know what mount/unmount meant. Big O Notation was just letters and symbols that meant nothing to me.

This year marks 3 years of coding for me. And honestly? It hasn’t always felt like progress.

The comparison trap

HTML and CSS came naturally, I picked them up fast and felt good about it. Then JavaScript hit and everything slowed down. It’s still the thing I wrestle with most to this day.

What made it worse was social media. I’d open Instagram or TikTok and see developers my age shipping insane projects, explaining complex systems like it was nothing, and I’d just… shrink. That’s the only word for it. I’d shrink into this shell of self doubt and start asking myself the same questions on repeat:

What can I build that hasn’t already been built?
What separates me from every other developer?
Out of 100 devs, why would anyone pick me?

It wasn’t hate. It was a healthy jealousy if that makes sense, I wanted to be where they were, I just couldn’t see how to get there from where I was standing.

The shift

I can’t point to one moment where everything changed. It was gradual.

I started learning React properly mid last year. This year I picked up DSA, SQL, and Express, because I got convinced pretty quickly that frontend alone doesn’t sell, and honestly I wasn’t comfortable staying in that lane anyway.

Built a few practice projects, freelanced a little here and there, nothing groundbreaking… well asides the current project I’m working on, the general idea is… well I believe it’ll be groundbreaking but currently the current product is all over the place but it’s real work. The job market hasn’t been the kindest but I’m still pushing. I’m still young, I have time, and I’m not saying that to slack off, I’m saying it because panicking about where I’m not yet has never once helped me get there faster.

What I actually think now

I stopped looking at it as a competition. I started having fun with it. And the moment I did that, the projects started coming, the progress started showing, and the self doubt got quieter.

You’re not behind. You’re just on your own timeline. And your progress is only measured by how far you can see yourself going, not by what someone else is shipping online. Low-key I’m saying this to convince myself too but nevertheless

If you’re in the same headspace I was in, keep building. Even the small stuff counts. It all compounds.

P.S: I still have trouble navigating github sometimes, Thank God for git-repo extensions on VScode lol

Adding SEO Checks to CI/CD Without Slowing Down Your Pipeline

We had a deploy last quarter that removed the canonical tags from about 200 pages. Nobody noticed for three weeks. By the time we caught it, Google had indexed duplicate versions of every page, and our organic traffic dipped 15%.

The fix took 10 minutes. The recovery took 6 weeks.

This is why i think SEO checks belong in CI/CD. But every time i bring this up, the reaction from other devs is the same: “We tried running Lighthouse in CI and it added 4 minutes to every build.”

Yeah. Dont do that.

Why Lighthouse in CI Is the Wrong Approach

Lighthouse is a browser-based audit tool. Running it in CI means spinning up a headless Chrome instance, loading every page, running a full performance audit, accessibility checks, SEO checks, and generating reports. It is comprehensive and also incredibly slow.

For a CI pipeline that runs on every PR, you dont need comprehensive. You need fast and focused.

According to web.dev’s Lighthouse documentation, a single Lighthouse run takes 15-45 seconds per page. If your checking 10 pages, thats 3-7 minutes added to your pipeline. Most teams will just skip it.

What to Actually Check in CI

Here’s the thing. Most SEO disasters from code changes fall into a small number of categories:

  1. Missing or changed title tags
  2. Missing or changed meta descriptions
  3. Broken canonical tags
  4. Noindex tags accidentally added
  5. Broken internal links
  6. Missing alt text on images
  7. Changed URL slugs without redirects
  8. Removed structured data

You dont need Lighthouse for any of these. You need a simple HTML parser that checks specific elements. And that runs in seconds, not minutes.

// SEO linting for CI/CD - runs in under 10 seconds
import { JSDOM } from 'jsdom';
import * as fs from 'fs';
import * as path from 'path';

interface SEOIssue {
  file: string;
  severity: 'error' | 'warning';
  message: string;
}

function lintHTMLForSEO(filePath: string): SEOIssue[] {
  const issues: SEOIssue[] = [];
  const html = fs.readFileSync(filePath, 'utf-8');
  const dom = new JSDOM(html);
  const doc = dom.window.document;

  // Check title tag
  const title = doc.querySelector('title');
  if (!title || !title.textContent?.trim()) {
    issues.push({
      file: filePath,
      severity: 'error',
      message: 'Missing or empty title tag',
    });
  } else if (title.textContent.length > 60) {
    issues.push({
      file: filePath,
      severity: 'warning',
      message: `Title too long (${title.textContent.length} chars, max 60)`,
    });
  }

  // Check meta description
  const metaDesc = doc.querySelector('meta[name="description"]');
  if (!metaDesc || !metaDesc.getAttribute('content')?.trim()) {
    issues.push({
      file: filePath,
      severity: 'error',
      message: 'Missing meta description',
    });
  }

  // Check for accidental noindex
  const robots = doc.querySelector('meta[name="robots"]');
  if (robots?.getAttribute('content')?.includes('noindex')) {
    issues.push({
      file: filePath,
      severity: 'error',
      message: 'Page has noindex directive',
    });
  }

  // Check canonical
  const canonical = doc.querySelector('link[rel="canonical"]');
  if (!canonical) {
    issues.push({
      file: filePath,
      severity: 'error',
      message: 'Missing canonical tag',
    });
  }

  // Check images for alt text
  const images = doc.querySelectorAll('img');
  images.forEach((img, i) => {
    if (!img.getAttribute('alt')) {
      issues.push({
        file: filePath,
        severity: 'warning',
        message: `Image ${i + 1} missing alt text`,
      });
    }
  });

  return issues;
}

That runs in milliseconds per file. Even for a site with 500 pages, your looking at maybe 5-10 seconds total.

The GitHub Action Setup

Here’s a minimal GitHub Action that catches the most common SEO regressions:

# .github/workflows/seo-lint.yml
name: SEO Lint
on:
  pull_request:
    paths:
      - 'src/**/*.html'
      - 'src/**/*.tsx'
      - 'src/**/*.jsx'
      - 'content/**/*.md'
      - 'public/**'

jobs:
  seo-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build site
        run: npm run build

      - name: Run SEO linter
        run: npx ts-node scripts/seo-lint.ts ./out

      - name: Check for URL changes
        run: |
          # Compare sitemap against main branch
          git diff origin/main -- public/sitemap.xml > /tmp/sitemap-diff.txt
          if [ -s /tmp/sitemap-diff.txt ]; then
            echo "::warning::Sitemap has changed - verify redirects for removed URLs"
          fi

The key is the paths filter. This only runs when files that could affect SEO are changed. No point linting SEO on a backend API change.

Catching URL Changes

This is the one people always miss. Someone renames a route or changes a slug, and the old URL returns a 404. No redirect. And if that old URL had backlinks or was ranking for anything, thats just gone.

// Compare URLs between builds to catch missing redirects
interface URLDiff {
  removed: string[];
  added: string[];
  changed: string[];
}

function compareURLSets(
  previousURLs: string[],
  currentURLs: string[]
): URLDiff {
  const prevSet = new Set(previousURLs);
  const currSet = new Set(currentURLs);

  const removed = previousURLs.filter(url => !currSet.has(url));
  const added = currentURLs.filter(url => !prevSet.has(url));

  return { removed, added, changed: [] };
}

function validateRedirects(
  removedURLs: string[],
  redirectRules: Map<string, string>
): string[] {
  const missing: string[] = [];

  for (const url of removedURLs) {
    if (!redirectRules.has(url)) {
      missing.push(url);
    }
  }

  return missing; // These need redirects before deploy
}

You can store your previous build’s URL list as an artifact and compare against the current build. If URLs were removed without redirects, fail the build. Simple as that.

The Structured Data Check

If your using JSON-LD structured data (and you should be), validate it in CI. Broken structured data means losing rich snippets in search results.

// Basic JSON-LD validation
function validateStructuredData(html: string): SEOIssue[] {
  const issues: SEOIssue[] = [];
  const dom = new JSDOM(html);
  const scripts = dom.window.document.querySelectorAll(
    'script[type="application/ld+json"]'
  );

  if (scripts.length === 0) {
    issues.push({
      file: '',
      severity: 'warning',
      message: 'No structured data found',
    });
    return issues;
  }

  scripts.forEach((script, i) => {
    try {
      const data = JSON.parse(script.textContent || '');
      if (!data['@context'] || !data['@type']) {
        issues.push({
          file: '',
          severity: 'error',
          message: `Structured data block ${i + 1}: missing @context or @type`,
        });
      }
    } catch (e) {
      issues.push({
        file: '',
        severity: 'error',
        message: `Structured data block ${i + 1}: invalid JSON`,
      });
    }
  });

  return issues;
}

Google’s structured data guidelines are strict about valid JSON-LD. A single syntax error means the entire block is ignored.

Performance Budget (the Smart Way)

Instead of running full Lighthouse in CI, set a performance budget based on file size and resource count:

// Performance budget checker - instant, no browser needed
interface Budget {
  maxHTMLSize: number;     // bytes
  maxTotalJSSize: number;   // bytes
  maxImageCount: number;
  maxThirdPartyScripts: number;
}

const DEFAULT_BUDGET: Budget = {
  maxHTMLSize: 100_000,      // 100KB
  maxTotalJSSize: 500_000,   // 500KB
  maxImageCount: 20,
  maxThirdPartyScripts: 5,
};

This isnt as thorough as a real Lighthouse audit, but it catches the biggest performance regressions (someone added a 2MB library, someone embedded 50 images) without the overhead.

The Point

SEO checks in CI shouldnt be comprehensive. They should be fast and catch the things that will actually hurt you. A 5 second lint that catches missing titles and broken canonicals is worth way more than a 7 minute Lighthouse run that nobody waits for.

Start small. Add the HTML linter. Add the URL comparison. Run it only on relevant file changes. Your pipeline stays fast, and you stop deploying SEO regressions.

The canonical tag incident i mentioned at the start? Would have been caught by a 3 line check in CI. Three lines of code versus 6 weeks of recovery. Seems like a good trade.

Delegation Without Awareness Is Still a Decision

“The model suggested it.”

“We let AI handle that part.”

“It just evolved.”

Those phrases sound neutral. They aren’t.

Delegating execution is easy to spot. You assign a task, review the output, and move on. Delegating judgment is quieter, especially when no single moment feels like the decision.

That’s what makes it dangerous.

In practice, decisions still get made. Scope gets set. Tradeoffs get accepted. Constraints harden. The only difference is that no one remembers choosing them.

AI didn’t take responsibility.

Responsibility was never explicitly claimed.

Because AI outputs look complete and reasonable, it’s easy to mistake motion for intent. A suggestion becomes a direction. A default becomes a decision. Over time, those small, unexamined choices become part of the structure.

And when something finally feels off, there’s no clear place to look.

No one can point to a moment where the call was made. No one feels fully accountable. Everyone agrees the outcome isn’t ideal, but ownership is thin and distributed.

That’s the trap.

When no one realizes they’re deciding, accountability doesn’t disappear; it fragments. And fragmented accountability behaves as if there were none at all.

AI didn’t create this dynamic.

It just made it easier to slip into it quietly.

Leadership takeaway

Delegating execution is not the same as delegating judgment. Implicit delegation quietly fragments accountability.

Action cues

  • Notice decisions that “just sort of happened”
  • Pay attention to outcomes no one can trace back to intent
  • Watch AI become a stand-in for consensus

When your agent needs to spend more than you told it to

You deploy a research agent with a $100 budget. The task is competitive landscape analysis: a few market reports, some API calls, a handful of web lookups. $100 is plenty.

Three weeks later, the scope changes. You need licensing data across 40 markets instead of 5. Same agent, same task, fundamentally different cost. Doing it properly runs $800.

What happens next depends entirely on how you set the budget. If you hardcoded a ceiling, the agent stops midway and you get a partial result. If you didn’t, it proceeds and you find out when you check your Stripe dashboard. Neither outcome is what you wanted. What you actually wanted was for the agent to ask you first.

That gap (between “agent has a spending limit” and “agent can request authorization before exceeding it”) is the problem Stripe’s Machine Payments Protocol (MPP) exposes but doesn’t solve.

What MPP actually does

MPP is an open standard for agent-to-service payments. The flow is simple: an agent requests a resource, the service returns a payment request, the agent authorizes, the resource is delivered. No accounts, no UI flows, no human involved. Stripe users can accept payments over MPP in a few lines of code.

The examples are real. Browserbase lets agents pay per browser session. PostalForm lets agents print and mail physical letters. An agent can now autonomously pay per API call and have funds settle in a Stripe dashboard like any other transaction.

This is not theoretical. It shipped this week.

But look at step three in that flow: “the agent authorizes.” That step is doing a lot of quiet work. It assumes the agent knows whether it should pay. Right now, that knowledge comes from static configuration you set before the agent ran.

The static config model

Today, agent spending limits live in environment variables. AGENT_BUDGET=500. Or a config file. Or hardcoded logic: if the cost is under $10, proceed; otherwise, stop.

This model works for predictable, bounded tasks. An agent paying $0.01 per browser session, with a hard cap you set at deploy time. Fine. The cost profile is known in advance.

It breaks the moment context changes.

The $100 budget you set isn’t wrong. It was right for the original scope. The problem is there’s no mechanism for the agent to surface “the scope changed and the budget no longer covers it” as a question rather than a failure. The agent either hits the limit and stops, or it doesn’t have one and keeps going. The two failure modes are “agent gives up” and “surprise charges.”

What’s missing is the third option: the agent pauses, routes the question to the person responsible for it, and waits for a response before continuing.

Why that’s harder than it sounds

The obvious solution is a Slack webhook. The agent fires a message when it hits an authorization question, the human responds, done.

Except webhooks are one-way. The agent fires and forgets. It can’t block on a webhook response. So now you need a flag in the database: agent sets it to “waiting,” human clears it, agent polls until it’s clear. This works. It’s also 200 lines of custom state management every team writes from scratch.

Then you have the response-in-context problem. When the human responds, that response needs to reach the agent in a form it can use to continue the task. Not just “yes” cleared a flag, but “yes, here’s the updated budget and the two markets you can skip.” Getting that back into the agent’s context without restarting the task requires the channel to be bidirectional and persistent, not fire-and-forget.

And then you have the multiple-agent problem. One agent asking one human is manageable. Five agents, three humans, overlapping tasks: you need routing. Which agent’s question goes to which human? How does the human know what context the question came from? How does the agent know its question is pending vs. lost?

Every team building HITL at any scale hits this progression. Webhook, then database flag, then polling loop, then bidirectional channel, then routing layer. The final result works. It’s also a custom communication infrastructure that has nothing to do with the actual task the agent was deployed to do.

What the authorization layer actually needs

Three things, in order of how often they’re missing:

A persistent bidirectional channel. Not a webhook, not a polling loop against a database flag. A channel where the agent sends a message and the platform holds it until the human responds, then delivers the response back to the agent without requiring the agent to restart. Blocking semantics, not fire-and-forget.

Policy primitives with escalation tiers. The binary model (“spend under $X, stop above it”) doesn’t handle context. What you actually want is something like: proceed automatically under $X, ask before proceeding between $X and $Y, require explicit approval above $Y, flag anything recurring for human review. That’s not three environment variables. That’s a policy layer that understands cost tiers and routes accordingly.

An audit trail that links transactions to authorization decisions. Stripe gives you transaction history. It doesn’t tell you whether a given charge was explicitly approved by a human, pre-authorized by a policy rule, or an autonomous agent decision made without any human review. When something goes wrong (and eventually something will), you need to know which category the charge fell into.

Why this matters now

Before MPP, this problem was mostly theoretical. Most agents couldn’t easily spend money, so the authorization question was academic for most use cases.

Stripe just changed that. Three lines of code and your agent has a payment credential. Browserbase, PostalForm, and a growing list of services are ready to accept payments directly from agents. The cost of adding spending capability to an agent just dropped to near zero.

That means the gap between “agent has spending capability” and “agent has authorization infrastructure” just became real infrastructure work. The static config model will hold for simple, predictable deployments. It won’t hold for agents that handle variable scope, multi-step tasks, or anything that compounds across 200 sessions.

MPP made the payment rail. The authorization layer is the next piece. Right now every team is building it from scratch, badly, in the same ways. That’s usually the signal that it should be infrastructure.

Naomi Kynes builds agent infrastructure. GitHub: github.com/naomi-kynes

İKÇÜ Film Festivali'nin finalistleri belli oldu

İzmir Katip Çelebi Üniversitesi tarafından düzenlenen İKÇÜ Film Festivali’nin finalist filmleri belirlendi. Festivalde kısa kurmaca, kısa belgesel, öğrenci filmleri ile animasyon ve yapay zeka kategorilerinde ödüller sahiplerini bulacak.

My friend unplugged my Raspberry Pi — so I built a homelab manager

It started with an accident.
My friend was at my place, tripped over a cable,
and unplugged my Raspberry Pi. The SD card got
corrupted. Network partition gone. Files — gone.
I had to re-flash everything from scratch.
No backup. No remote monitoring. No alerts.
I didn’t even know it was down until I tried
to SSH in the next day.
That’s when I decided: never again.

The Problem

If you run a homelab, you probably know the drill:

  • SSH into each server manually
  • Run docker ps to check containers
  • Forgot which port Uptime Kuma is on
  • No idea if your disk is 90% full
  • Deploying a new app means writing YAML by hand

I had 2 servers (Mac Mini + Raspberry Pi 5).
Managing them was death by a thousand SSH sessions.

What I Built

homebutler
— a single 15MB Go binary that manages
your entire homelab.

# Check all servers at once
homebutler status --all
# Install an app in 30 seconds
homebutler install uptime-kuma
# Container resource usage
homebutler docker stats
# Wake a sleeping machine
homebutler wake nas

No Docker required to run it. No database.
No config files to write (there’s an init wizard).
Just download and go.

One-Command App Install

This is the feature I’m most proud of.
Instead of writing docker-compose files:

homebutler install jellyfin --media /mnt/movies

That’s it. It:

  1. Checks if Docker is running
  2. Checks if the port is available
  3. Generates docker-compose.yml
  4. Pulls the image and starts the container
  5. Verifies it’s running

Currently supports 6 apps: uptime-kuma,
vaultwarden, filebrowser, it-tools, gitea,
and jellyfin.

Built-in AI Integration

homebutler includes a built-in MCP server
with 15 tools. Connect it to Claude, ChatGPT,
or Cursor, and you can manage your homelab
from chat:

  • “Install uptime-kuma on the raspberry pi”
  • “What’s the CPU usage on all servers?”
  • “Restart the nginx container”

The AI part is completely optional.
homebutler works perfectly as a standalone CLI.

The Tech

  • Go — single binary, cross-compiled for Linux/macOS (ARM + x86)
  • cobra — CLI framework with auto-generated help and shell completion
  • go:embed — web dashboard baked into the binary
  • Bubble Tea — terminal TUI dashboard
  • No frameworks for HTTP — just net/http

The whole thing is 15MB. Zero runtime dependencies.

What’s Next

  • More installable apps (targeting 15+)
  • Scheduled backups with one-command restore
  • Server down notifications (Telegram/Discord)
  • Reverse proxy auto-configuration

Try It

brew install Higangssh/homebutler/homebutler
# or
curl -fsSL https://raw.githubusercontent.com/Higangssh/homebutler/main/install.sh | sh

Then:

homebutler init    # setup wizard
homebutler status  # see your server

GitHub: https://github.com/Higangssh/homebutler

Demo video: https://www.youtube.com/watch?v=MFoDiYRH_nE

What’s your homelab stack?
I’d love to know what apps you’d want homebutler to support next.

CA 40 – Alter Tables

This exercise was about ALTER TABLE. Instead of creating table again, we modify existing table like adding constraints, defaults, columns etc.

  1. Make email NOT NULL in customers table

sql
id=”h4r9b2″
ALTER TABLE customers
ALTER COLUMN email SET NOT NULL;

This makes sure future rows must have email.

  1. Make username unique in users table

sql
id=”9zv1qx”
ALTER TABLE users
ADD CONSTRAINT unique_username UNIQUE (username);

This prevents duplicate usernames.

  1. Add check constraint price > 0 in products table

sql
id=”g6p8la”
ALTER TABLE products
ADD CONSTRAINT price_check CHECK (price > 0);

Now price cannot be zero or negative.

  1. Set default status ‘pending’ in orders table

sql
id=”s8k3po”
ALTER TABLE orders
ALTER COLUMN status SET DEFAULT ‘pending’;

If status not given, it will automatically become pending.

  1. Add salary column in employees table with conditions

sql
id=”x2w7mn”
ALTER TABLE employees
ADD COLUMN salary INT NOT NULL;

ALTER TABLE employees
ADD CONSTRAINT salary_check CHECK (salary > 10000);

Salary cannot be null and must be > 10000.

  1. Modify foreign key so delete department deletes employees

sql
id=”f5t9yr”
ALTER TABLE employees
DROP CONSTRAINT employees_department_id_fkey;

ALTER TABLE employees
ADD CONSTRAINT employees_department_id_fkey
FOREIGN KEY (department_id)
REFERENCES departments(id)
ON DELETE CASCADE;

Now deleting department deletes employees automatically.

  1. Remove check constraint balance >= 0 from accounts table

sql
id=”k7d3qp”
ALTER TABLE accounts
DROP CONSTRAINT accounts_balance_check;

This removes the check constraint.

  1. Make combination of user_id and transaction_id unique in payments table

sql
id=”m1c8zs”
ALTER TABLE payments
ADD CONSTRAINT unique_payment UNIQUE (user_id, transaction_id);

This prevents duplicate transaction per user.

From this exercise I learned ALTER TABLE is used to modify table structure without deleting table. Very useful when database already has data.