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))