How I Used Claude Code to Plan a Complex Security Migration using AskUserQuestionTool

I recently migrated my SaaS application PhotoQuest from Supabase Auth to Spring Security 7.
The result? ~700 lines of custom auth code eliminated.
But the real story isn't the code. It's how two conversations with Claude Code shaped the entire approach.
Planning complex migrations is one of those things developers usually do in their heads. We sketch something on a whiteboard, maybe write a few notes, then dive into code, and figure out the edge cases as we go. Claude Code can interview you about your architecture decisions and produce a comprehensive spec before you write a single line of code.
The First Conversation: Wrong Direction, Right Process
I started with a different goal. Every time I needed to get the current wedding/event, I was doing a 3-table join:
// EventService.kt
val event = sql.select()
.from(EVENT)
.join(EVENT_MEMBER).on(EVENT_MEMBER.EVENT_ID.eq(EVENT.ID))
.join(WEBSITE_USER).on(WEBSITE_USER.SUPABASE_ID.eq(EVENT_MEMBER.SUPABASE_USER_ID))
.where(WEBSITE_USER.SUPABASE_ID.eq(currentUserId))
.fetchOne()
My idea: put event_id directly in the Supabase JWT. The query becomes a simple lookup:
// EventService.kt (refactored)
val event = sql.selectFrom(EVENT)
.where(EVENT.ID.eq(eventIdFromJwt))
.fetchOne()
Much cleaner, right?
Let's first ask Claude to interview me about this refactor:
Currently I implemented multiple users for one event via the event_member table.
I was thinking of refactoring this and putting the event_id in the JWT of the SupabaseUser.
Interview me in detail using the AskUserQuestionTool about literally anything:
technical implementation, UI & UX, concerns, tradeoffs, etc.
The key here is asking Claude to use the AskUserQuestionTool. This triggers an interactive interview mode
where Claude asks you questions one by one, rather than making assumptions.
Claude produced a comprehensive spec and wrote it to docs/TODO/event-member-refactor-spec.md.
Summary of key decisions from interview:
| Area | Decision |
| JWT claims | active_event_id + active_event_role (CREATOR/MANAGER only) |
| Multi-event | Single active event, switcher in settings page |
| event_member | Remove entirely after migration |
| Owner tracking | New event.owner_user_id column replaces CREATOR role |
| Relation | Move to website_user, nullable, single-relation accepted |
| onboarding_step | Remove |
| JWT update | Direct SQL to auth.users.raw_app_meta_data |
| Event creation | PENDING → ACTIVE status flow, email admin on JWT failure |
| Invitation accept | Force JWT refresh via redirect to /api/user/jwt |
| Starter library | Breaking change, expose appMetadata as raw Map |
| Migration | 6 phases: prep → starter → dual-write → dual-read → data → cleanup |
Then I asked one question that changed everything:
Me: What if we moved away from Supabase Auth and just used Spring Security?
What followed was an interview that systematically explored every dimension of this alternative. Claude didn't just say yes or no. It asked questions that forced me to think through the implications.
The Systematic Interview
Claude started with the fundamentals:
Claude: What's driving the consideration to move away from Supabase Auth? Is it the complexity of JWT claim management we just discussed, or other factors?
My answer: "Want more control over auth flow."
Claude: Supabase Auth currently handles: anonymous sign-in, email magic links, OAuth (Google/Apple/Azure), email confirmation flow. Which of these do you actually need long-term?
My answer: "All of them I need feature parity."
This established the scope. Then Claude drilled into the technical tradeoffs:
Session vs JWT
Claude: For anonymous sign-in with Spring Security: this typically requires session-based auth (cookie), not JWT. Are you okay with session-based auth, or do you want stateless JWT throughout?
My answer: "Haven't thought about it, need guidance."
Claude: Session vs JWT tradeoff: Sessions are simpler (no refresh tokens, instant revocation, easy to store event_id), but require sticky sessions or shared session store for horizontal scaling. What's your current/planned deployment?
My answer: "Multiple instances, no shared store yet."
Claude: For multiple instances without a shared store: you'll need either Redis for sessions, or go stateless JWT. Redis adds infrastructure but simplifies auth. JWT adds token refresh complexity. Preference?
My answer: "Go JWT, accept refresh complexity."
This chain of questions narrowed down the architecture. Each answer constrained the next decision.
In detail:
Session-based auth→ simpler token management, but requires shared session store for scalingJWT→ stateless and scales easily, but adds refresh token complexity- I chose JWT because I didn't want to add Redis just for sessions
Anonymous User Flow
The anonymous→verified user flow was critical. Claude mapped it out:
Claude: Anonymous users are currently Supabase anonymous auth. With Spring Security, options: (A) guest session with upgrade path, (B) lazy account creation on first action, (C) require sign-up upfront. Which fits your UX?
My answer: "Keep current flow: anon -> email confirmation."
Claude: For anonymous->confirmed user flow with JWT: the anon user gets a JWT, then confirms email, and the JWT needs to be replaced with one containing their verified identity. How should this transition work?
My answer: "The Anon JWT needs to be long lived as I want the user to convert at a later date. What do you think?"
Claude: Long-lived anon JWT that converts: the cleanest approach is to keep the same user ID (UUID) throughout, just upgrade the 'verified' flag and add email claim on confirmation. The JWT gets replaced but user identity persists. Sound right?
My answer: "Yes, same user ID, upgrade claims."
Infrastructure Decisions
Claude systematically uncovered my infrastructure constraints:
Claude: Email magic links without Supabase means you need: email sending, token generation/validation, rate limiting. Do you have email infrastructure already (SES, Resend, etc.)?
My answer: "Yes, already using SES/similar."
Claude: Rate limiting and brute-force protection: Supabase handles this for you. With Spring Security, you'll need to implement it. Do you have rate limiting infrastructure (Redis, bucket4j, etc.)?
My answer: "No rate limiting infra yet."
Claude: For rate limiting without Redis: bucket4j can use JCache with local cache (Caffeine) for single-instance, , or PostgreSQL for distributed. Given your multi-instance setup, prefer DB-backed rate limiting or add Redis?
My answer: "DB-backed rate limiting (simpler infra)."
Each question built on the previous answer. The "no Redis" constraint shaped everything that followed.
The Tradeoff Explanations
When I needed guidance, Claude explained the options in detail:
Magic Link Token Storage
Claude: Let me explain the magic link token tradeoffs:
Option A: Dedicated table - Easy revocation, can track usage, simple expiry queries. Cons: DB write per request.
Option B: Stateless signed token - No DB storage, scales infinitely. Cons: Cannot revoke before expiry.
Option C: Hybrid - Fast validation (check signature first), DB only for revocation.
Recommendation: Given you want anon → confirmed conversion and might need to invalidate tokens, I'd lean toward Option A - the DB overhead is minimal for magic links (low volume).
CSRF Protection
Claude:
Sec-Fetch-Siteis a modern approach to CSRF protection. The header is browser-set (cannot be forged):
same-origin: Request from your domaincross-site: Request from different domainFor CSRF protection, you'd reject state-changing requests where
Sec-Fetch-Site: cross-site.Pros: Simpler than CSRF tokens, no JS configuration needed for HTMX. Cons: Older browsers don't send it (Safari <16.4).
Recommendation: This works well for PhotoQuest since you target modern browsers (wedding guests with smartphones).
The Pivot Moment
After 14 tool calls and 6 messages, Claude had mapped out the entire architecture. I asked the final question:
Claude: Should this Spring Security migration be a separate spec, or should I update the existing event-member-refactor-spec to replace the Supabase JWT approach entirely?
My answer: "Combine into one comprehensive auth refactor spec."
Claude: Should I keep the phased approach where Supabase JWT refactor is Phase 1 (quick win), and Spring Security migration is Phase 2 (full control)? Or skip Supabase refactor entirely and go straight to Spring Security?
My answer: "Skip Supabase refactor, go straight to Spring."
Claude rewrote the entire spec. The original Supabase JWT approach was replaced with a comprehensive Spring Security migration. But I realized we'd only scratched the surface.
The Second Conversation: Deep-Dive Interview
The next day, I started fresh with the Spring Security spec. This time I wanted a thorough interview:
Read this @docs/TODO/supabase-auth-refactor-spec.md and interview me in detail
about literally anything: technical implementation, UI & UX, concerns, tradeoffs,
etc. but make sure the questions are not obvious - be very in-depth and continue
interviewing me continually until it's complete
Lookup Spring Security best practices: query-docs
/websites/spring_io_spring-security_reference_7_0 {query}
💡
I'm using the @ symbol to reference a file directly in the prompt. Claude Code will read the file and include it in context. The query-docs part tells Claude to look up Spring Security documentation as needed.
What followed: 15+ rounds of probing questions. Claude also looked up Spring Security 7 documentation in real-time. This meant the interview was grounded in current best practices, not outdated patterns from Stack Overflow circa 2019.
Here are some questions that forced me to think:
Session & Cookie Edge Cases
Claude: For Guest users with device cookies: What's the recovery strategy when a guest clears cookies mid-upload or switches devices?
My answer: Accept data loss. Guests must stay on one device. Simple beats complex.
Claude: Session timeout is 30 minutes. For wedding guests who might take photos over 3–4 hours, what's the acceptable re-auth UX?
My answer: Use persistent remember-me tokens. This led to adding persistent_logins table.
OAuth Provider Quirks
Claude: Apple OAuth may not return email on subsequent logins. How do we handle account linking when email is missing?
My answer: Require email on first Apple login only. Store in oauth_account.provider_email.
Security vs. UX Tradeoffs
Claude: If oneTimeTokenLogin validation fails (expired, invalid, already used), should we show specific error message or generic 'Invalid link'?
Security says generic. UX says specific. I chose specific: "Link expired", "Link already used". The attack surface is minimal.
Catching Claude's Confusion
The best moment: I caught Claude mixing concepts.
Claude: Anonymous user creation: When should anon users be created? On first visit, or only when they perform action?
My answer: "You are mixing guest and anon users. Anon users are brides trying the demo. Guests don't have an account."
This clarification became a table in the final spec:
| User Type | Purpose | Auth Mechanism | Has Session? |
| Guest | Wedding guest uploading | Device cookie | No |
| Anonymous | Bride trying demo | Session (is_anonymous=true) | Yes |
| Verified | Paid customer | Session (email verified) | Yes |
Asking Claude for Recommendations
Claude: Integration tests should cover magic link + OAuth flows. For OAuth, should we mock provider responses or use Spring Security's OAuth2 test support?
My answer: "What would you recommend?"
Claude suggested Spring Security's built-in test support plus GreenMail for E2E magic link tests. Both made it into the spec.
The Output
Two conversations. 900-line specification. And we are done!
Here's what the final spec covered:
- Database migrations (6 tables:
website_user,oauth_account,magic_link_token,device_cookie,persistent_logins,rate_limit_bucket) - Complete
SecurityConfig.ktwith remember-me, CSRF, session fixation protection - Unified
PhotoQuestUserimplementing bothUserDetailsandOAuth2User - Account deletion service with ownership transfer checks
- Anonymous user cleanup job (7-day grace period)
- Data migration strategy with email normalization
Why This Works
Architecture exploration. The first conversation explored Supabase JWT claims in depth. This made the pivot question ("what about Spring Security?") more informed. Sometimes you need to plan the wrong thing to discover the right thing.
Rubber duck that asks questions. Claude forced me to articulate decisions I would have made implicitly – and probably inconsistently. Unlike a real rubber duck, this one pushes back.
Documentation as a side effect. The interview process produced a spec that's actually useful for implementation. No more "I'll document it later" (We all know how that ends).
Edge case discovery. Questions like "what if Apple OAuth doesn't return email?" aren't obvious when you're thinking about the happy path. Claude surfaces these before they become production bugs.
Try It Yourself
For your next complex feature or migration:
- Write a rough spec or design doc
- Ask Claude to interview you using
AskUserQuestionTool - Be open to pivots – if Claude's questions reveal a better approach, explore it
- Answer honestly – including "I don't know" or "what would you recommend?"
- Let Claude write the updated spec
The interview format forces you to make decisions explicitly – rather than discovering them during implementation.
And sometimes the right question changes the entire direction.
If you want to learn more about building with Spring Security, HTMX, and server-side rendering, check out my HTMX + Spring Boot series. My side business PhotoQuest is also built entirely with this stack – no JavaScript framework headaches, just clean server-rendered HTML with htmx for interactivity.
If you have questions about using Claude Code for architecture planning, ping me on twitter.com/tschuehly!



