the walkthrough
Six phases, months apart between some of them (auth, then blog, then the frontend, the admin panel, uploads, then Caddy and compose tying all of it together), and none of them had ever actually run together against the live stack in one sitting until the real launch check: register a real account, comment on a post, like it, report it, log in as the seeded admin, write a post with an image and a video attached, moderate a comment, ban a user straight from one of their own comments. The kind of walkthrough that's supposed to be the easy final step once every individual service already has its own test suite passing.
Mostly it went fine, which was almost disappointing after building this thing for so long half expecting fireworks. Register worked, comment worked, the like count updated without a refresh, reporting a comment queued it in the admin panel exactly where it should, and writing a post with an actual image and a real video attached went through the whole upload path from the last few of these entries without a single hiccup. Banning a user straight from one of their own comments, something I'd only ever exercised through individual handler tests before, worked end to end on the first real try too.
the cookie that quietly expires your session
The one real bug the walkthrough turned up wasn't inside any single service, it only showed up once everything ran together for more than about 15 minutes. Login is supposed to keep you signed in for 30 days, that's the entire point of the refresh token, but the silent-refresh logic in hooks.server.ts tries to read the refresh cookie on every ordinary page load:
if (!sessionFromAccessToken) {
const refreshToken = event.cookies.get('refresh_token');
if (refreshToken) {
const response = await event.fetch('/auth/refresh', { method: 'POST' });
...
}
}Except that cookie is scoped to Path=/auth, on purpose, so it never gets sent to the auth service on requests that don't actually need it, keeping it out of, say, MinIO's own access logs on an unrelated media request. Which also means the browser never sends it here either, on an ordinary page route, since none of this code runs under /auth at all. event.cookies.get('refresh_token') was never going to find anything, every single time, on any normal page in the site.
The access token alone only lasts 15 minutes. So the practical effect, unnoticed until I actually left a tab open and came back to it later, is that "stay logged in for 30 days" quietly meant "stay logged in for about 15 minutes," then a perfectly normal-looking redirect back to the login page for no reason anyone would guess without reading this exact chain of cookie scoping rules. Fixing it means either widening that cookie's path or making the refresh call from somewhere that's actually inside /auth's own scope, and I want to sit with that trade-off a bit before committing to one, rather than widen the path just because it's the smaller diff.
the comment 413 that never actually happened, as far as I can tell
Somewhere in an earlier testing session I hit a 413 posting an entirely normal-length comment, nothing weird about it at all, and it worried me enough to write it down as a real bug to fix. Went back later specifically to chase it down and couldn't reproduce it, not at 100 bytes, not at 9 kilobytes, not right up near the actual 10,000-character content limit. Blog's own JSON decode limit for a comment is 64 kilobytes, miles above anything a real comment would ever hit, and nothing else in the stack should be anywhere near that tight either.
Best guess, and it's genuinely just a guess: whatever I hit that day was almost certainly a leftover from an earlier, far more restrictive BODY_SIZE_LIMIT I'd been testing with at the time, something transitional like 3 megabytes or even a placeholder of zero, not a real limit that ever shipped. Wrote a regression test posting a 9 kilobyte comment straight through Caddy to lock in that it works correctly now, and left the mystery exactly as unsolved as that, an old config ghost rather than a live bug still hiding somewhere. Not everything scary you hit while building something turns out to be a real thing once you go looking for it properly.
what's still sitting on the list
The walkthrough itself passed. What's left isn't a bug, it's just the actual release, and I'm deliberately treating it as its own separate decision rather than something that happens automatically the moment tests go green: merging dev into main, pushing to the real git remote and watching the CI pipeline actually go green against a real container registry instead of my own machine, then running the production compose overlay against either a real domain or a throwaway one just to watch Caddy's auto-TLS actually try to do its thing for the first time. None of that has happened yet. All six phases are built and merged to dev, and every one of those remaining steps is explicitly mine to trigger when I'm ready, not something a script decides for me.
what actually stuck with me
Nothing here is complicated in isolation. Hashing a password, proxying a request, capping a body size, none of it is hard on its own. What actually took the time was everything meeting everything else: a cookie path decided for one service's own logging concerns quietly breaking a completely unrelated frontend assumption three services away, a size limit at the edge silently shadowing a much more generous one two layers further in, a client-side role check that looks exactly like security until you actually trace what happens the moment you lie to it.
Started this whole project mostly because I didn't want to just use WordPress. Ended up learning more about how the pieces of a real web service actually fit together, and fail together, than any tutorial ever bothered to show me. Django and CS50 taught me the syntax. Building this taught me where things actually break.
Still haven't merged this to main. That part's still mine to decide when.