Last Week

The login procedure is complete! The implementation spans all architectural layers – presentation, domain, and data – with OTP authentication fully operational. Setting up the email infrastructure with Resend for SMTP services ensures the app can scale beyond Supabase’s built-in email limits.

Login Implementation Across All Layers

The week-long implementation might seem excessive for a login flow, but strict adherence to clean architecture and test-driven development justified the time investment. The first attempt at this app taught a harsh lesson: shortcuts in architecture accumulate technical debt that makes debugging nearly impossible.

Each layer now has clearly defined responsibilities:

  • Presentation Layer: User interface and interaction handling
  • Domain Layer: Login/logout use cases with business logic
  • Data Layer: Repository pattern interfacing with Supabase backend

The Supabase backend configuration includes OTP authentication with Resend SMTP integration for production-ready email delivery. While Supabase provides limited free OTP emails, Resend handles higher volumes reliably.

What does it mean in English?

Building a login system properly means creating separate, independent parts that each do one job well. When users enter their email, they receive a one-time code to verify their identity. The app is built so each piece can be tested and fixed independently, preventing the domino effect where fixing one bug creates three more.

Nerdy Details

OTP Authentication Flow with Supabase and Resend

// Domain Layer - Use Case
class RequestOtpUseCase(
    private val authRepository: AuthRepository
) {
    suspend operator fun invoke(email: String): Result<Unit> {
        return authRepository.sendOtp(email)
    }
}

class VerifyOtpUseCase(
    private val authRepository: AuthRepository
) {
    suspend operator fun invoke(
        email: String, 
        token: String
    ): Result<User> {
        return authRepository.verifyOtp(email, token)
    }
}

// Data Layer - Repository Implementation
class SupabaseAuthRepository(
    private val client: SupabaseClient
) : AuthRepository {
    override suspend fun sendOtp(email: String): Result<Unit> {
        return try {
            client.auth.signInWith(OTP) {
                this.email = email
            }
            Result.success(Unit)
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
    
    override suspend fun verifyOtp(
        email: String, 
        token: String
    ): Result<User> {
        return try {
            val result = client.auth.verifyEmailOtp(
                type = OtpType.Email.MAGIC_LINK,
                email = email,
                token = token
            )
            Result.success(result.session?.user?.toDomainUser() 
                ?: throw Exception("Invalid session"))
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

Supabase Email Configuration with Resend

// supabase/config.toml
[auth.email]
enable_signup = true
double_confirm_changes = true
enable_confirmations = true

[auth.smtp]
host = "smtp.resend.com"
port = 587
user = "resend"
pass = "env(RESEND_API_KEY)"
sender_name = "Shokken"
sender_email = "[email protected]"

Test Coverage Strategy

Domain and data layers have comprehensive test coverage, but two significant gaps remain:

  1. Integration Tests: Supabase’s inline functions resist mocking, requiring actual backend instances for testing
  2. UI Tests: Compose Multiplatform’s iOS support recently stabilized, but testing tooling remains immature
// Domain Layer Tests (Working)
class LoginUseCaseTest {
    @Test
    fun `successful OTP verification returns user`() = runTest {
        val authRepository = mockk<AuthRepository>()
        coEvery { 
            authRepository.verifyOtp(any(), any()) 
        } returns Result.success(testUser)
        
        val useCase = VerifyOtpUseCase(authRepository)
        val result = useCase("[email protected]", "123456")
        
        assertTrue(result.isSuccess)
    }
}

// Integration Tests (Planned)
class AuthIntegrationTest {
    @Test
    fun `complete OTP flow with real backend`() = runIntegrationTest {
        // Requires Docker container with Supabase
        val client = createTestSupabaseClient()
        val repository = SupabaseAuthRepository(client)
        
        // Send OTP
        repository.sendOtp("[email protected]")
        
        // Retrieve token from test email service
        val token = getTestEmailToken("[email protected]")
        
        // Verify OTP
        val result = repository.verifyOtp("[email protected]", token)
        assertTrue(result.isSuccess)
    }
}

The decision to postpone UI tests is calculated risk. UI bugs are less catastrophic than business logic or data layer failures. The plan is to add comprehensive UI testing after the MVP launch when the testing ecosystem matures.

State Management for Login Flow

class LoginViewModel(
    private val requestOtp: RequestOtpUseCase,
    private val verifyOtp: VerifyOtpUseCase
) : ViewModel(), ContainerHost<LoginState, LoginSideEffect> {
    
    override val container = container<LoginState, LoginSideEffect>(
        LoginState()
    )
    
    fun onRequestOtp(email: String) = intent {
        reduce { state.copy(isLoading = true) }
        
        requestOtp(email).fold(
            onSuccess = {
                reduce { 
                    state.copy(
                        isLoading = false,
                        step = LoginStep.VERIFY_OTP
                    )
                }
            },
            onFailure = { error ->
                reduce { 
                    state.copy(
                        isLoading = false,
                        error = error.message
                    )
                }
            }
        )
    }
    
    fun onVerifyOtp(token: String) = intent {
        reduce { state.copy(isLoading = true) }
        
        verifyOtp(state.email, token).fold(
            onSuccess = { user ->
                reduce { state.copy(isLoading = false) }
                postSideEffect(LoginSideEffect.NavigateToDashboard(user))
            },
            onFailure = { error ->
                reduce { 
                    state.copy(
                        isLoading = false,
                        error = "Invalid code. Please try again."
                    )
                }
            }
        )
    }
}

Next Week

The user profile page is next – a straightforward implementation since it leverages the existing Supabase auth repository. After that comes the significant challenge: designing and implementing the Postgres database schema with row-level security, DTOs, and object mapping. That infrastructure work will likely span 2-3 weeks.

Every extra hour spent on proper architecture now saves days of debugging later.