Multi-platform plugin API framework for Minecraft servers — providing modular service discovery, lifecycle-managed components, NMS bridge abstractions, and rich DSLs across Paper, Velocity, and Standalone platforms.
surf-api is the shared foundation for SLNE's server plugin ecosystem. It provides:
- Platform-abstracted APIs for Paper (Bukkit/NMS), Velocity, and Standalone environments
- Service discovery via
ServiceLoader(requiredService<T>()) for singleton bridges - Component system with compile-time KSP discovery, topological lifecycle ordering, and conditional activation
- NMS bridge pattern to safely abstract version-specific internals behind stable interfaces
- Inventory framework DSLs, command helpers, scoreboard utilities, and extension-rich APIs
- Gradle plugin (
surf-api-gradle-plugin) that configures downstream projects, handles dependency shading, and runs the KSP processor
surf-api
├── surf-api-core # Platform-agnostic core API & implementation
│ ├── surf-api-core # Core API interfaces
│ └── surf-api-core-server # Core implementations
│
├── surf-api-paper # Paper/Bukkit platform
│ ├── surf-api-paper # Public API (extensions, bridges, DSLs)
│ ├── surf-api-paper-server # Implementations (shaded into final JAR)
│ ├── surf-api-paper-nms
│ │ ├── surf-api-paper-nms-common # Shared NMS bridge interfaces
│ │ ├── surf-api-paper-nms-v1-21-11 # NMS impl for 1.21.11
│ │ └── surf-api-paper-nms-v26-1 # NMS impl for 26.1
│ └── surf-api-paper-plugin-test # Local test server (excluded from CI)
│
├── surf-api-velocity # Velocity proxy platform
│ ├── surf-api-velocity # Public API
│ └── surf-api-velocity-server # Implementations
│
├── surf-api-standalone # Standalone (non-server) platform
│
├── surf-api-shared # Cross-platform annotations & component system
│ ├── surf-api-shared-public # Public shared annotations (@ComponentMeta, etc.)
│ └── surf-api-shared-internal # Internal shared utilities
│
├── surf-api-gradle-plugin # Gradle convention plugins & KSP processor
│ └── surf-api-processor # KSP processor — discovers @ComponentMeta at compile time
│
└── surf-api-generator # NMS module scaffolding generator (local only)
API/Implementation split:
-apimodules define public interfaces consumed by downstream projects.-servermodules contain implementations that are shaded into the final JAR and never exposed as a public API dependency.
surf-api-core provides small, type-safe environment-variable access without a separate schema or
lookup phase. Direct reads resolve immediately:
val DATABASE_HOST: String = env.required("DATABASE_HOST")
val DATABASE_PORT: Int = env.int("DATABASE_PORT", default = 5432) {
requireIn(1..65535)
}
val INSTANCE_ID: UUID? = env.optionalUuid("INSTANCE_ID")Property delegates resolve lazily on first access, infer the variable name exactly from the property, and cache the parsed value thread-safely:
object ServiceEnvironment {
val DATABASE_HOST by env.requiredNonBlank()
val DATABASE_PORT by env.int(default = 5432) {
requireIn(1..65535)
}
val DATABASE_PASSWORD by env.requiredSecret()
val DEBUG by env.boolean(default = false)
}Use env.required().named("DATABASE_HOST") for an explicitly named delegate. Required values are
non-null, optional values are nullable, and defaulted values are non-null. Missing required values,
conversion failures, and validation failures throw dedicated exceptions containing the variable
name. Mark values as sensitive, or use requiredSecret, to keep raw values and parser causes out of
error details. Required strings preserve present blank values; requiredNonBlank rejects them.
Built-in converters cover strings, numeric types, strict booleans, UUIDs, enums, and Kotlin
durations. Custom converters can be passed directly, for example
env.required("ID", UUID::fromString).
Two mechanisms are available depending on the use case:
For singleton platform bridges and NMS abstractions. Backed by Java ServiceLoader; implementations
register themselves with @AutoService.
// In -api: declare the bridge interface with companion delegation
@NmsUseWithCaution
interface SurfBukkitNmsCommonBridge {
fun nextEntityId(): Int
companion object : SurfBukkitNmsCommonBridge by requiredService()
}
// In -server: provide the implementation
@AutoService(SurfBukkitNmsCommonBridge::class)
class SurfBukkitNmsCommonBridgeImpl : SurfBukkitNmsCommonBridge {
init { checkInstantiationByServiceLoader() }
override fun nextEntityId(): Int = TODO()
}
// Usage — callers go through the companion, unaware of the implementation
val id = SurfBukkitNmsCommonBridge.nextEntityId()Classes annotated with @ComponentMeta (or the meta-annotations @Service / @Repository) are
discovered at compile-time by the KSP processor, which writes JSON metadata to
META-INF/surfapi/components/. At runtime, ComponentService:
- Loads all component metadata
- Topologically sorts by
@Priorityand@DependsOnComponent - Checks conditions (
@ConditionalOnProperty,@DependsOnPlugin,@DependsOnClass, …) - Calls
suspend fun load()→enable()→disable()in order
@Service
class MyFeatureService : SurfComponent {
override suspend fun enable() {
// runs after all dependencies are enabled
}
override suspend fun disable() {
// runs in reverse order on shutdown
}
}- Java 25 (GraalVM recommended for CI)
- Gradle 8+
# Full build (produces shadow JARs with relocated dependencies)
./gradlew build shadowJar
# Build a single module
./gradlew :surf-api-paper:surf-api-paper-server:build
# Publish to local Maven repository
./gradlew publishToMavenLocal
# Run the Paper test server (local only, not available in CI)
./gradlew :surf-api-paper:surf-api-paper-plugin-test:runServer| Convention | Details |
|---|---|
| Context parameters | Enabled globally (-Xcontext-parameters). Used in inventory DSLs and other scoped builders. |
@InternalSurfApi |
@RequiresOptIn — mark new internal APIs with this annotation. All subprojects opt-in via compiler args. |
| Coroutines | All async work uses Kotlin coroutines. Component lifecycle methods are suspend. Bukkit uses MCCoroutine (Folia-aware). |
| Extension functions | Primary API enrichment pattern; organized in files named *-extension.kt or *-extensions.kt. |
| DSL markers | @InventoryFrameworkDSL, @ItemDsl, @PaneMarker restrict scope in builder DSLs. |
Use Google Flogger via the logger() helper:
private val log = logger() // FluentLogger.forEnclosingClass()
log.atInfo().log("Server started")
log.atWarning().withCause(e).log("Failed to load component %s", name)dev.slne.surf.api # Root
dev.slne.surf.api.core.api # Core API interfaces
dev.slne.surf.api.core.server # Core implementations
dev.slne.surf.api.paper.api # Paper API (extensions, bridges, DSLs)
dev.slne.surf.api.paper.server # Paper implementations
dev.slne.surf.api.velocity.api # Velocity API
dev.slne.surf.api.velocity.server # Velocity implementations
dev.slne.surf.api.shared.api # Shared annotations & component system
dev.slne.surf.api.libs.* # Relocation target for all shaded dependencies
All non-platform dependencies are shaded via Shadow and relocated to dev.slne.surf.api.libs.*
(configured via relocationPrefix in gradle.properties). Platform APIs (Paper, Velocity) remain
compileOnly. Relocations are declared in CommonSurfPlugin using an infix DSL:
"me.devnatan.inventoryframework" relocatesTo "devnatan.inventoryframework"| Library | Purpose |
|---|---|
| PacketEvents | Packet-level networking abstraction |
| CommandAPI | Brigadier-based command framework |
| MCCoroutine (SLNE fork) | Folia-aware coroutine dispatching |
| Inventory Framework | GUI/inventory DSL |
| Adventure | Text & component API |
| LuckPerms | Permissions API |
| PlaceholderAPI | Placeholder support |
| Caffeine | High-performance caching |
| Flogger | Fluent logging |
| Ktor | HTTP client |
| Configurate | Configuration loading |
| KSP | Compile-time component discovery |
| AutoService | ServiceLoader registration |
When making changes:
- API changes go in
-apimodules. Never expose-servertypes in the public API. - Implementations go in
-servermodules. - New components: annotate with
@ComponentMeta/@Service/@Repositoryand implement the appropriate lifecycle methods. - New NMS bridges: declare the interface in
-api, implement in-server, register via@AutoService, and callcheckInstantiationByServiceLoader()in theinitblock. - Publishing is automated — push to a
version/*branch to trigger a release.
This project is licensed under the GNU General Public License v3.0.
- 📖 Documentation
- 📦 Releases
- 🐛 Issues
- 💬 Discussions