Variant Experience Server Reference

Variant Experience ServerResourcesDocumentation0.9Experience Server ⟫ Reference

Variant Experience Server, Reference

Release 0.9, June 2018

Related documentation: Variant API JavaDoc | Variant Server User Guide

a sche

1Variant Server Installation

Variant server is entirely self-contained and does not have any external dependencies. It requires Java runtime 8 or later. To install Variant server:

Download Variant Server distribution.

Unpack the distribution in a directory of your choice by running

$ unzip /path/to/variant-server-<release>.zip

This will create a server installation with the following directory structure:

Directory Description
bin/ Contains the server startup shell script variant.sh along with dependent scripts which you should never have to use.
conf/ Configuration directory, containing the server config file variant.conf, logging config file logback.xml, and other dependent configuration artifacts.
lib/ System libraries.
ext/ User extensions, developed on top of the ExtAPI, and their transitive dependencies. The distribution contains the the standard extension library variant-extapi-standard-<release>.jar. All JAR files in the ext/ directory are added to Variant server’s runtime classpath.
log/ Default destination for the server log files.
schemata/ Default location of the variation schema files.

To start Variant server in the foreground:

$ bin/variant.sh start

If all goes well, the server console output should look something like this:

$ bin/variant.sh start
[info] 14:14:40.758 c.v.c.c.ConfigLoader - Found  config resource [/variant.conf] as [/private/tmp/demo/variant-server-0.9.1/conf/variant.conf]
[info] 14:14:43.232 c.v.s.s.SchemaDeployerFileSystem - Mounted schemata directory [/private/tmp/demo/variant-server-0.9.1/schemata]
[info] 14:14:43.234 c.v.s.s.SchemaDeployerFileSystem - [431] No schemata found in [/private/tmp/server/variant-server-0.9.1/schemata]
[info] 14:14:43.593 c.v.s.b.VariantServerImpl - [431] Variant Experience Server release 0.9.1 bootstrapped on :5377/variant in 00:00.393

At this point, the server is ready to deploy variation schema files, which you copy into the schemata/ directory.

By default, the server attaches to port 5377 and listens to client connections at the HTTP context /variant. To start the server on a non-default port, you must pass the port number as a JVM system variable http.port. For example, to start on port 9999:

$ bin/variant.sh start -Dhttp.port=9999

To change the server’s HTTP context, pass the new value in the server configuration property play.http.context either on the command line:

$ bin/variant.sh start -Dhttp.port=9999 -Dplay.http.context=/foo

or by changing it in the conf/variant.conf file.

You may always ping a running server by typing its URL into a Web browser, or with the cURL command, which should display the server’s version and uptime.

$ curl http://localhost:5377/variant
Variant Experience Server release 0.9.3. Uptime 29207.

A running server can be stopped by pressing Control-C in the shell where it was started, or by typing in a different shell window

$ bin/variant.sh stop

2Variant Server Configuration

2.1Sources Of Configuration

Variant server is configured via configuration parameters organized in conf files. Variant uses the Typesafe Config library , an implementation of the HOCON configuration grammar .

At startup, Variant server looks for configuration in the file conf/variant.conf. If it is found, its contents override the default settings, listed in the next section. If you need to modify any configuration parameters, you may either update them directly in the conf/variant.conf, or provide an alternate configuration file whose contents take precedence over those of the conf/variant.conf file.

The alternate config file may be provided on the command line either as a file system file:

$ variant.sh start -Dvariant.config.file=/path/to/alt/config/as/file

or as a Java classpath resource:

$ variant.sh start -Dvariant.config.resource=resource-name

The simplest way to add a resource to the server classpath is to place it in the server’s conf directory, which is added to the server’s runtime classpath at the root level. In other words, the file conf/extra.conf should be referenced as

$ variant.sh start -Dvariant.config.resource=/extra.conf

It is an error to set both variant.config.file and variant.config.resource system properties.

Finally, each individual config parameter may be overridden from the command line via the JVM system variable of the same name:

$ variant.sh start -Dvariant.schemata.dir=/home/variant/schemata

2.2Variant Server Configuration Properties

The following table lists all config properties recognized by Variant server.

Property Default Value / Description
variant.schemata.dir "schemata"
The directory where Variant server looks for variation schemata, as an OS file name. If starts with a slash, interpreted as an absolute path. Otherwise, as relative path to the server startup directory.
variant.session.timeout 900
Session timeout. User sessions are cleaned out after this many seconds of inactivity.
variant.session.vacuum.interval 10
The session vacuum thread is woken up no less frequently than this many seconds.
variant.event.writer.buffer.size 20000
Max number of trace events which can be held in memory between flushes. Flusher thread is woken up whenever this buffer becomes 50% full, but no less frequently than specified by variant.event.writer.max.delay. Each variation schema gets its own event buffer, shared between all variations in that schema.
variant.event.writer.max.delay 30
Wake up event flusher thread no less frequently than this many seconds, even if there may not be any new trace events to flush.
variant.event.flusher.class.name com.variant.server.api.EventFlusherAppLogger
Default event flusher implementation, implicitly provided to those variation schemata that do not define their own.
variant.event.flusher.class.init null
Arbitrary JSON object, whose parsed representation will be passed to the constructor of the event flusher implementation. Not required by the default implementation.

2.3Play/Akka Configuration Properties

Variant Experience Server is an HTTP server, built on top of the Play Framework 2.6  running the Akka HTTP Server . Both Play and Akka have a number of their own config parameters, which can be set in order to change their default behavior, e.g. configure thread pools or SSL certificates.

For the most part, Variant server does not override the default Play and Akka config settings, with the following two exceptions:

  • Play's Application Secret.
    Play framework has a robust way of protecting a Web application against attacks like session hijacking with the application secret mechanism . Variant ships with one pre-set, so that everything works right out-of-the-box. If you want to generate your own application secret, set it in the play.http.secret.key config property
  • Play's cross-origin resource sharing (CORS) filter configuration.
    In order for the Variant JavaScript client to make asynchronous HTTP calls to Variant server, it must implement the CORS protocol. Play framework comes with such an implementation  which is enabled and configured in a series of config properties explained below. You may comment out these properties, if you are not using the JavaScript client.

The following table lists all Play or Akka config properties overridden by Variant in the file conf/variant.conf:

Property Default Value / Description
play.http.context "/variant"
Variant server default context
play.http.secret.key A long random string.
Play's application secret.
play.filters.enabled "play.filters.cors.CORSFilter"
Enable Play's cross-origin resource sharing (CORS) filter.
play.filters.cors.pathPrefixes ["/"]
Play's CORS filter configuration: allow all paths.
play.filters.cors.allowedOrigins null
Play's CORS filter configuration: allow all origins
play.filters.cors.allowedHttpMethods ["GET", "POST", "PUT", "DELETE"]
Play's CORS filter configuration: allow the methods used by Variant.
play.filters.cors.allowedHttpHeaders null
Play's CORS filter configuration: allow all headers.

3Variation Schema Grammar

Variant manages variation metadata in human readable files, called schema files. Each schema file contains a single XVM schema describing a set of related experience variations instrumented on some host application using a JSON grammar. This chapter provides detailed information on the schema grammar.

3.1Syntax

The following conventions are used throughout this chapter:

any-string Arbitrary, case sensitive, quoted Unicode string. You must follow JSON's escape rules if you want a string contain special characters.
name-string Case sensitive, quoted string containing only Unicode letters, digits or the '_' (underscore) and not starting with a digit. The name mySchema in the above example is a name-string. Variant server will emit a parse time error if a non-conforming string is found where a name-string is expected.
boolean JSON Boolean value of true or false.
number JSON numeric value.
literal Arbitrary JSON literal. This may be a primitive type, an object, or an array.
object@type JSON object literal enclosed in {...} of a particular type.
[object@type] JSON array literal enclosed in [...] of objects of a particular type.

Variation schema format extends the standard JSON grammar by allowing comments. A single-line comment starts with // and continues to the end of line, and a multi-line comment starts with /* and ends with */.

Property names, i.e. the strings to the left of the ‘:’, are the schema keywords. Keywords are case-insensitive: in the listing above, 'States' or 'NAME' will also work.

3.2General Structure

A variation schema is comprised of three sections: meta, states and tests, which must appear in this order:

{                                                           
  'meta':object@meta-spec,
  'states':[object@@state-spec],
  'variations':[object@variation-spec]
}

Variant schema parser is single-pass, so no forward references are allowed. The meta section must appear first, followed by states, followed by tests, because test definitions refer back to states. Similarly, referenced tests must be defined before the concurrent tests that reference them.

3.3META Section

The meta section contains properties which apply to the entire schema:

meta-spec :=
  {  
    'name':name-string,
    'comment':any-string,
    'flusher':object@flusher-spec,
    'hooks':[object@hook-spec]
  }
Property Type Description Required Default
name name-string The name by which this schema will be connected to by clients. Yes
comment any-string A comment about this schema. No
flusher Object of type flusher-spec An event flusher specification defines a schema-specific trace event flusher. Applies to all trace events generated by the tests configured by this schema. No As configured by the server config properties.
hooks Array of objects of type hook-spec A list of schema-scoped lifecycle hook specifications. Hooks defined at this scope apply to all states and all tests defined by this schema. No

The flusher specification defines a schema-specific event flusher:

flusher-spec :=
  {  
    'class':any-string,
    'init':literal
  }
Property Type Description Required Default
class any-string Fully qualified name of the implementing class. Yes
init literal Externalized initial state, passed to the object's constructor. No

Each schema gets its own instance of an event flusher, instantiated by Variant server at schema deployment time. The Java class referenced by the class property must be on the server's class path. You may pass arbitrary state data to this instance by supplying the init property, which can be any JSON literal. This init object will be parsed into a Typesafe Config  Java object and passed to the constructor of the implementing type, which must be provided by the implementation.

For more information, see Section 4.3 Event Flushers.

3.4Lifecycle Hooks

hook-spec :=
  {  
    'name':name-string,
    'class':any-string,
    'init':literal
  }
Property Type Description Required Default
name name-string The name of the hook. Must be unique within its scope. Yes
class any-string Fully qualified name of the implementing class. Yes
init literal Externalized initial state, passed to the object's constructor. No

The Java class referenced by the class property must be on the server's class path. You may pass arbitrary state data to this instance by supplying the init property, which can be any JSON literal. This object will be parsed into a Typesafe Config  Java object and passed to the constructor of the implementing type, which must be provided by the implementation.

Lifecycle hooks can be defined at the meta, state, or variation scope, which determines what objects and lifecycle event types post them. For more information, see Section 4.1 Lifecycle Hooks.

3.5SATES Section

In Experience Variation Model, a state represents an interface state of the host application where it pauses for user input. XVM states are rather abstract: the only required property is name, by which the state can be referenced.

In XVM, states form a set, not a graph. In other words, user sessions traverse states one at a time, but there is no predefined order in which these states can be visited or which states are accessible from any particular state.

state-spec :=
  {  
    'name':name-string,
    'parameters':[object@state-parameter-spec],
    'hooks':[object@hook-spec]
  }
Property Type Description Required Default
name name-string The name of the state. Must be unique within a schema. Yes
parameters Array of objects of type state-parameter-spec A list of state parameter specifications. No
hooks Array of objects of type hook-spec A list of state-scoped lifecycle hook specifications. Hooks defined at this scope apply to this state only and must be subscribed to a lifecycle event descendant from StateAwareLifecycleEvent . No

2.6State Parameters

Although XVM is agnostic of the nature or technology of the host application, it provides a mechanism for the host application to attach application-specific state to XVM states, which the host application can access at run time. This is accomplished with state parameters. They are specified at the state level, as already explained in the previous section, and at the state variant level, as explained later in Section 3.7.3. Each state parameter is a name/value pair, where both the name and the value are strings.

state-parameter-spec :=
  {  
    'name':name-string,
    'value':any-string
  }
Property Type Description Required Default
name name-string Parameter's name. Yes
value any-string Parameter's value. Yes

Whenever the host application calls the Session.targetForState() method, server picks from the given state's variant space a single state variant. The state parameters defined at that state variant, if any, override the like-named parameters defined at the base state. (Remember, names are case sensitive.) These resolved state parameters are available to the host application via the StateRequest.getResolvedParameters() method.

3.7VARIATIONS Section

3.7.1Variations

A variation is an experience variation, instrumented over one or more states. It must have exactly one control experience (the one mapped to the current code path) and one or more variant experiences, mapped to the candidate code path(s).

variation-spec :=
  {                       
    'name':name-string,
    'isOn':boolean, 
    'experiences':[object@experience-spec],
    'conjointVariationRefs':[name-string],
    'onStates':[object@on-state-spec],
    'hooks':[object@hook-spec]
  }
Property Type Description Required Default
name name-string The name of the variation. Must be unique within a schema. Yes
isOn boolean Indicates whether the variation is online or offline. No true
experiences Array of objects of type experience-spec A list of experience specifications. Yes
conjointVariationRefs Array of objects of type name-string Conjoint variation references. These are names of variations conjointly concurrent with this variation. Referenced variation(s) must be already defined earlier in the schema. No
onStates Array of objects of type on-state-spec A list of on-state specifications. Yes
hooks Array of objects of type hook-spec A list of variation-scoped lifecycle hook specifications. Hooks defined at this scope apply to this variation only and must be subscribed to a lifecycle event descendant from VariationAwareLifecycleEvent . No

For each state on the onStates list, Variant builds a state variant space as a Cartesian product of this variation's experience set and the experience sets of all those variations mentioned on the conjointVariationRefs list. Refer to Variant Server User Guide for more information on concurrent variations.

Each variation-scoped hook must listen to a lifecycle event descendant from VariationAwareLifecycleEvent . For more information, see Section 4.1 Lifecycle Hooks.

The isOn property is used to turn an experiement or a feature toggle temporarily offline without removing it from the schema. No sessions are targeted for an offline variation, as if it didn't even exist. In fact, the only differences between an offline variation and a variation that is completely removed from the schema is that if it defines its targeting or qualification persistence as durable, this information is preserved. In practice this means that, after an offline variation is taken back online, return users will see the same experience they saw before the variation was taken offline.

3.7.2Variation Experiences

Each element of a variation's experiences property describes one of its experiences.

experience-spec :=
  {
    'name':name-string,
    'weight':number,
    'isControl':boolean
  }
Property Type Description Required Default
name name-string The name of the experience. Must be unique within enclosing variation. Yes
weight number The probabilistic weight of this experience relative to other experiences of this variation. Used by the default targeting hook, which targets sessions randomly, according to these weights. The probability that a session is targeted to an experience is the ratio of the weight of the experience to the sum of weights of all of the variation's experiences. No 1
isControl boolean Indicates whether this experience is the control experience in the enclosing variation. Exactly one experience must be marked as control. No false

3.7.3Variation On-States

The onStates property contains a list of elements, each of which describes the enclosing variation's instrumentation details on a particular state.

on-state-spec :=
  {                                                
    'stateRef':name-string
    'variants':[object@state-variant-spec]
  }
Property Type Description Required Default
stateRef name-string Back reference to a state that’s already been defined, whose instrumentation by the enclosing test is defined by this property. Yes
varaints Array of objects of type state-variant-spec A list of this state's variant specifications. No

Whenever a variation V instruments state S, Variant schema parser creates the state variant space ν(V,S) as a Cartesian product of the set of V's experiences and the experience sets of all variations conjointly concurrent with V. All state variants in code>ν(V,S) implicitly inherit the state parameters as defined in the base state S. In most cases, this implicitly derived state variant space is sufficient, and you will not need to define state vairants explicitly.

However, there are two use cases when explicit re-definition of one or more state variants is needed:

  1. Phantom state variants.
    Implicitly inferred state variants are not phantom. If you need to define a state variant as phantom, you must define that state phantom explicitly. Refer to Server User Guide for more information on mixed instrumentation.
  2. Variant specific state parameters.
    Implicitly inferred state variants inherit their state parameters from the base state. If you need to define variant-specific parameters, you must define that state phantom explicitly. Refer to Server User Guide for more information on state parameter inheritence.

3.7.4State Variants

The variants property contains a list of elements, each of which describes a particular state variant.

state-variant-spec :=
  {
    'isPhantom':boolean,
    'experienceRef':name-string,
    'conjointExperienceRefs': [
      {                                                 
        'testRef':name-string,
        'experienceRef':name-string
      }
      , ...
    ],
    'parameters':[object@state-parameter-spec]?
  }
Property Type Description Required Default
isPhantom boolean Indicates whether this state variant is phantom. If set to true, incompatible with any other property.

No false
experienceRef name-string Reference to one of the enclosing variation's own variant experiences. Cannot refer to the control experience. Not allowed in phantom state variants. Yes
conjointExperienceRefs Array of objects. Reference to one of the enclosing variation's conjoint experiences. Not allowed in phantom state variants. Yes
parameters Array of objects of type state-parameter-spec. A list of state parameter specifications. Not allowed in phantom state variants. No

A state variant is phantom in a particular experience if it is not instrumented by the experience. Phantom state variants possess the following semantics:

  • When a session is targeted for a variation, phantom experiences are not considered.
  • When a session requests a state which is phantom in a live experience, Variant will emit a runtime user error.

Refer to Variant Server User Guide for more information on mixed instrumentation.

4Server Extension API

Variant server's functionality can be extended through the use of the server-side Extension API, or ExtAPI, which exposes Java bindings for supplying custom server-side extensions. These extensions facilitate injection of custom semantics into the server's default execution path via a callback mechanism. Two types of user-defined callback objects are supported:

  • Lifecycle Event Hooks are listeners for various lifecycle events, raised by the server. They encapsulate custom, application-aware code, which takes over and alters the default handling of lifecycle events.
  • Trace Event Flushers handle the final ingestion of trace events. Each variation schema can have its own event flusher.

Both lifecycle hooks and event flushers are configured in the variation schema. The next two chapters explain their semantics and configuration, and chapter 4.3 provides details on how to write them.

4.1Lifecycle Hooks

4.1.1Instantiation and Scope

Lifecycle hooks provide callback methods which are posted by Variant server whenever a lifecycle event of interest is raised. A lifecycle hook listens to lifecycle events of a certain type, and, when an event of that type is raised, the hook is notified via its callback method LifecycleHook.post()  method.

A custom lifecycle hook must implement the LifecycleHook  interface. By contract, an implementation must also provide at least one of these constructors:

  • Nullary constructor, if no init property was given in the hook definition.
  • Single argument constructor with argument type Config . If no init property was given and no nullary constructor is available, this constructor will be called with null argument; otherwise, the value of the init property will be parsed and passed to this constructor.

Hook definitions may appear in variation schema in one of three scopes:

  • Schema-scoped hooks are defined inside of the meta section. These hooks are applicable to all states and all variations in the schema. Hooks listening to lifecycle events descendant from StateAwareLifecycleEvent  will be posted for each state in the schema by every state aware lifecycle event. Hooks listening to lifecycle events descendant from VariationAwareLifecycleEvent  will be posted for all online variations in the schema by every variation aware lifecycle event.
  • State-scoped hooks are defined inside of the state definition. These hooks are applicable to the enclosing state only and must listen to events descendant from StateAwareLifecycleEvent .
  • Variation-scoped hooks are defined inside of the variation definition. These hooks are applicable to the enclosing variation only and must listen to events descendant from VariationAwareLifecycleEvent .

4.1.2Lifecycle Events

The following is the complete list of lifecycle events raised by Variant server:

Parse-time Lifecycle Event Applicable Scope Default Hook
StateParsedLifecycleEvent  Schema
State
No action
Raised during parsing of the schema, whenever a state has been successfully parsed. Will not be triggered for states that contain parse errors. Posts all eligible schema-scoped hooks and those state-scoped hooks whose state matches that of the triggering event. Lifecycle hook, subscribed to this event, may perform additional parse time checks, e.g. for presence or correctness of state parameters.
VariationParsedLifecycleEvent  Schema
Variation
No action
Raised during parsing of the schema, whenever a variation is successfully parsed. Will not be triggered for variations that contain parse errors. Posts all eligible schema-scoped hooks and those variation-scoped hooks whose variation matches that of the triggering event. Lifecycle hook, subscribed to this event, may perform additional parse time checks, e.g. for presence or correctness of state variants or parameters.
Runtime Lifecycle Event Applicable Scope Default Hook
VariationQualificationLifecycleEvent  Schema
Test
Session is qualified for the variation.
Raised when a Variant session must be qualified for a variation. Posts all eligible schema-scoped hooks and those variation-scoped hooks whose variation matches that of triggering event. Used to qualify (or disqualify) a user session for the triggering variation, based on a custom qualification criteria.
VariationTargetingLifecycleEvent  Schema
State
Variation
Variation is targeted randomly, according to the weight properties.
Raised when a Variant session must be targeted for a variation. Posts all eligible schema-scoped hooks, those state-scoped hooks whose state matches that of triggering event, and those variation-scoped hooks whose variation matches that of the triggering event. Used to provide custom targeting algorithm.

Keep in mind that if you are conducting an online experiment, your custom targeting algorithm must be randomized.

4.1.3Hook Chaining

A lifecycle hook is eligible to be posted by a lifecycle event E, if its getLifecycleEventClass() method returns a class assignable to E.

If more than one hook is eligible to be posted, they form a hook chain and are posted in the following order:

  • Variation-scoped hooks, then state-scoped hooks, then schema-scoped hooks.
  • Within a scope, hooks are posted in the ordinal order, i.e. the order in which they are defined in the schema, in the corresponding scope.

When a hook is posted, its post(E event)  method is called by Variant server with the actual triggering lifecycle event instance. If the post(E event) method returns a non-null value, Variant server ignores the rest of the hook chain, expecting the returned object to contain the information it requires to proceed. Otherwise, Variant posts the next hook on the chain.

If no lifecycle hook on the chain returned a non-null value (or no eligible hooks were defined) Variant posts the built-in default hook for the event, which always returns a value.

4.2Trace Event Flushers

Variant trace events are the elementary data points generated by user traffic as it flows through Variant variations with the purpose of subsequent analysis by a downstream process. Trace events can be triggered implicitly, by Variant, or explicitly by the host application. In either case, the host application can attach attributes to these events, to aid in the downstream analysis.

Variant server enriches all trace events with the following metadata:

  • Variant session ID by which related events can be associated.
  • Live test experience names.

Whenever a trace event is triggered, it is handed off to the asynchronous event writer, which first stores it in the server's memory buffer. A dedicated flusher thread is woken up periodically to flush the content of this buffer to external storage. A single event writer serves the entire Variant server instance. All trace events, triggered by all variations in all schemata, are buffered in the single event writer buffer, whose size is configured by the variant.event.writer.buffer.size config property. The larger the buffer, the better the event writer can cope with bursts in tracing volume, though at the expense of additional memory footprint. Whenever the flusher thread is not keeping up with the tracing volume, event writer will discard new events until the flusher thread has drained some events from the buffer.

The flusher thread wakes up when the event buffer becomes half full, but no less frequently than every variant.event.writer.max.delay seconds. For handling of the actual writes to their final destination, e.g. a database, flusher thread delegates to the appropriate event flusher object. Each variation schema gets its own copy of an event flusher, which is instantiated at schema deployment time and can be configured with the flusher property in the meta section.

The only implicitly triggered trace event is the state visited event. It is created at the end of the targeting step and added to Variant state request, but not yet triggered. The host application can get a hold of it by calling StateRequest.getStateVisitedEvent()  and add custom event attributes to it, which will be serialized with the event when it's handed over to the event flusher.

The state visited event is triggered when the host application either commits or fails the state request. It is the responsibility of the host application to commit or fail each state request by calling StateRequest.commit()  or StateRequest.fail() , respectively.

There are two built-in flusher classes that you may find useful for testing purposes because they don't actually require any external storage.

Built-in Flusher Class Description
EventFlusherNull  Discards all Variant events.
EventFlusherAppLogger  Appends events to the server log file. Out of the box, Variant server is configured to use this as the default flusher.

Additionally, the following pre-built trace event flushers can be found in the server's standard extension, discussed in Section 4.4. These flusher classes are available to the server at runtime, so you can simply configure them in your schema.

Standard Extension Flusher Class Description
TraceEventFlusherH2  Writes Variant events to an H2 database. Database connection is configured with the init property. The SQL scripts required to create the database schema expected by this flusher can be found in db/h2  directory. You must also copy the H2 JDBC driver into Variant server's ext/ directory.
TraceEventFlusherMysql  Writes Variant events to a MySQL database. Database connection is configured with the init property. The SQL scripts required to create the database schema expected by this flusher can be found in db/mysql  directory. You must also copy the MySQL JDBC driver into Variant server's ext/ directory.
TraceEventFlusherPostgres  Writes Variant events to a PostgreSQL database. Database connection is configured with the init property. The SQL scripts required to create the database schema expected by this flusher can be found in db/postgres  directory. You must also copy the Postgres JDBC driver into Variant server's ext/ directory.

4.3Developing for the ExtAPI

Variant server extension API library variant-server-extapi-<release>.jar can be downloaded from the Variant website. Create a new Java project in your IDE, add this JAR file to the classpath and you are set. You must package your project as JAR. To add the packaged JAR file to Variant server's runtime classpath, copy it (and its dependencies) into the server's ext/ directory.

Alternatively, but with the same ultimate result, — you may clone the standard extension public repository  into a local workspace, remove the source files, change the groupId, artifactId and the version  to suit your environment,— and you have a working shell of a brand new ExtAPI development project.

Note, that a running Variant server loads all hook and flusher classes from the class path only once, when they are first encountered. Therefore, replacing classes in the server's ext/ directory will not have any effect and may even lead to unexpected behavior. Always restart your Variant server when you redeploy your custom ExtAPI classes.

4.4The Standard Extension Library

Variant server standard extension is a library of general purpose extensions, that are not part of the core Variant server but are developed on top of the server-side ExtAPI. All classes in the standard extension are automatically available at runtime, because it is included in the server distribution as file variant-extapi-standard-<release>.jar in the ext/ directory. All JAR files in the ext/ directory become part to Variant server's runtime classpath.

The standard extension library is an open source project, available on GitHub  under the Apache 2 license. You may find it useful to examine the standard compnents' source code first before developing your own custom ExtAPI objects.