<feed xmlns="http://www.w3.org/2005/Atom"> <id>https://nogamsung.github.io/</id><title>nogamsung</title><subtitle>The engineering blog of product engineer Geomseong Ahn (nogamsung). Spring Boot, JPA, Next.js, real-time messaging, auth, zero-downtime deploys, and AI coding agents: the problems I hit in production and why I made the calls I did.</subtitle> <updated>2026-11-13T09:57:46+09:00</updated> <author> <name>Geomseong Ahn</name> <uri>https://nogamsung.github.io/</uri> </author><link rel="self" type="application/atom+xml" href="https://nogamsung.github.io/en/feed.xml"/><link rel="alternate" type="text/html" hreflang="en" href="https://nogamsung.github.io/en/"/> <generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator> <rights> © 2026 Geomseong Ahn </rights> <icon>/assets/img/favicons/favicon.ico</icon> <logo>/assets/img/favicons/favicon-96x96.png</logo> <entry><title>Keeping call audio alive in the background by registering in-app calls with the OS</title><link href="https://nogamsung.github.io/en/posts/callkit-background-call-audio/" rel="alternate" type="text/html" title="Keeping call audio alive in the background by registering in-app calls with the OS" /><published>2026-11-11T00:00:00+09:00</published> <updated>2026-11-11T00:00:00+09:00</updated> <id>https://nogamsung.github.io/posts/callkit-background-call-audio/</id> <content type="text/html" src="https://nogamsung.github.io/posts/callkit-background-call-audio/" /> <author> <name>Geomseong Ahn</name> </author> <category term="frontend" /> <category term="realtime" /> <summary>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.</summary> </entry> <entry><title>Managing server env files with GitHub Environment Secrets and Kamal, hiding values but keeping failure reasons</title><link href="https://nogamsung.github.io/en/posts/github-environment-secrets-kamal-env/" rel="alternate" type="text/html" title="Managing server env files with GitHub Environment Secrets and Kamal, hiding values but keeping failure reasons" /><published>2026-11-10T00:00:00+09:00</published> <updated>2026-11-10T00:00:00+09:00</updated> <id>https://nogamsung.github.io/posts/github-environment-secrets-kamal-env/</id> <content type="text/html" src="https://nogamsung.github.io/posts/github-environment-secrets-kamal-env/" /> <author> <name>Geomseong Ahn</name> </author> <category term="devops" /> <category term="deployment" /> <summary>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.</summary> </entry> <entry><title>Measuring and cutting monorepo deploy time, from a 35-minute full backend deploy to 13</title><link href="https://nogamsung.github.io/en/posts/monorepo-deploy-time-reduction/" rel="alternate" type="text/html" title="Measuring and cutting monorepo deploy time, from a 35-minute full backend deploy to 13" /><published>2026-11-09T00:00:00+09:00</published> <updated>2026-11-09T00:00:00+09:00</updated> <id>https://nogamsung.github.io/posts/monorepo-deploy-time-reduction/</id> <content type="text/html" src="https://nogamsung.github.io/posts/monorepo-deploy-time-reduction/" /> <author> <name>Geomseong Ahn</name> </author> <category term="devops" /> <category term="ci-cd" /> <summary>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.</summary> </entry> <entry><title>Zero-downtime deploys on an on-prem server with Kamal</title><link href="https://nogamsung.github.io/en/posts/kamal-zero-downtime/" rel="alternate" type="text/html" title="Zero-downtime deploys on an on-prem server with Kamal" /><published>2026-11-04T00:00:00+09:00</published> <updated>2026-11-04T00:00:00+09:00</updated> <id>https://nogamsung.github.io/posts/kamal-zero-downtime/</id> <content type="text/html" src="https://nogamsung.github.io/posts/kamal-zero-downtime/" /> <author> <name>Geomseong Ahn</name> </author> <category term="devops" /> <category term="deployment" /> <summary>A Watchtower deploy that recreated containers turned a single redeploy into 503s across most of our services. This post covers how I compared hand-rolled blue-green, Kamal, Coolify and Docker Swarm, why I picked Kamal, and the problems I hit moving over: Tailscale-only SSH, service-to-service calls, dev and prd on one server, SMTP, scheduled jobs and monitoring.</summary> </entry> <entry><title>Roles changed, but the old ones kept working for 30 minutes: putting a permission version in the JWT</title><link href="https://nogamsung.github.io/en/posts/jwt-permission-version/" rel="alternate" type="text/html" title="Roles changed, but the old ones kept working for 30 minutes: putting a permission version in the JWT" /><published>2026-11-03T18:00:00+09:00</published> <updated>2026-11-03T18:00:00+09:00</updated> <id>https://nogamsung.github.io/posts/jwt-permission-version/</id> <content type="text/html" src="https://nogamsung.github.io/posts/jwt-permission-version/" /> <author> <name>Geomseong Ahn</name> </author> <category term="backend" /> <category term="spring" /> <summary>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.</summary> </entry> </feed>
