JVM SDK (Spring Boot)
Integrate Perly churn prevention into your Spring Boot application with a starter dependency, auto-configuration, and a bean-based user resolver.
ai.perly:perly-spring-boot-starterv1.0.05 minutes
Installation
Maven
<dependency>
<groupId>ai.perly</groupId>
<artifactId>perly-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>Gradle (Kotlin DSL)
implementation("ai.perly:perly-spring-boot-starter:1.0.0")Usage
The Spring Boot starter provides auto-configuration. Set the API key in your application.yml and the middleware filter registers automatically.
# application.yml
perly:
api-key: ${PERLY_API_KEY}The starter registers a servlet filter that intercepts all requests and resolves the current user via the PerlyUserResolver bean.
User Resolver
Define a PerlyUserResolver bean that maps the authenticated user from the servlet request to a Perly user profile.
Kotlin
// config/PerlyConfig.kt
import ai.perly.core.PerlyBuilder
import ai.perly.core.PerlyUserResolver
import jakarta.servlet.http.HttpServletRequest
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class PerlyConfig(
private val userService: UserService
) {
@Bean
fun perlyUserResolver(): PerlyUserResolver {
return PerlyUserResolver { request: HttpServletRequest ->
val principal = request.userPrincipal ?: return@PerlyUserResolver null
val user = userService.findByUsername(principal.name)
PerlyBuilder()
.setId(user.id)
.setMetadata(mapOf(
"plan" to user.plan,
"region" to user.region,
"companyId" to user.companyId
))
.linkStripeById(user.stripeCustomerId)
.linkHubspotById(user.hubspotContactId)
.build()
}
}
}Java
// config/PerlyConfig.java
import ai.perly.core.PerlyBuilder;
import ai.perly.core.PerlyUserResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Map;
@Configuration
public class PerlyConfig {
@Bean
public PerlyUserResolver perlyUserResolver(UserService userService) {
return request -> {
var principal = request.getUserPrincipal();
if (principal == null) return null;
var user = userService.findByUsername(principal.getName());
return new PerlyBuilder()
.setId(user.getId())
.setMetadata(Map.of("plan", user.getPlan(), "region", user.getRegion()))
.linkStripeById(user.getStripeCustomerId())
.linkHubspotById(user.getHubspotContactId())
.build();
};
}
}Tracking Events
Inject PerlyClient into your services or controllers to track customer events.
import ai.perly.core.PerlyClient
import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/api/onboarding")
class OnboardingController(
private val perlyClient: PerlyClient
) {
@PostMapping("/complete")
fun complete(@AuthenticationPrincipal user: UserDetails): Map<String, Boolean> {
perlyClient.track(user.id, "onboarding_completed")
return mapOf("success" to true)
}
@PostMapping("/export-report")
fun exportReport(
@AuthenticationPrincipal user: UserDetails,
@RequestBody request: ExportRequest
): Map<String, String> {
perlyClient.track(user.id, "report_exported", mapOf("format" to request.format))
return mapOf("url" to reportUrl)
}
}Expansion Signals
Send signals from services or scheduled jobs when customers approach plan limits.
import ai.perly.core.PerlyClient
import org.springframework.stereotype.Service
@Service
class UsageCheckService(
private val perlyClient: PerlyClient
) {
fun checkSeatLimit(userId: String, current: Int, limit: Int) {
if (current > limit * 0.9) {
perlyClient.signal(userId, "seat_limit_near", mapOf(
"current" to current,
"limit" to limit
))
}
}
}
// Other signal types
perlyClient.signal(userId, "api_usage_high", mapOf("current" to 9500, "limit" to 10000))
perlyClient.signal(userId, "rate_limit_hit", mapOf("endpoint" to "/api/search"))
perlyClient.signal(userId, "token_usage_high", mapOf("current" to 950000, "limit" to 1000000))
perlyClient.signal(userId, "billing_retry_failed", mapOf("attempt" to 3))