nogamsung
Keeping call audio alive in the background by registering in-app calls with the OS

Keeping call audio alive in the background by registering in-app calls with the OS

Calls built with Flutter and LiveKit lost audio when the user went to the home screen, and the Dynamic Island never showed up. UIBackgroundModes audio alone wasn't enough; the fix was registering outgoing calls and calls accepted in-app with CallKit and Android Telecom too, and making CallKit the single owner of the audio session. Also covers re-attaching audio in web calls and handling video calls in the background.

Managing server env files with GitHub Environment Secrets and Kamal, hiding values but keeping failure reasons

Managing server env files with GitHub Environment Secrets and Kamal, hiding values but keeping failure reasons

How I moved env files we used to edit with vi on the server into a single GitHub Environment Secret bundle, and made every deploy atomically rewrite the server file Kamal reads. It covers keeping values out of every log while still reporting failures by line number and fixed category codes, a safeguard that stops before swapping containers if the first cutover doesn't match the old server values, and an SSHKit host key pinning issue.

Measuring and cutting monorepo deploy time, from a 35-minute full backend deploy to 13

Measuring and cutting monorepo deploy time, from a 35-minute full backend deploy to 13

I measured 73 deploys of a monorepo with 10 backends and 13 web apps: 60% were full deploys, caused by a single response-code enum in a shared module and one chown -R line in the Dockerfile. Splitting response codes per service, deploying each image as soon as it's ready, and building and checking only what the real dependency graph says is affected for web and Flutter brought a full deploy from 35.6 to 13.7 minutes.

Roles changed, but the old ones kept working for 30 minutes: putting a permission version in the JWT

Roles changed, but the old ones kept working for 30 minutes: putting a permission version in the JWT

The roles claim in a stateless JWT is fixed at issue time, so when an admin changed someone's permissions, menus and APIs kept running on the old ones for the token's lifetime (30 minutes). Instead of a blacklist, we put the time permissions were computed (pv) into the token, compared it with a change timestamp in Redis, and cut stale tokens off with a 401. This post covers that design, a cache-reload race before commit, and why the gate fails open.

Adding circuit breakers to internal service calls, and switching permission checks from fail-open to fail-closed

Adding circuit breakers to internal service calls, and switching permission checks from fail-open to fail-closed

Nine service APIs ask the account service for permissions, and the old design allowed the request when that lookup failed (fail-open), which meant an outage was a permission bypass. This post covers four per-call-site Resilience4j circuits, what counts as a failure (not 4xx), why an open circuit isn't wired to liveness, the decision to make permission checks fail closed with a 503, and the deploy gap that showed up right after.

One harness for Claude Code and Codex — from CLAUDE.md to AGENTS.md

One harness for Claude Code and Codex — from CLAUDE.md to AGENTS.md

So that everything still runs on one side if the other's subscription lapses — and with no config file maintained twice by hand. The five PRs from one day: switching the instruction file to a single AGENTS.md, moving the build and coverage gates from agent hooks to git hooks, gathering 32 skills into one folder, and having the opposite model review every PR. Plus the story of "the hook that had never blocked anything" and "the git hooks that had never run".

Upgrading to Spring Boot 4, and finding out our LAZY was really EAGER because the entities were final

Upgrading to Spring Boot 4, and finding out our LAZY was really EAGER because the entities were final

What I learned upgrading 18 modules from Spring Boot 3.4 to 4.1 and Kotlin 1.9 to 2.3 (K2) in a day. Kotlin 2.x's plugin.jpa turns on all-open, so entities that had been final became open, which revealed that 67 @ManyToOne(LAZY) associations had been running as EAGER all along, plus 179 fake fetch joins. Also covers how Jackson 3 made every request fail with a 408 in 39 ms.