This is the last post in the series, and I want to write it the way I’d actually talk about this project to another developer over coffee, not as a tidy list of best practices. Some of these decisions I’d make again immediately. A couple I’m still not sure about.

What held up

The two-guard auth model, JwtAuthGuard globally protecting the admin dashboard and a completely separate VirtualKeyGuard for the gateway, never once caused confusion once it existed. Every new controller I added just worked correctly by default, because the default is “protected,” and the gateway module made its opt-out explicit and obvious with @Public(). I’d design that the same way again.

Keeping money-related logic (budget enforcement) in Postgres, and only the cheap, approximate stuff (rate-limit counters) in Redis, was the right split. I never had to reconcile two sources of truth about spend, because there was only ever one. If I’d been tempted to put everything in Redis for speed, I think I’d have regretted it the first time a Redis restart or a missed write left the numbers wrong.

Not modeling libs/frontend/ui or any generic shared-component library ahead of time, and only extracting @gatify/contracts and @gatify/data-access once actual duplication showed up, was the right call too. I’ve seen codebases with a beautiful library structure and almost nothing inside it worth the abstraction. Waiting for the pain to be real before reaching for the fix meant every library in this repo earns its place.

Skipping strict DTO validation on the gateway’s /v1/* routes, in favor of passthrough compatibility with whatever the OpenAI SDK sends, was a genuine tradeoff and I still think it was the right one for what this project is. It means Gatify isn’t as defensively validated at that boundary as the admin API is. I accepted that because the entire point of an OpenAI-compatible surface is staying compatible, and a strict whitelist DTO would break that promise the first time OpenAI ships a new optional field I hadn’t modeled yet.

What I’d do differently

The login bug from the test-chat chapter (the Vite pre-bundling and missing tsconfig paths issue) is a symptom of something I should have set up on day one of extracting the shared libraries: explicit path mappings, not implicit npm workspace symlink resolution. I got lucky that this surfaced as a loud, unmissable syntax error rather than something quieter. If I set up another Nx workspace with shared libraries tomorrow, tsconfig.base.json paths get added in the same commit as the library, not two commits later once something breaks.

I’d write the Dockerfiles and the Coolify deployment much earlier in the project, maybe right after the first backend feature was working, rather than as the very last chapter. Every failure in the deployment diary chapter was a surprise precisely because production-shaped constraints (no devDependencies by default, a different base image, an Nginx health check that isn’t tied to the app itself) never got exercised until the very end, all at once. Deploying early and often, even to a throwaway environment, would have caught the apk-versus-Debian mismatch and the missing postcss.config.json months of mental effort earlier, when they’d have been one isolated problem each instead of a debugging marathon at the end.

I also went back and forth more than I’d like to admit on whether to build the rate limiter myself versus reaching for an existing NestJS throttling library. I built it myself, and I think it was the right call given how specific the token-per-minute semantics needed to be (checking before the call, recording actual usage after), but it took longer than a library would have, and I want to be honest that “I built it myself” isn’t automatically the better answer just because it’s more interesting to write about.

What’s next

The obvious next step is adding more providers beyond Azure OpenAI and Azure AI Foundry, since the ProviderType enum and the client abstraction were built with that in mind even though only Azure exists today. I also want proper structured logging and a real alerting path for budget thresholds, right now the only signal that a key is close to its budget is checking the dashboard by hand. Multi-admin support is explicitly not on the list, this is a personal gateway for personal Azure credits, and I don’t want to build out a permissions system for a use case that doesn’t exist yet.

If you made it through this whole series, thank you for reading. The repo is public, the commit history is real, and if you spot something in here that you’d have done differently, I’d genuinely like to hear it.