Blog
Real problems from real projects: what broke, why it broke, and how I fixed it.
- DebuggingNov 7, 20264 min read
My site worked everywhere except on my own internet
My portfolio loaded fine on a VPN and on my phone's mobile data, but on my home internet it spun forever and ended in a browser error. The code was fine. The problem was a redirect to www and a few server IPs my ISP couldn't reach.
Problem: On my home ISP the site loaded forever and failed, while a VPN or mobile data worked, because the apex domain redirected to a www address whose IPs my ISP couldn't reach.
#DNS#Vercel#Networking#Redirects#SEORead the story → - BackendNov 6, 20263 min read
When imgur goes down, my server shouldn't
My portfolio served project screenshots from imgur through Next.js image optimization. Every time imgur had a bad day, my own server paid for it. Here's why, and how I moved that work off the server.
Problem: Slow or failing imgur requests made my own server hang, because Next.js fetched every remote image server-side.
#Next.js#Resilience#Image Optimization#Third-party ServicesRead the story → - DevOpsNov 5, 20269 min read
Deploys that can't hang and can't lose data: a safer Prisma db push entrypoint
A container that loops forever on "DB not ready" while the real problem is a refused schema change. A deploy that could silently drop a column. Here is how I rewrote a Docker entrypoint to wait with a limit, print the exact SQL before applying it, refuse destructive changes, and follow one golden rule: additive changes only.
Problem: Deploys could hang forever behind a misleading "DB not ready" message, and nothing stopped a schema change that loses production data.
#Docker#Prisma#PostgreSQL#Deployment#Database Migrations#DevOpsRead the story → - BackendNov 4, 20268 min read
From any SQL or JSON to dropdown options: building a strict mapping pipeline
Every data source returns a different shape: flat SQL rows, nested API responses, arrays inside objects. The UI only wants label and value. Here is the mapping pipeline I built: a JSON skeleton of the result, dotted paths that walk through arrays, validation against real data, and a strict mode that fails loudly instead of returning silent nulls.
Problem: Sources return wildly different shapes, and mappings that silently produced null values hid broken or outdated configurations.
#Data Mapping#JSON#API Design#Validation#TypeScript#Node.jsRead the story → - SecurityNov 4, 20267 min read
Letting users write SQL against your databases (without letting them break anything)
Our data source service lets users save their own SQL queries and run them against real company databases. That's powerful and dangerous. Here is the read-only guard I built, why it strips comments and uses word boundaries, where it's checked, and why a keyword filter must only be one layer of defense among several.
Problem: Users can save arbitrary SQL that runs against shared production databases, so one careless or malicious query could modify or destroy data.
#SQL#Security#PostgreSQL#Input Validation#Defense in Depth#Node.jsRead the story → - BackendNov 3, 20267 min read
Did the data really change? Detecting real changes with canonical JSON hashing
A nightly job refreshes hundreds of stored results. To react only to real changes, I fingerprint each result with a hash. But PostgreSQL jsonb doesn't keep key order, so the same data can hash differently every night. Here is how canonical JSON fixes that, why array order must stay, and how a single changed flag becomes useful.
Problem: Comparing refreshed results with plain JSON.stringify would report changes that never happened, because the same data can come back with keys in a different order.
#Hashing#JSON#PostgreSQL#Change Detection#Node.js#Data SyncRead the story → - BackendNov 2, 20268 min read
Transient vs. permanent errors: when to retry, and what a public API is allowed to say
Not all errors deserve the same treatment. A full database server will be fine in a second; a broken SQL query never will. Here is how I classify errors by PostgreSQL SQLSTATE and Node.js errno codes, retry only what can succeed, and expose short, safe labels on public endpoints while keeping full details in the logs.
Problem: Every failure looked the same: retries were impossible to decide, the public response was a useless null, and real details were either lost or at risk of leaking.
#Error Handling#Retries#PostgreSQL#Node.js#API Design#SecurityRead the story → - PerformanceNov 1, 20262 min read
A 32 MB JavaScript chunk: how a few icons broke my dev server
My dev server kept throwing ChunkLoadError timeouts. The root layout chunk had grown to 32 MB, and almost all of it came from importing a handful of icons.
Problem: Random ChunkLoadError: Loading chunk app/layout failed (timeout) in development.
#Next.js#Webpack#Bundle Size#DebuggingRead the story → - ArchitectureNov 1, 20269 min read
Stop querying live: stored results and a nightly sync job that can't hurt anyone
The best way to stop overloading a shared database is to stop asking it questions on the user's path. Here is how I moved a data service from live queries to stored results, with a slow nightly sync, refresh-on-edit, self-healing broken sources, and a job that survives restarts, races and impatient users.
Problem: Every batch request ran live queries against a shared, overloaded database server, so users paid the cost and the risk on every request.
#Caching#Background Jobs#Cron#PostgreSQL#NestJS#System DesignRead the story → - DebuggingNov 1, 20263 min read
The black screen: how one failed texture unmounted my whole app
Sometimes my home page loaded as a completely black screen. No header, no content, nothing. The cause was a 3D scene, eight remote images, and a missing error boundary.
Problem: Occasionally the home page rendered as a fully black screen with nothing loaded.
#React#Next.js#Three.js#Error HandlingRead the story → - BackendOct 31, 20269 min read
One pool per database, one budget per server: connecting to many PostgreSQL databases at once
A service that queries many user-defined databases can't size one connection pool and call it done. Here is how I built lazy pools per connection string that share a fixed connection budget per server, rebalance themselves when a new database appears, and never crash on a dropped idle connection.
Problem: The service connected to several databases on the same shared server, with no limit on the total connections it could open there.
#PostgreSQL#node-postgres#Connection Pooling#Node.js#Multi-database#PerformanceRead the story → - DebuggingOct 30, 20268 min read
Random nulls: the bug that hid behind a silent catch
Some data sources randomly came back as null, never the same ones twice, and those nulls were saved forever into cached configurator trees. The cause was a full shared database server, but the real villain was an empty catch block that turned a loud error into quiet, permanent data loss.
Problem: Random sources returned null in batch requests, a different set each time, and the nulls were stored permanently in cached data.
#Debugging#Error Handling#PostgreSQL#Node.js#Observability#Data IntegrityRead the story → - ArchitectureOct 29, 202610 min read
One engine, many services: how a configurator backend talks to auth, ERP, folders and data sources
A product configurator engine doesn't live alone. It depends on an auth service, an ERP, a folder service, external data sources, Redis and a shared PostgreSQL server. Here is the map of those connections, what each service owns, and the rules I follow so one slow neighbor doesn't take the whole engine down.
Problem: The engine depends on several other services, and each integration had its own way of failing: slow responses, missing data, duplicated records or unclear errors.
#Microservices#NestJS#Architecture#Integration#Resilience#RedisRead the story → - ArchitectureOct 27, 20268 min read
Keeping a local copy of users from another service: versioned push sync done right
Our users live in a central auth service, but the configurator engine needed their names, emails and team memberships locally. Here is how I built a copy that stays correct with full-list pushes, version numbers, a row lock against out-of-order updates, soft removal instead of deletes, and a pull sync to repair it.
Problem: The engine needed user names and team memberships, but users live in another service: calling it for every screen was slow and fragile, and naive syncing could apply old data over new.
#Microservices#Data Sync#PostgreSQL#Prisma#Event-driven#ConsistencyRead the story → - DatabaseOct 25, 20268 min read
Uniqueness across two tables: a PostgreSQL trigger and a safe batch update
A condition in our configurator engine is a logic tree made of two kinds of nodes stored in two tables. Their position numbers must be unique across both tables, and a normal UNIQUE constraint can't do that. Here is how a trigger guards inserts, and how a batch function reorders nodes across both tables without ever creating a collision.
Problem: Operations and comparisons share one numbering space inside a condition, but they live in two tables, so a standard UNIQUE constraint couldn't prevent duplicates between them.
#PostgreSQL#Triggers#PL/pgSQL#Data Integrity#SQL#PrismaRead the story → - DevOpsOct 24, 20268 min read
PostgreSQL + Prisma in Docker: migrations that are safe to run on every container start
I like running migrations automatically when the app container starts: no manual step, no forgotten migration. But Prisma can refuse to run on a database that was migrated by hand, and a build can fail just because there is no database yet. Here is my startup script, the empty-history baseline trick, a migrations folder that only ships what production still needs, and the small Docker details that make it boring.
Problem: Automatic migrations on container start failed on a hand-migrated production database (P3005), builds needed a database URL just to generate code, and the full local migration history didn't belong on the server.
#PostgreSQL#Prisma#Docker#Database Migrations#NestJS#DevOpsRead the story → - DatabaseOct 24, 20269 min read
Reordering a tree without breaking unique constraints: termnum, parentTermnum and DEFERRABLE
Our configurator trees are stored with a position number (termnum) and a parent position (parentTermnum), unique per configurator. Swapping two items breaks that unique constraint halfway through. Here is how PostgreSQL's DEFERRABLE constraints, a batch PL/pgSQL function and one transaction made drag-and-drop reordering safe.
Problem: Swapping or reordering items failed with unique-constraint errors, because every intermediate step created a temporary duplicate position.
#PostgreSQL#Prisma#SQL#Constraints#Transactions#Tree StructuresRead the story → - DevOpsOct 23, 20267 min read
The node_modules volume trick: live code in Docker without breaking dependencies
Mounting your source code into a container gives you live reload, and immediately breaks node_modules: your Mac's packages overwrite the container's Linux ones, or vanish completely. Here is the named-volume trick that keeps dependencies inside Docker, why each app (front and back) gets its own node_modules volume, and how it saves disk space and install time.
Problem: Bind-mounting source code into containers hid or overwrote the container's node_modules, causing native module crashes and slow, repeated installs.
#Docker#Node.js#Docker Compose#node_modules#Developer Experience#YarnRead the story → - SecurityOct 22, 20268 min read
Blocking a user mid-session: revoking JWTs before they expire
JWTs are great until you need to log someone out right now. A token valid for 8 hours stays valid for 8 hours. Here is how token versions in Redis make revocation instant on every HTTP request, why WebSockets needed an extra check, and the small design choices that keep the whole thing safe.
Problem: A logged-out or blocked user kept a valid 8-hour JWT, and an already-open WebSocket could keep acquiring edit locks with it.
#JWT#Authentication#Redis#WebSockets#NestJS#SecurityRead the story → - DevOpsOct 22, 20267 min read
Where the data lives: Docker volumes, dev vs. prod databases, dumps and a preconfigured pgAdmin
Containers are meant to be thrown away. Databases are not. Here is how I keep PostgreSQL, Redis and pgAdmin data safe across rebuilds with named volumes, why development and production never share a volume, which Docker command really deletes data, and how a committed dump plus a preconfigured pgAdmin gives every new developer a working database in one command.
Problem: Rebuilding containers risked losing database data, dev and prod data could mix, and every new developer started with an empty database and a manual pgAdmin setup.
#Docker#Docker Volumes#PostgreSQL#Redis#pgAdmin#BackupsRead the story → - DevOpsOct 21, 20268 min read
One command, a whole stack: running many services with Docker Compose that start in the right order
Each of my backend projects needs a database, a cache, the app, and admin tools. With Docker Compose, all of it starts with one command, in the right order, on any machine. Here is how I structure those stacks: health checks, dependency order, networks vs. host mode, port planning on a shared server, and the project-name trap that made a production stack destroy the dev one.
Problem: Every project needed several services running together, and setting them up by hand on each machine and server was slow, fragile and different every time.
#Docker#Docker Compose#PostgreSQL#Redis#DevOps#InfrastructureRead the story → - BackendOct 21, 20269 min read
Who can edit what: owners, group owners and claim-on-write
Locks stop two people from editing at the same time. Ownership decides who may edit at all. Here is how I designed multi-owner configurators with team ownership, an atomic claim-on-write rule, fail-closed defaults, and a kill switch that turns enforcement off with a restart instead of a release.
Problem: Anyone could edit any configurator, even ones they had nothing to do with, and adding a single "owner" column didn't fit how teams really work.
#Authorization#NestJS#Prisma#PostgreSQL#Access Control#GuardsRead the story → - DevOpsOct 20, 20267 min read
Deploying Next.js under a sub-path: basePath gotchas we hit in production
Running a Next.js app at /tools/auth-admin instead of / looks like a one-line config change. In production, it broke the post-login redirect, links to static files and environment handling. Here is what basePath does and doesn't prefix, the bug that sent users to the wrong URL after login, and the configuration that finally made it boring.
Problem: Behind a reverse proxy at a sub-path, the admin panel sent users to the wrong URL after login and broke links to static files.
#Next.js#basePath#Deployment#Docker#Nginx#Reverse ProxyRead the story → - FrontendOct 19, 20269 min read
A UI that prevents mistakes before the backend has to refuse them
A good backend refuses invalid actions with clear errors. A good admin panel makes sure the admin rarely meets them. Here is how the auth admin panel guides setup in the order the backend enforces, warns about broken configurations hours before a real user hits them, stays honest about what it can't know, and ships the integration docs inside the product.
Problem: Admins discovered misconfigurations through backend errors or, worse, through users who couldn't log in.
#UX#Admin Panel#React#Next.js#Developer Experience#DocumentationRead the story → - BackendOct 19, 20267 min read
Ghost locks: releasing everything when a user disconnects
Edit locks are easy to take and easy to forget. A closed tab, a sleeping laptop or a lost Wi-Fi connection can leave a configurator locked by someone who is gone. Here is how a socket reverse index, disconnect cleanup, TTLs and a separate viewer-presence layer keep locks honest.
Problem: Users who closed a tab or lost their connection left "ghost" locks behind, blocking everyone else from editing.
#Socket.IO#Redis#WebSockets#NestJS#Real-time#PresenceRead the story → - FrontendOct 18, 20268 min read
A React Query structure where one invalidation refreshes everything that changed
Small admin apps become messy fast: URLs typed everywhere, axios calls inside components, and stale screens after a save. Here is the structure I used for the auth admin panel: single sources of truth for endpoints, routes, query keys and config; hierarchical query keys so one invalidation refreshes a whole subtree; and one mutation wrapper for every write.
Problem: In a data-heavy admin panel, scattered URLs, axios calls inside components and hand-picked cache invalidation led to stale screens and inconsistent error handling.
#React#TanStack Query#Next.js#TypeScript#Architecture#AxiosRead the story → - BackendOct 18, 20269 min read
Two users, one configurator: building edit locks with Redis and WebSockets
Two people opened the same configurator, both edited it, and the last save silently erased the other's work. Here is how I designed a lock system with Redis and Socket.IO: lock levels, who blocks whom, how the HTTP API enforces it, and how the UI tells you exactly who is editing.
Problem: Two users could edit the same configurator at the same time, and the last save silently overwrote the other user's changes.
#Redis#WebSockets#Socket.IO#NestJS#Concurrency#Distributed LocksRead the story → - FrontendOct 17, 20268 min read
401 vs. 403: what the frontend should do with each (and why mixing them up locks users in a loop)
A 401 and a 403 both mean "no", but they ask the frontend for opposite reactions. Treating them the same sends users to a login screen that can't help them. Here is how the auth admin panel handles each one, why route protection is client-side when cookies live on another origin, and the four states every protected app should render.
Problem: Treating every auth error as "logged out" would send users with a valid session but no access to a login screen that could never let them in.
#Authentication#HTTP Status Codes#React#Next.js#Axios#SecurityRead the story → - BackendOct 17, 20267 min read
Prisma ignored my connection limit: connection pooling with driver adapters
I added connection_limit to my database URL and thought the pool was under control. With Prisma's PostgreSQL driver adapter, that parameter is silently ignored. Here is how connection pooling really works with driver adapters, why it matters on a shared database, and how to size the pool.
Problem: The pool size set in the database URL (connection_limit) had no effect, so the service could open more connections than intended on a shared PostgreSQL server.
#Prisma#PostgreSQL#Connection Pooling#node-postgres#NestJSRead the story → - PerformanceOct 16, 202611 min read
"Too many clients": how one export fired 3,200 database queries at the same second
A configurator export started crashing in production with "sorry, too many clients already". The cause was a friendly-looking loop: load the tree item by item, all in parallel. Here is how I found it, measured it, and rewrote it to use a fixed number of queries, with byte-for-byte identical output.
Problem: Exporting a large configurator crashed production with "too many clients already": the code opened thousands of queries at once.
#Node.js#NestJS#Prisma#PostgreSQL#N+1 Queries#Connection PoolingRead the story → - FrontendOct 16, 20268 min read
Draft + Save, not save-on-click: editing permissions without flooding the backend
Auto-saving every checkbox feels modern. For a permission editor where each write refreshes every member's session, it's wrong in three ways. Here is the small React hook that turns clicks into a draft and a diff, how it resets safely when server data changes, and the guard that stops admins from losing unsaved work.
Problem: Saving each permission checkbox immediately meant many API calls, a session refresh for every group member on every click, and no way to review a batch of changes.
#React#Hooks#TanStack Query#UX#Next.js#State ManagementRead the story → - FrontendOct 15, 20268 min read
Designing an admin panel for dangerous actions: reveal-once secrets, honest confirmations and the last admin
An auth admin panel is full of buttons that can lock people out of every app in the company. Here is how I designed its dangerous moments: a secret that can only be shown once, confirmations that state real consequences with real numbers, type-to-confirm for the irreversible, and actions that are disabled before the backend has to refuse them.
Problem: Admin actions in the auth panel could lose an unrecoverable API key, remove access for dozens of people, or lock out the last administrator, behind generic "Are you sure?" dialogs.
#UX#React#Next.js#Admin Panel#shadcn/ui#AccessibilityRead the story → - SecurityOct 14, 20267 min read
Logging in through an ERP with a one-time nonce: authentication vs. authorization
Our users already have accounts in the company ERP, so the auth service doesn't manage passwords at all. Here is how a login is handed off to the ERP and back, why a short-lived, single-use nonce protects that round trip, why the ERP saying yes is not enough to get in, and how a pre-registered user list keeps control in the company's hands.
Problem: Users already authenticate in the company ERP; the auth service needed to reuse that identity safely without handling passwords or trusting every ERP account.
#Authentication#SSO#OAuth#Nonce#Web Security#OdooRead the story → - BackendOct 13, 20268 min read
Turning off an app without breaking anything: disable, restore and hard delete in an auth service
Removing an application from a central auth service sounds like one delete. In reality, it touches groups, permissions, live sessions, Root Admins and foreign keys that cascade in an order PostgreSQL doesn't promise. Here is the two-step lifecycle we built, the cascade bug we had to fix, and why bumping versions alone doesn't end a session.
Problem: Removing an app either couldn't be undone, failed on foreign keys, or left users with working tokens for an app that was supposed to be gone.
#PostgreSQL#Prisma#Data Lifecycle#Soft Delete#Authorization#Foreign KeysRead the story → - SecurityOct 12, 20269 min read
Server-to-server tokens with exact permissions: designing service tokens
When one backend calls another, there's no user to log in. Shared secrets give too much access and are impossible to rotate cleanly. Here is how I designed service tokens: one app, an explicit permission list, instant revocation through Redis, reissue that kills the old token at the same moment, and an encrypted copy admins can retrieve later.
Problem: Services calling each other had no proper identity: shared secrets gave all-or-nothing access and couldn't be revoked or scoped.
#Authentication#Microservices#JWT#Redis#API Security#Machine-to-MachineRead the story → - BackendOct 11, 202610 min read
Making an admin change apply everywhere, immediately: the writer side of Redis versions
When an admin changes a group's permissions, every affected user in every affected app should feel it on their next request, without logging out. Here is how the auth service decides exactly whose versions to bump, why versions are keyed per user and per app, why Root Admins need special handling, and why the auth service is the only writer.
Problem: Permission changes only took effect when tokens expired, up to 8 hours later, and bumping the wrong scope either missed users or logged people out of unrelated apps.
#Redis#Authorization#Cache Invalidation#JWT#System Design#NestJSRead the story → - BackendOct 9, 20268 min read
Permissions the apps declare themselves: a self-syncing permission catalog
If admins type permission names by hand, they drift from the code: typos, forgotten endpoints, permissions for routes that no longer exist. Here is how each connected app publishes a manifest of its own permissions, how the auth service pulls it with an encrypted API key, and how removed permissions are pruned without leaving anyone with stale access.
Problem: Permission names lived in two places, the code and the admin panel, and kept drifting apart.
#Authorization#Microservices#API Design#Encryption#NestJS#SecurityRead the story → - BackendOct 7, 20268 min read
Groups by department: giving a whole team access in one click
Assigning permissions user by user doesn't scale: every new hire, every move, every new app means dozens of clicks and inevitable mistakes. Here is how we modeled access around departments: global groups, a separate activation step per app that acts as the real access boundary, and permission ticks that can never leak into an app the group wasn't opened for.
Problem: Giving access user by user across several apps was slow, error-prone, and impossible to review.
#Authorization#RBAC#Access Control#Data Modeling#Prisma#System DesignRead the story → - BackendOct 5, 20268 min read
One user, one group: why we removed multiple group membership
Letting a user belong to several groups sounds flexible. In practice, it made permissions ambiguous: which group wins, should rights be merged, and what happens when one group allows and another forbids? Here is why we moved to exactly one group per user, how a single column made the rule impossible to break, and the trade-off we accepted.
Problem: Users in several groups had ambiguous, hard-to-explain permissions, and every token had to merge or choose between groups.
#Authorization#RBAC#Data Modeling#PostgreSQL#Prisma#Access ControlRead the story → - DebuggingOct 3, 20268 min read
The cookie that logged you out of the other app: cookie name collisions in a multi-app SSO
Logging into one app silently broke another. Then our SSO session started fighting with the ERP's own session. Both bugs had the same root cause: cookies with the same name on the same host. Here is how cookies are really scoped, why names collide, and the naming scheme that fixed it.
Problem: Apps on the same domain overwrote each other's auth cookies, and the SSO session cookie collided with the ERP's own session cookie.
#Cookies#SSO#Authentication#Web Security#HTTP#DebuggingRead the story → - ArchitectureOct 1, 20269 min read
One login, many apps: designing a central auth service with SSO and app-scoped tokens
Every new internal app came with its own login, its own users and its own idea of permissions. Here is how I designed one auth service for all of them: a global session for single sign-on, a separate token per app, a strict rule that only the auth service can mint tokens, and request checks that never call the auth service at all.
Problem: Each internal app handled login, users and permissions on its own, so users logged in many times and admins managed access in many places.
#Authentication#SSO#JWT#Microservices#NestJS#Redis#System DesignRead the story →
