Skip to main content

Command Palette

Search for a command to run...

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

Updated
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:

AreaDecision
JWT claimsactive_event_id + active_event_role (CREATOR/MANAGER only)
Multi-eventSingle active event, switcher in settings page
event_memberRemove entirely after migration
Owner trackingNew event.owner_user_id column replaces CREATOR role
RelationMove to website_user, nullable, single-relation accepted
onboarding_stepRemove
JWT updateDirect SQL to auth.users.raw_app_meta_data
Event creationPENDING → ACTIVE status flow, email admin on JWT failure
Invitation acceptForce JWT refresh via redirect to /api/user/jwt
Starter libraryBreaking change, expose appMetadata as raw Map
Migration6 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 scaling
  • JWT → 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:

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-Site is a modern approach to CSRF protection. The header is browser-set (cannot be forged):

  • same-origin: Request from your domain
  • cross-site: Request from different domain

For 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:

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 is why the interview format works. Claude's confusion surfaced a domain concept that was only in my head. By catching it early, we avoided building the wrong abstractions.

This clarification became a table in the final spec:

User TypePurposeAuth MechanismHas Session?
GuestWedding guest uploadingDevice cookieNo
AnonymousBride trying demoSession (is_anonymous=true)Yes
VerifiedPaid customerSession (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.kt with remember-me, CSRF, session fixation protection
  • Unified PhotoQuestUser implementing both UserDetails and OAuth2User
  • 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:

  1. Write a rough spec or design doc
  2. Ask Claude to interview you using AskUserQuestionTool
  3. Be open to pivots – if Claude's questions reveal a better approach, explore it
  4. Answer honestly – including "I don't know" or "what would you recommend?"
  5. 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!

More from this blog

Thomas Schilling | Spring/HTMX/Claude Code

22 posts

Youngest Speaker @Spring I/O & Spring ViewComponent creator.

Passionate about building awesome software with Spring + HTMX. Pushing full-stack development with Spring forward.