When I built a custom authentication UI with an LLM, the code snippets turned out to be the least useful thing it gave me. What mattered was having a structured partner for the whole development lifecycle. That way of working changed both what I built and how I tackle hard development problems.
The moment my development workflow changed
Last month I had a familiar but awkward job to do: replace a standard redirect-based authentication flow with a custom in-app UI that talks to our OIDC provider for the “Spell Coach” application. I wanted users to sign in inside the app rather than bouncing out to the provider’s hosted login page. Normally that means days of research, planning and careful security work. This time it became a chance to rethink how I develop.
Instead of opening an editor, I treated Claude as a planning partner and started with a broad question: “How can I improve the authentication experience without compromising security?” What came back changed how I think about AI-assisted development.
The model did more than spit out snippets or generic advice. It read our project structure, looked into our provider’s capabilities through targeted web searches, and proposed a documentation-first approach. That was the shift for me. The tool acted as a thinking partner that helped me get clear before writing any code, not as a code vending machine.
Building a foundation: the documentation-first approach
The thing that clicked was that documentation should come before code. Not as a tidy deliverable, but as the actual conversation I was having with the AI. So I asked Claude to write a detailed authentication specification, AUTH.md, that we would both treat as the shared understanding.
The document covered the user flows for registration, login and password reset; the frontend component specifications with clear MVC boundaries; the API endpoints with their request and response formats; the security considerations, including how we would implement PKCE; and the integration patterns for our provider’s APIs.
That document became our source of truth, a contract between me and the AI assistant that shaped everything that followed.
When we defined the user registration flow, for example, the specification named security concerns that often go unnoticed until you are halfway through coding:
## User Registration Flow
1. User enters registration details in custom UI form
2. Frontend validates format (email, password complexity) client-side
3. Frontend calls `POST /auth/register` endpoint
4. Backend:
- Validates input
- Creates user via provider's Management API
- Initiates PKCE flow
- Returns authorization URL with state parameter
5. Frontend redirects to authorization URL
6. After authorization, provider redirects to callback URL with auth code
7. Backend exchanges code for tokens, validates state param for CSRF protectionThis level of detail changed how the AI could help me. Every conversation after that had clear context and boundaries, which cut down ambiguity and stopped the usual problem of plausible-looking but wrong code.
From specification to executable plan
Once we had a shared understanding, I needed to break the feature into manageable, ordered tasks. I asked Claude to produce a TODO.md based on the specification. The result turned a daunting project into a clear roadmap of phased, concrete steps.
The part that worked well was that each TODO item pointed back at the specification. For instance:
## Backend Development
1. [ ] Implement user registration endpoint (POST /auth/register)
- Reference: AUTH.md Section 3.1
- Validate input parameters according to AUTH.md Section 5.2
- Integrate with provider Management API as specified in AUTH.md Section 7.1
- Implement PKCE flow initialization as described in AUTH.md Section 6.3
- Return appropriate response format per AUTH.md Section 3.1.2Linking tasks to specifications this way created a useful feedback loop. While working on any one task, both the AI and I could check the exact requirements, so intention and implementation stayed close together.
It also made the AI a much better coding partner. Instead of asking for “help implementing authentication,” I could ask for help “implementing the user registration endpoint as specified in AUTH.md Section 3.1.” That context lifted both the quality and the security of what came back.
The meta-prompt: instructing the AI how to assist
At one point I realised I needed a consistent way to work through the task list. I asked Claude to create a meta-prompt template to guide each implementation task. It became our shared protocol:
Task Declaration: I'm working on [current task from TODO.md].
Context Review:
1. From AUTH.md: [relevant specification details]
2. From CLAUDE.md: [relevant project structure/standards]
Implementation Plan:
1. [Step 1]
2. [Step 2]
3. [Step 3]
For this specific step, I need to:
[Current specific implementation need]
When complete, I'll update TODO.md to reflect progress.This turned what could have been a string of disconnected coding requests into a coherent, progressive piece of work. By referencing the documentation and moving through tasks in order, we held context across several sessions and kept security at the front of every decision.
Architectural decisions that matter
A custom authentication UI rests on a few architectural decisions with real security weight, and the documentation-first approach let me think these through before committing to anything. The biggest one was how to handle the OIDC flow. I had two main options:
- Frontend-driven OIDC: The React frontend would handle the entire OIDC flow directly with our provider
- Backend orchestration: The Go backend would prepare OIDC parameters and guide the flow
After weighing the security implications, I went with backend orchestration. It let me validate and sanitise every parameter properly. It kept state on the server so I could guard against CSRF. It gave me a central place to log authentication attempts for monitoring. And it kept the frontend simpler while leaving the security controls where I could see them.
That choice shaped the endpoints:
POST /auth/login
- Request: { username, password }
- Response: { authorizationUrl, state }
POST /auth/callback
- Request: { code, state }
- Response: { tokens, userProfile }The backend handles every sensitive OIDC operation: code exchange, token validation and secure storage of refresh tokens, whilst the frontend deals only with the user experience.
Documenting this decision in AUTH.md paid off during implementation. Whenever the AI proposed an alternative approach, I could point back at the agreed architecture and keep things consistent and secure.
Implementing with precision: the AI as a pair programmer
With the specifications and tasks in place, implementation got sharp. For each task I followed the meta-prompt structure to steer the AI’s contributions.
Implementing the user registration endpoint, for example, I gave this context:
Task Declaration: I'm implementing the user registration endpoint (POST /auth/register) as outlined in TODO.md item 1 under Backend Development.
Context Review:
1. From AUTH.md Section 3.1: The endpoint should accept email, password, name and validate according to our security requirements.
2. From AUTH.md Section 7.1: We need to use the provider's Management API to create the user.
3. From AUTH.md Section 6.3: We need to initialize a PKCE flow with a code_verifier and code_challenge.
4. From CLAUDE.md: Our backend uses Go with standard net/http package and follows MVC patterns.
Implementation Plan:
1. Define request/response structs
2. Implement input validation
3. Create PKCE parameters
4. Call provider Management API
5. Generate and store state parameter
6. Return authorization URL with stateGiven that structure, the AI could propose precise, security-focused code that fitted our architecture and standards. Here is a simplified excerpt of the code it proposed for PKCE parameter generation:
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
)
func generateCodeVerifier() (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(b), nil
}
func generateCodeChallenge(verifier string) string {
h := sha256.New()
h.Write([]byte(verifier))
return base64.RawURLEncoding.EncodeToString(h.Sum(nil))
}This did the job and did it with proper security practise: cryptographically secure random values and the correct SHA-256 transformation that PKCE requires.
The most useful part was not the code. It was how the AI explained the security considerations and tradeoffs at each step, so I understood not just what to implement but why one approach was safer than another.
What made this approach different
What set this apart from my earlier projects was the structured, documentation-driven method. By pinning down the specifications and tasks before coding, I built a frame that got the most out of the AI while I kept control of architecture and security.
A few things made it work. AUTH.md acted as a contract, a clear specification both of us could reference, which reduced ambiguity and made sure the security requirements were actually met. TODO.md broke the project into manageable chunks, each pointing back at the specification. The meta-prompt template gave us a consistent protocol that held context across tasks. And because the security requirements lived explicitly in AUTH.md, security stayed a first-class concern the whole way through.
Together these changed the level at which I worked with the AI. Rather than “write code to do X,” I could have a higher-level conversation about how best to build a component within our constraints.
Where it needed careful handling
LLM hallucinations and API assumptions
The AI sometimes invented details about our provider’s API, assuming endpoints or parameters that did not exist. I dealt with this by always checking API details against the official documentation, correcting the AI explicitly whenever it got something wrong, and adding the verified details into AUTH.md so they were there next time.
Context window limitations
Juggling several documentation files (AUTH.md, TODO.md, code files) inside the AI’s context window got difficult. I worked around it by focusing on one task at a time, referencing only the relevant sections of the documentation, and using the meta-prompt to keep the structure consistent across interactions.
Security review requirements
The AI was good at implementing security best practise, but I still insisted on human review for every piece of security-sensitive code. That was partly about catching errors. It was also about making sure I genuinely understood the security implications of each decision.
Lessons for effective AI-assisted development
A few patterns came out of this that make AI coding assistants noticeably more effective.
1. Documentation before implementation
Writing detailed specifications before any code gives the AI the context it needs to help well. The point is not the documentation as an artefact. It is the shared understanding that guides the work.
2. Structured task management
Breaking a project into well-defined tasks that reference the specifications creates clarity and focus. This matters most with AI assistants, which tend to struggle with very open-ended requests.
3. Consistent interaction patterns
A consistent protocol for interactions, like our meta-prompt, keeps context alive and stops important information getting lost between sessions. That consistency counts double on security-sensitive features.
4. Human-in-the-loop architecture
The best results came from keeping human control over architecture and security decisions while leaning on the AI for implementation. That balance protects quality and security without throwing away the productivity gain.
5. Iterative refinement
Every implementation cycle is a chance to improve both the code and the documentation. Updating the specifications and tasks as you learn keeps the whole thing improving on itself.
Transforming development through AI partnership
Building this authentication system showed me that the strongest use of AI in software development is not generating code. It is reshaping the whole process. By writing clear specifications, breaking the work into tasks and keeping consistent interaction patterns, I turned a tricky, security-sensitive project into something structured and manageable.
The documentation-first approach improved the AI’s help, and it improved my own thinking about the problem. Forcing myself to spell out requirements, security considerations and architectural decisions before coding meant I caught problems earlier and ended up with a more coherent design.
This has changed how I approach hard development tasks. The AI works best inside a structured, documentation-driven method, and once I started treating it that way my code got better and the process itself became more systematic, secure and manageable.
As these tools keep improving, I think this structured, collaborative way of working will only matter more. The developers who build the context and structure around an AI assistant get far more out of it than those who just ask for snippets.