How a simple authentication refactor taught me that AI assistants are great at code, but need human guidance for architectural decisions
This article is based on my experience refactoring the authentication system in the heyradcode/do-not-stop project.
I had a shared authentication package (@do-not-stop/shared-auth) that was being used by both my frontend (React web) and mobile (React Native) apps. Interestingly, this package was originally created by my AI assistant during a "vibe coding" session - I was just going with the flow and letting it build the structure. The code had some duplication - both projects were manually wiring up the same hooks and API clients. Simple task: consolidate the duplicated code.
When I asked my AI assistant to consolidate, it immediately jumped to a factory pattern:
// AI's first suggestion export const createEthereumAuth = ({ apiUrl, storageAdapter }) => { const apiClient = createAuthApiClient(apiUrl); const useNonce = createUseNonce(apiClient); const useVerifySignature = createUseVerifySignature(apiClient, onTokenSuccess); const AuthProvider = createAuthProvider({ useAccountHook: useAccount, useSignMessageHook: useSignMessage, useNonce, useVerifySignature, storageAdapter, }); return { AuthProvider, useNonce, useVerifySignature }; };
At first glance, this seems reasonable. It removes duplication, right? But it's still passing everything around. The AI assistant kept adding layers:
When I looked at the code, I kept asking simpler questions:
"Why do we need createAuthProvider? Can't we just use AuthProvider directly?"
"Why pass apiClient when we can set it globally and reuse it?"
"Why re-export hooks through bridge files when we can import directly?"
Each question stripped away another unnecessary layer.
Instead of factories and parameters, we used global configuration:
// config.ts - configure once setApiBaseUrl(API_URL); setTokenSuccessCallback(callback); setStorageAdapter(adapter); // AuthContext.tsx - just use directly import { useAccount, useSignMessage } from 'wagmi'; import { useNonce, useVerifySignature } from '../hooks'; export const AuthProvider = ({ children }) => { const { address } = useAccount(); // No parameters! const { signMessage } = useSignMessage(); // ... } // App.tsx - simple import import { AuthProvider } from '@do-not-stop/shared-auth';
The difference:
The Final Architecture I ended up with:
packages/shared-auth/ ├── api.ts # Singleton API client ├── hooks/ │ ├── useNonce.ts # Direct hook (uses shared client) │ └── useVerifySignature.ts └── contexts/ └── AuthContext.tsx # Direct component (uses hooks directly) frontend/src/config.ts # setApiBaseUrl(), setStorageAdapter() mobile/src/config.ts # Same, different adapter App.tsx # import { AuthProvider } from 'shared-auth'
Zero factories. Zero parameters. Zero bridge files.
The breakthrough questions I kept asking were all about simplification:
AI is excellent at:
AI struggles with:
The solution? Use AI for implementation, but keep a human in the loop for architectural decisions. When AI suggests adding complexity, ask: "Can I do this more simply?"
The best code is often the code you don't write. AI doesn't always know that.
This article is based on my real refactoring session with an AI coding assistant while working on the heyradcode/do-not-stop project. The authentication system works great now, and the codebase is simpler than when I started.


