Refactor couch_epi to simplify it
diff --git a/src/couch_epi.erl b/src/couch_epi.erl
index 7388ac2..ab3703e 100644
--- a/src/couch_epi.erl
+++ b/src/couch_epi.erl
@@ -13,7 +13,7 @@
 -module(couch_epi).
 
 %% subscribtion management
--export([subscribe/5, unsubscribe/1, get_handle/1]).
+-export([get_handle/1]).
 -export([register_service/1]).
 
 %% queries and introspection
@@ -28,7 +28,18 @@
 
 -export([is_configured/3]).
 
--export_type([service_id/0, app/0, key/0, handle/0, notify_cb/0]).
+%% ------------------------------------------------------------------
+%% Types Definitions
+%% ------------------------------------------------------------------
+
+-export_type([
+    service_id/0,
+    app/0,
+    key/0,
+    handle/0,
+    plugin_id/0,
+    data_spec/0
+]).
 
 -type app() :: atom().
 -type key() :: term().
@@ -36,11 +47,7 @@
 
 -type properties() :: [{key(), term()}].
 
--type notification() :: {data, term()} | {modules, [module()]}.
--type notify_cb() :: fun(
-    (App :: app(), Key :: key(), Data :: notification(), Extra :: term()) -> ok).
-
--type subscription() :: term().
+-type plugin_id() :: module().
 
 -opaque handle() :: module().
 
@@ -51,6 +58,12 @@
 
 -type apply_opts() :: [apply_opt()].
 
+-type data_spec()
+    :: {module, module()}
+        | {priv_file, FileName :: string()}
+        | {file, FileName :: string()}.
+
+
 %% ------------------------------------------------------------------
 %% API Function Definitions
 %% ------------------------------------------------------------------
@@ -115,22 +128,6 @@
 subscribers(Handle) ->
     couch_epi_data_gen:subscribers(Handle).
 
-
-%% Passed MFA should implement notify_cb() type
--spec subscribe(App :: app(), Key :: key(),
-    Module :: module(), Function :: atom(), Args :: [term()]) ->
-        {ok, SubscriptionId :: subscription()}.
-
-subscribe(App, Key, M, F, A) ->
-    couch_epi_server:subscribe(App, Key, {M, F, A}).
-
-
--spec unsubscribe(SubscriptionId :: subscription()) -> ok.
-
-unsubscribe(SubscriptionId) ->
-    couch_epi_server:unsubscribe(SubscriptionId).
-
-%% The success typing is (atom() | tuple(),_,_,[any()],_) -> [any()]
 -spec apply(Handle :: handle(), ServiceId :: atom(), Function :: atom(),
     Args :: [term()], Opts :: apply_opts()) -> ok.
 
@@ -166,14 +163,8 @@
     [] /= couch_epi_functions_gen:modules(Handle, Function, Arity).
 
 
--spec register_service({ServiceId :: service_id(), Key :: key()}) -> ok;
-                (ServiceId :: service_id()) -> ok.
+-spec register_service(PluginId :: plugin_id()) ->
+    [supervisor:child_spec()].
 
-register_service({_ServiceId, _Key} = EPIKey) ->
-    register_service(couch_epi_data_gen, EPIKey);
-register_service(ServiceId) when is_atom(ServiceId) ->
-    register_service(couch_epi_functions_gen, ServiceId).
-
-register_service(Codegen, Key) ->
-    Handle = Codegen:get_handle(Key),
-    couch_epi_module_keeper:register_service(Codegen, Handle).
+register_service(Plugin) ->
+    couch_epi_sup:plugin_childspecs(Plugin).
diff --git a/src/couch_epi.hrl b/src/couch_epi.hrl
new file mode 100644
index 0000000..a8bd1d5
--- /dev/null
+++ b/src/couch_epi.hrl
@@ -0,0 +1,15 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-record(couch_epi_spec, {
+    behaviour, app, kind, options, key, value, codegen, type
+}).
diff --git a/src/couch_epi_codechange_monitor.erl b/src/couch_epi_codechange_monitor.erl
new file mode 100644
index 0000000..7384804
--- /dev/null
+++ b/src/couch_epi_codechange_monitor.erl
@@ -0,0 +1,63 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_epi_codechange_monitor).
+
+-behaviour(gen_server).
+
+%% ------------------------------------------------------------------
+%% API Function Exports
+%% ------------------------------------------------------------------
+
+-export([start_link/1]).
+
+%% ------------------------------------------------------------------
+%% gen_server Function Exports
+%% ------------------------------------------------------------------
+
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+         terminate/2, code_change/3]).
+
+%% ------------------------------------------------------------------
+%% API Function Definitions
+%% ------------------------------------------------------------------
+
+start_link(Handler) ->
+    gen_server:start_link(?MODULE, [Handler], []).
+
+%% ------------------------------------------------------------------
+%% gen_server Function Definitions
+%% ------------------------------------------------------------------
+
+init([Handler]) ->
+    couch_epi_module_keeper:reload(Handler),
+    {ok, Handler}.
+
+handle_call(_Request, _From, State) ->
+    {reply, ok, State}.
+
+handle_cast(_Msg, State) ->
+    {noreply, State}.
+
+handle_info(_Info, State) ->
+    {noreply, State}.
+
+terminate(_Reason, _State) ->
+    ok.
+
+code_change(_OldVsn, Keeper, _Extra) ->
+    couch_epi_module_keeper:reload(Keeper),
+    {ok, Keeper}.
+
+%% ------------------------------------------------------------------
+%% Internal Function Definitions
+%% ------------------------------------------------------------------
diff --git a/src/couch_epi_codegen.erl b/src/couch_epi_codegen.erl
index caedb88..1901887 100644
--- a/src/couch_epi_codegen.erl
+++ b/src/couch_epi_codegen.erl
@@ -50,7 +50,6 @@
     case erl_parse:parse_form(Forms) of
         {ok, AST} -> {ok, AST};
         {error,{_,_, Reason}} ->
-            %%Expr = [E || {E, _Form} <- Tokens],
             {error, Expr, Reason}
     end.
 
diff --git a/src/couch_epi_data.erl b/src/couch_epi_data.erl
index 502ed69..9370482 100644
--- a/src/couch_epi_data.erl
+++ b/src/couch_epi_data.erl
@@ -12,139 +12,103 @@
 
 -module(couch_epi_data).
 
--behaviour(gen_server).
+-include("couch_epi.hrl").
 
 %% ------------------------------------------------------------------
 %% API Function Exports
 %% ------------------------------------------------------------------
 
--export([childspec/4]).
--export([start_link/4, reload/1]).
--export([wait/1, stop/1]).
-
-%% ------------------------------------------------------------------
-%% gen_server Function Exports
-%% ------------------------------------------------------------------
-
--export([init/1, handle_call/3, handle_cast/2, handle_info/2,
-         terminate/2, code_change/3]).
-
--record(state, {
-    subscriber, module, key, hash, handle,
-    initialized = false, pending = []}).
+-export([interval/1, data/1]).
 
 %% ------------------------------------------------------------------
 %% API Function Definitions
 %% ------------------------------------------------------------------
 
-childspec(Id, App, EpiKey, Module) ->
-    {
-        Id,
-        {?MODULE, start_link, [
-            App,
-            EpiKey,
-            Module,
-            []
-        ]},
-        permanent,
-        5000,
-        worker,
-        [Module]
-    }.
+interval(Specs) ->
+    extract_minimal_interval(Specs).
 
-start_link(SubscriberApp, {epi_key, Key}, Module, Options) ->
-    maybe_start_keeper(Key),
-    gen_server:start_link(?MODULE, [SubscriberApp, Module, Key, Options], []).
-
-reload(Server) ->
-    gen_server:call(Server, reload).
-
-wait(Server) ->
-    gen_server:call(Server, wait).
-
-stop(Server) ->
-    catch gen_server:call(Server, stop).
-
-%% ------------------------------------------------------------------
-%% gen_server Function Definitions
-%% ------------------------------------------------------------------
-
-init([Subscriber, Module, Key, _Options]) ->
-    gen_server:cast(self(), init),
-    {ok, #state{
-        subscriber = Subscriber,
-        module = Module,
-        key = Key,
-        handle = couch_epi_data_gen:get_handle(Key)}}.
-
-handle_call(wait, _From, #state{initialized = true} = State) ->
-    {reply, ok, State};
-handle_call(wait, From, #state{pending = Pending} = State) ->
-    {noreply, State#state{pending = [From | Pending]}};
-handle_call(reload, _From, State) ->
-    {Res, NewState} = reload_if_updated(State),
-    {reply, Res, NewState};
-handle_call(stop, _From, State) ->
-    {stop, normal, State};
-handle_call(_Request, _From, State) ->
-    {reply, ok, State}.
-
-handle_cast(init, #state{pending = Pending} = State) ->
-    {_, NewState} = reload_if_updated(State),
-    [gen_server:reply(Client, ok) || Client <- Pending],
-    {noreply, NewState#state{initialized = true, pending = []}};
-handle_cast(_Msg, State) ->
-    {noreply, State}.
-
-handle_info(_Info, State) ->
-    {noreply, State}.
-
-terminate(_Reason, _State) ->
-    ok.
-
-code_change(_OldVsn, State, _Extra) ->
-    {_, NewState} = reload_if_updated(State),
-    {ok, NewState}.
+data(Specs) ->
+    Locators = locate_sources(Specs),
+    case lists:foldl(fun collect_data/2, {ok, [], []}, Locators) of
+        {ok, Hashes, Data} ->
+            {ok, couch_epi_util:hash(Hashes), Data};
+        Error ->
+            Error
+    end.
 
 %% ------------------------------------------------------------------
 %% Internal Function Definitions
 %% ------------------------------------------------------------------
 
-reload_if_updated(#state{handle = Module} = State) ->
-    case couch_epi_util:module_exists(Module) of
+collect_data({App, Locator}, {ok, HashAcc, DataAcc}) ->
+    case definitions(Locator) of
+        {ok, Hash, Data} ->
+            {ok, [Hash | HashAcc], [{App, Data} | DataAcc]};
+        Error ->
+            Error
+    end;
+collect_data({_App, _Locator}, Error) ->
+    Error.
+
+extract_minimal_interval(Specs) ->
+    lists:foldl(fun minimal_interval/2, undefined, Specs).
+
+minimal_interval({_App, #couch_epi_spec{options = Options}}, Min) ->
+    case lists:keyfind(interval, 1, Options) of
+        {interval, Interval} -> min(Interval, Min);
+        false -> Min
+    end.
+
+locate_sources(Specs) ->
+    lists:map(fun({ProviderApp, #couch_epi_spec{value = Src}}) ->
+        {ok, Locator} = locate(ProviderApp, Src),
+        {ProviderApp, Locator}
+    end, Specs).
+
+locate(App, {priv_file, FileName}) ->
+    case priv_path(App, FileName) of
+        {ok, FilePath} ->
+            ok = check_exists(FilePath),
+            {ok, {file, FilePath}};
+        Else ->
+            Else
+    end;
+locate(_App, {file, FilePath}) ->
+    ok = check_exists(FilePath),
+    {ok, {file, FilePath}};
+locate(_App, Locator) ->
+    {ok, Locator}.
+
+priv_path(AppName, FileName) ->
+    case code:priv_dir(AppName) of
+        {error, _Error} = Error ->
+            Error;
+        Dir ->
+            {ok, filename:join(Dir, FileName)}
+    end.
+
+check_exists(FilePath) ->
+    case filelib:is_regular(FilePath) of
         true ->
-            do_reload_if_updated(State);
+            ok;
         false ->
-            {ok, State}
+            {error, {notfound, FilePath}}
     end.
 
-do_reload_if_updated(#state{hash = OldHash, module = Module} = State) ->
-    case couch_epi_functions_gen:hash([Module]) of
-        OldHash ->
-            {ok, State};
-        Hash ->
-            safe_set(Hash, State)
-    end.
+definitions({file, FilePath}) ->
+    case file:consult(FilePath) of
+        {ok, Data} ->
+            {ok, hash_of_file(FilePath), Data};
+        {error, Reason} ->
+            {error, {FilePath, Reason}}
+    end;
+definitions({module, Module}) when is_atom(Module) ->
+    definitions({module, [Module]});
+definitions({module, Modules}) ->
+    Data = lists:append([M:data() || M <- Modules]),
+    Hash = couch_epi_functions_gen:hash(Modules),
+    {ok, Hash, Data}.
 
-safe_set(Hash, #state{} = State) ->
-    #state{
-        handle = Handle,
-        subscriber = Subscriber,
-        module = Module,
-        key = Key} = State,
-    try
-        Data = get_from_module(Module),
-        OldData = couch_epi_data_gen:current_data(Handle, Subscriber),
-        ok = couch_epi_data_gen:set(Handle, Subscriber, Data),
-        couch_epi_server:notify(Subscriber, Key, {data, OldData}, {data, Data}),
-        {ok, State#state{hash = Hash}}
-    catch Class:Reason ->
-        {{Class, Reason}, State}
-    end.
-
-get_from_module(Module) ->
-    Module:data().
-
-maybe_start_keeper(Key) ->
-    Handle = couch_epi_data_gen:get_handle(Key),
-    couch_epi_module_keeper:maybe_start_keeper(couch_epi_data_gen, Handle).
+hash_of_file(FilePath) ->
+    {ok, Data} = file:read_file(FilePath),
+    couch_epi_util:md5(Data).
diff --git a/src/couch_epi_data_gen.erl b/src/couch_epi_data_gen.erl
index d7e0c65..16a5986 100644
--- a/src/couch_epi_data_gen.erl
+++ b/src/couch_epi_data_gen.erl
@@ -18,21 +18,13 @@
 %% To get an idea about he code of the generated module see preamble()
 
 -export([get_handle/1]).
--export([set/3, get/1, get/2, get/3]).
+-export([get/1, get/2, get/3]).
+-export([generate/2]).
 -export([by_key/1, by_key/2]).
 -export([by_source/1, by_source/2]).
 -export([keys/1, subscribers/1]).
 
--export([save/3]).
--export([current_data/2]).
-
-set(Handle, Source, Data) ->
-    case is_updated(Handle, Source, Data) of
-        false ->
-            ok;
-        true ->
-            couch_epi_module_keeper:save(Handle, Source, Data)
-    end.
+-export([get_current_definitions/1]).
 
 get(Handle) ->
     Handle:all().
@@ -173,36 +165,8 @@
 module_name({Service, Key}) when is_list(Service) andalso is_list(Key) ->
     list_to_atom(string:join([atom_to_list(?MODULE), Service, Key], "_")).
 
-is_updated(Handle, Source, Data) ->
-    Sig = couch_epi_util:hash(Data),
-    if_exists(Handle, version, 1, true, fun() ->
-        try Handle:version(Source) of
-            {error, {unknown, Source}} -> true;
-            {error, Reason} -> throw(Reason);
-            Sig -> false;
-            _ -> true
-        catch
-            Class:Reason ->
-                throw({Class, {Source, Reason}})
-        end
-    end).
 
-save(Handle, undefined, []) ->
-    case current_data(Handle) of
-        [] -> generate(Handle, []);
-        _Else -> ok
-    end;
-save(Handle, Source, Data) ->
-    CurrentData = current_data(Handle),
-    NewDefs = lists:keystore(Source, 1, CurrentData, {Source, Data}),
-    generate(Handle, NewDefs).
-
-current_data(Handle, Subscriber) ->
-    if_exists(Handle, by_source, 1, [], fun() ->
-        Handle:by_source(Subscriber)
-    end).
-
-current_data(Handle) ->
+get_current_definitions(Handle) ->
     if_exists(Handle, by_source, 0, [], fun() ->
         Handle:by_source()
     end).
@@ -237,76 +201,66 @@
 -include_lib("eunit/include/eunit.hrl").
 
 basic_test() ->
-    try
-        Module = foo_bar_baz_bugz,
+    Module = foo_bar_baz_bugz,
 
-        meck:new(couch_epi_module_keeper, [passthrough]),
-        meck:expect(couch_epi_module_keeper, save, fun
-            (Handle, Source, Modules) -> save(Handle, Source, Modules)
-        end),
+    Data1 = [some_nice_data],
+    Data2 = "other data",
+    Data3 = {"even more data"},
+    Defs1 = [{foo, Data1}],
+    Defs2 = lists:usort([{foo, Data2}, {bar, Data3}]),
 
-        Data1 = [some_nice_data],
-        Data2 = "other data",
-        Data3 = {"even more data"},
-        Defs1 = [{foo, Data1}],
-        Defs2 = lists:usort([{foo, Data2}, {bar, Data3}]),
+    Defs = [{app1, Defs1}, {app2, Defs2}],
+    generate(Module, Defs),
 
-        set(Module, app1, Defs1),
-        set(Module, app2, Defs2),
+    ?assertEqual([bar, foo], lists:usort(Module:keys())),
+    ?assertEqual([app1, app2], lists:usort(Module:subscribers())),
 
-        ?assertEqual([bar, foo], lists:usort(Module:keys())),
-        ?assertEqual([app1, app2], lists:usort(Module:subscribers())),
+    ?assertEqual(Data1, Module:get(app1, foo)),
+    ?assertEqual(Data2, Module:get(app2, foo)),
+    ?assertEqual(Data3, Module:get(app2, bar)),
 
-        ?assertEqual(Data1, Module:get(app1, foo)),
-        ?assertEqual(Data2, Module:get(app2, foo)),
-        ?assertEqual(Data3, Module:get(app2, bar)),
+    ?assertEqual(undefined, Module:get(bad, key)),
+    ?assertEqual(undefined, Module:get(source, bad)),
 
-        ?assertEqual(undefined, Module:get(bad, key)),
-        ?assertEqual(undefined, Module:get(source, bad)),
+    ?assertEqual("3KZ4EG4WBF4J683W8GSDDPYR3", Module:version(app1)),
+    ?assertEqual("4EFUU47W9XDNMV9RMZSSJQU3Y", Module:version(app2)),
 
-        ?assertEqual("3KZ4EG4WBF4J683W8GSDDPYR3", Module:version(app1)),
-        ?assertEqual("4EFUU47W9XDNMV9RMZSSJQU3Y", Module:version(app2)),
+    ?assertEqual({error,{unknown,bad}}, Module:version(bad)),
 
-        ?assertEqual({error,{unknown,bad}}, Module:version(bad)),
+    ?assertEqual(
+        [{app1,"3KZ4EG4WBF4J683W8GSDDPYR3"},
+         {app2,"4EFUU47W9XDNMV9RMZSSJQU3Y"}], lists:usort(Module:version())),
 
-        ?assertEqual(
-            [{app1,"3KZ4EG4WBF4J683W8GSDDPYR3"},
-             {app2,"4EFUU47W9XDNMV9RMZSSJQU3Y"}], lists:usort(Module:version())),
+    ?assertEqual(
+        [{app1,[some_nice_data]},{app2,"other data"}],
+        lists:usort(Module:by_key(foo))),
 
-        ?assertEqual(
-           [{app1,[some_nice_data]},{app2,"other data"}],
-           lists:usort(Module:by_key(foo))),
+    ?assertEqual([], lists:usort(Module:by_key(bad))),
 
-        ?assertEqual([], lists:usort(Module:by_key(bad))),
-
-        ?assertEqual(
-            [
-                {bar, [{app2, {"even more data"}}]},
-                {foo, [{app2, "other data"}, {app1, [some_nice_data]}]}
-            ],
-            lists:usort(Module:by_key())),
+    ?assertEqual(
+        [
+            {bar, [{app2, {"even more data"}}]},
+            {foo, [{app2, "other data"}, {app1, [some_nice_data]}]}
+        ],
+        lists:usort(Module:by_key())),
 
 
-        ?assertEqual(Defs1, lists:usort(Module:by_source(app1))),
-        ?assertEqual(Defs2, lists:usort(Module:by_source(app2))),
+    ?assertEqual(Defs1, lists:usort(Module:by_source(app1))),
+    ?assertEqual(Defs2, lists:usort(Module:by_source(app2))),
 
-        ?assertEqual([], lists:usort(Module:by_source(bad))),
+    ?assertEqual([], lists:usort(Module:by_source(bad))),
 
-        ?assertEqual(
-            [
-                {app1, [{foo, [some_nice_data]}]},
-                {app2, [{foo, "other data"}, {bar, {"even more data"}}]}
-            ],
-            lists:usort(Module:by_source())),
+    ?assertEqual(
+        [
+            {app1, [{foo, [some_nice_data]}]},
+            {app2, [{foo, "other data"}, {bar, {"even more data"}}]}
+        ],
+        lists:usort(Module:by_source())),
 
-        ?assertEqual(
-            lists:usort([Data1, Data2, Data3]), lists:usort(Module:all())),
-        ?assertEqual(lists:usort([Data1, Data2]), lists:usort(Module:all(foo))),
-        ?assertEqual([], lists:usort(Module:all(bad))),
-        ok
-    after
-        meck:unload(couch_epi_module_keeper)
-    end,
+    ?assertEqual(
+       lists:usort([Data1, Data2, Data3]), lists:usort(Module:all())),
+    ?assertEqual(lists:usort([Data1, Data2]), lists:usort(Module:all(foo))),
+    ?assertEqual([], lists:usort(Module:all(bad))),
     ok.
 
 -endif.
diff --git a/src/couch_epi_data_source.erl b/src/couch_epi_data_source.erl
deleted file mode 100644
index ef83b14..0000000
--- a/src/couch_epi_data_source.erl
+++ /dev/null
@@ -1,194 +0,0 @@
-% Licensed under the Apache License, Version 2.0 (the "License"); you may not
-% use this file except in compliance with the License. You may obtain a copy of
-% the License at
-%
-% http://www.apache.org/licenses/LICENSE-2.0
-%
-% Unless required by applicable law or agreed to in writing, software
-% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-% License for the specific language governing permissions and limitations under
-% the License.
-
--module(couch_epi_data_source).
-
--behaviour(gen_server).
--define(MONITOR_INTERVAL, 5000).
-
-%% ------------------------------------------------------------------
-%% API Function Exports
-%% ------------------------------------------------------------------
-
--export([childspec/5]).
--export([start_link/4, reload/1]).
--export([wait/1, stop/1]).
-
-%% ------------------------------------------------------------------
-%% gen_server Function Exports
-%% ------------------------------------------------------------------
-
--export([init/1, handle_call/3, handle_cast/2, handle_info/2,
-         terminate/2, code_change/3]).
-
--record(state, {
-    subscriber, locator, key, hash, handle,
-    initialized = false, pending = []}).
-
-%% ------------------------------------------------------------------
-%% API Function Definitions
-%% ------------------------------------------------------------------
-
-childspec(Id, App, EpiKey, Locator, Options) ->
-    {
-        Id,
-        {?MODULE, start_link, [
-            App,
-            EpiKey,
-            Locator,
-            Options
-        ]},
-        permanent,
-        5000,
-        worker,
-        [?MODULE]
-    }.
-
-start_link(SubscriberApp, {epi_key, Key}, Src, Options) ->
-    maybe_start_keeper(Key),
-    {ok, Locator} = locate(SubscriberApp, Src),
-    gen_server:start_link(?MODULE, [SubscriberApp, Locator, Key, Options], []).
-
-reload(Server) ->
-    gen_server:call(Server, reload).
-
-wait(Server) ->
-    gen_server:call(Server, wait).
-
-stop(Server) ->
-    catch gen_server:call(Server, stop).
-
-%% ------------------------------------------------------------------
-%% gen_server Function Definitions
-%% ------------------------------------------------------------------
-
-init([Subscriber, Locator, Key, Options]) ->
-    gen_server:cast(self(), init),
-    Interval = proplists:get_value(interval, Options, ?MONITOR_INTERVAL),
-    {ok, _Timer} = timer:send_interval(Interval, self(), tick),
-    {ok, #state{
-        subscriber = Subscriber,
-        locator = Locator,
-        key = Key,
-        handle = couch_epi_data_gen:get_handle(Key)}}.
-
-handle_call(wait, _From, #state{initialized = true} = State) ->
-    {reply, ok, State};
-handle_call(wait, From, #state{pending = Pending} = State) ->
-    {noreply, State#state{pending = [From | Pending]}};
-handle_call(reload, _From, State) ->
-    {Res, NewState} = reload_if_updated(State),
-    {reply, Res, NewState};
-handle_call(stop, _From, State) ->
-    {stop, normal, State};
-handle_call(_Request, _From, State) ->
-    {reply, ok, State}.
-
-handle_cast(init, #state{pending = Pending} = State) ->
-    {_, NewState} = reload_if_updated(State),
-    [gen_server:reply(Client, ok) || Client <- Pending],
-    {noreply, NewState#state{initialized = true, pending = []}};
-handle_cast(_Msg, State) ->
-    {noreply, State}.
-
-handle_info(tick, State0) ->
-    {_Res, State1} = reload_if_updated(State0),
-    {noreply, State1};
-handle_info(_Info, State) ->
-    {noreply, State}.
-
-terminate(_Reason, _State) ->
-    ok.
-
-code_change(_OldVsn, State, _Extra) ->
-    {ok, State}.
-
-%% ------------------------------------------------------------------
-%% Internal Function Definitions
-%% ------------------------------------------------------------------
-
-locate(App, {priv_file, FileName}) ->
-    case priv_path(App, FileName) of
-        {ok, FilePath} ->
-            ok = ensure_exists(FilePath),
-            {ok, {file, FilePath}};
-        Else ->
-            Else
-    end;
-locate(_App, {file, FilePath}) ->
-    ok = ensure_exists(FilePath),
-    {ok, {file, FilePath}}.
-
-priv_path(AppName, FileName) ->
-    case code:priv_dir(AppName) of
-        {error, _Error} = Error ->
-            Error;
-        Dir ->
-            {ok, filename:join(Dir, FileName)}
-    end.
-
-ensure_exists(FilePath) ->
-    case filelib:is_regular(FilePath) of
-        true ->
-            ok;
-        false ->
-            {error, {notfound, FilePath}}
-    end.
-
-reload_if_updated(#state{handle = Module} = State) ->
-    case couch_epi_util:module_exists(Module) of
-        true ->
-            do_reload_if_updated(State);
-        false ->
-            {ok, State}
-    end.
-
-do_reload_if_updated(#state{hash = OldHash, locator = Locator} = State) ->
-    case read(Locator) of
-        {ok, OldHash, _Data} ->
-            {ok, State};
-        {ok, Hash, Data} ->
-            safe_set(Hash, Data, State);
-        Else ->
-            {Else, State}
-    end.
-
-safe_set(Hash, Data, #state{} = State) ->
-    #state{
-        handle = Handle,
-        subscriber = Subscriber,
-        key = Key} = State,
-
-    try
-        OldData = couch_epi_data_gen:current_data(Handle, Subscriber),
-        ok = couch_epi_data_gen:set(Handle, Subscriber, Data),
-        couch_epi_server:notify(Subscriber, Key, {data, OldData}, {data, Data}),
-        {ok, State#state{hash = Hash}}
-    catch Class:Reason ->
-        {{Class, Reason}, State}
-    end.
-
-read({file, FilePath}) ->
-    case file:consult(FilePath) of
-        {ok, Data} ->
-            {ok, hash_of_file(FilePath), Data};
-        {error, Reason} ->
-            {error, {FilePath, Reason}}
-    end.
-
-hash_of_file(FilePath) ->
-    {ok, Data} = file:read_file(FilePath),
-    couch_epi_util:md5(Data).
-
-maybe_start_keeper(Key) ->
-    Handle = couch_epi_data_gen:get_handle(Key),
-    couch_epi_module_keeper:maybe_start_keeper(couch_epi_data_gen, Handle).
diff --git a/src/couch_epi_functions.erl b/src/couch_epi_functions.erl
index 33c4f91..34d1a06 100644
--- a/src/couch_epi_functions.erl
+++ b/src/couch_epi_functions.erl
@@ -12,152 +12,37 @@
 
 -module(couch_epi_functions).
 
--behaviour(gen_server).
+-include("couch_epi.hrl").
 
 %% ------------------------------------------------------------------
 %% API Function Exports
 %% ------------------------------------------------------------------
 
--export([childspec/4]).
--export([start_link/4, reload/1]).
--export([wait/1, stop/1]).
-
-%% ------------------------------------------------------------------
-%% gen_server Function Exports
-%% ------------------------------------------------------------------
-
--export([init/1, handle_call/3, handle_cast/2, handle_info/2,
-         terminate/2, code_change/3]).
-
--record(state, {
-    provider, service_id, modules, hash, handle,
-    initialized = false, pending = []}).
+-export([interval/1, data/1]).
 
 %% ------------------------------------------------------------------
 %% API Function Definitions
 %% ------------------------------------------------------------------
 
-childspec(Id, App, Key, Module) ->
-    {
-        Id,
-        {?MODULE, start_link, [
-            App,
-            {epi_key, Key},
-            {modules, [Module]},
-            []
-        ]},
-        permanent,
-        5000,
-        worker,
-        [Module]
-    }.
+interval(_) ->
+    undefined.
 
-start_link(ProviderApp, {epi_key, ServiceId}, {modules, Modules}, Options) ->
-    maybe_start_keeper(ServiceId),
-    gen_server:start_link(
-        ?MODULE, [ProviderApp, ServiceId, Modules, Options], []).
-
-reload(Server) ->
-    gen_server:call(Server, reload).
-
-wait(Server) ->
-    gen_server:call(Server, wait).
-
-stop(Server) ->
-    catch gen_server:call(Server, stop).
-
-%% ------------------------------------------------------------------
-%% gen_server Function Definitions
-%% ------------------------------------------------------------------
-
-init([Provider, ServiceId, Modules, _Options]) ->
-    gen_server:cast(self(), init),
-    {ok, #state{
-        provider = Provider,
-        modules = Modules,
-        service_id = ServiceId,
-        handle = couch_epi_functions_gen:get_handle(ServiceId)}}.
-
-handle_call(wait, _From, #state{initialized = true} = State) ->
-    {reply, ok, State};
-handle_call(wait, From, #state{pending = Pending} = State) ->
-    {noreply, State#state{pending = [From | Pending]}};
-handle_call(reload, _From, State) ->
-    {Res, NewState} = reload_if_updated(State),
-    {reply, Res, NewState};
-handle_call(stop, _From, State) ->
-    {stop, normal, State};
-handle_call(_Request, _From, State) ->
-    {reply, ok, State}.
-
-handle_cast(init, #state{pending = Pending} = State) ->
-    {_, NewState} = reload_if_updated(State),
-    [gen_server:reply(Client, ok) || Client <- Pending],
-    {noreply, NewState#state{initialized = true, pending = []}};
-handle_cast(_Msg, State) ->
-    {noreply, State}.
-
-handle_info(_Info, State) ->
-    {noreply, State}.
-
-terminate(_Reason, State) ->
-    safe_remove(State),
-    ok.
-
-code_change(_OldVsn, State, _Extra) ->
-    {_, NewState} = reload_if_updated(State),
-    {ok, NewState}.
+data(Specs) ->
+    Defs = [{A, definitions(M)} || {A, #couch_epi_spec{value = M}} <- Specs],
+    Modules = lists:flatten([M || {_App, #couch_epi_spec{value = M}} <- Specs]),
+    {ok, couch_epi_functions_gen:hash(Modules), group(Defs)}.
 
 %% ------------------------------------------------------------------
 %% Internal Function Definitions
 %% ------------------------------------------------------------------
 
-reload_if_updated(#state{handle = Module} = State) ->
-    case couch_epi_util:module_exists(Module) of
-        true ->
-            do_reload_if_updated(State);
-        false ->
-            {ok, State}
-    end.
+definitions(Module) when is_atom(Module) ->
+    definitions([Module]);
+definitions(Modules) ->
+    Blacklist = [{module_info, 0}, {module_info, 1}],
+    [{M, M:module_info(exports) -- Blacklist} || M <- Modules].
 
-do_reload_if_updated(#state{hash = OldHash, modules = Modules} = State) ->
-    case couch_epi_functions_gen:hash(Modules) of
-        OldHash ->
-            {ok, State};
-        Hash ->
-            safe_add(Hash, State)
-    end.
-
-safe_add(Hash, #state{modules = OldModules} = State) ->
-    #state{
-        handle = Handle,
-        provider = Provider,
-        modules = Modules,
-        service_id = ServiceId} = State,
-    try
-        ok = couch_epi_functions_gen:add(Handle, Provider, Modules),
-        couch_epi_server:notify(
-            Provider, ServiceId, {modules, OldModules}, {modules, Modules}),
-        {ok, State#state{hash = Hash}}
-    catch Class:Reason ->
-        {{Class, Reason}, State}
-    end.
-
-safe_remove(#state{} = State) ->
-    #state{
-        handle = Handle,
-        provider = Provider,
-        modules = Modules,
-        service_id = ServiceId} = State,
-    try
-        ok = couch_epi_functions_gen:remove(Handle, Provider, Modules),
-        couch_epi_server:notify(
-            Provider, ServiceId, {modules, Modules}, {modules, []}),
-        {ok, State#state{modules = []}}
-    catch Class:Reason ->
-        {{Class, Reason}, State}
-    end.
-
-maybe_start_keeper(ServiceId) ->
-    Handle = couch_epi_functions_gen:get_handle(ServiceId),
-    couch_epi_module_keeper:maybe_start_keeper(couch_epi_functions_gen, Handle).
+group(KV) ->
+    dict:to_list(lists:foldr(fun({K,V}, D) ->
+        dict:append_list(K, V, D)
+    end, dict:new(), KV)).
diff --git a/src/couch_epi_functions_gen.erl b/src/couch_epi_functions_gen.erl
index be04644..4990b60 100644
--- a/src/couch_epi_functions_gen.erl
+++ b/src/couch_epi_functions_gen.erl
@@ -12,9 +12,18 @@
 
 -module(couch_epi_functions_gen).
 
--export([add/3, remove/3, get_handle/1, hash/1, apply/4, apply/5, modules/3]).
+-export([
+    generate/2,
+    get_current_definitions/1,
+    get_handle/1,
+    hash/1
+]).
 
--export([save/3]).
+-export([
+    apply/4,
+    apply/5,
+    modules/3
+]).
 
 -ifdef(TEST).
 
@@ -24,29 +33,10 @@
 
 -record(opts, {
     ignore_errors = false,
-    ignore_providers = false,
     pipe = false,
     concurrent = false
 }).
 
-add(Handle, Source, Modules) ->
-    case is_updated(Handle, Source, Modules) of
-        false ->
-            ok;
-        true ->
-            couch_epi_module_keeper:save(Handle, Source, Modules)
-    end.
-
-remove(Handle, Source, Modules) ->
-    CurrentDefs = get_current_definitions(Handle),
-    {SourceDefs, Defs} = remove_from_definitions(CurrentDefs, Source),
-
-    NewSourceDefs = lists:filter(fun({M, _}) ->
-        not lists:member(M, Modules)
-    end, SourceDefs),
-
-    generate(Handle, Defs ++ NewSourceDefs).
-
 get_handle(ServiceId) ->
     module_name(atom_to_list(ServiceId)).
 
@@ -61,7 +51,6 @@
     Modules = providers(Handle, Function, length(Args), DispatchOpts),
     dispatch(Handle, Modules, Function, Args, DispatchOpts).
 
-
 %% ------------------------------------------------------------------
 %% Codegeneration routines
 %% ------------------------------------------------------------------
@@ -188,36 +177,6 @@
 module_name(ServiceId) when is_list(ServiceId) ->
     list_to_atom(string:join([atom_to_list(?MODULE), ServiceId], "_")).
 
-is_updated(Handle, Source, Modules) ->
-    Sig = hash(Modules),
-    if_exists(Handle, version, 1, true, fun() ->
-        try Handle:version(Source) of
-            {error, {unknown, Source}} -> true;
-            {error, Reason} -> throw(Reason);
-            Sig -> false;
-            _ -> true
-        catch
-            Class:Reason ->
-                throw({Class, {Source, Reason}})
-        end
-    end).
-
-save(Handle, undefined, []) ->
-    case get_current_definitions(Handle) of
-        [] -> generate(Handle, []);
-        _Else -> ok
-    end;
-save(Handle, Source, Modules) ->
-    CurrentDefs = get_current_definitions(Handle),
-    Definitions = definitions(Source, Modules),
-    NewDefs = lists:keystore(Source, 1, CurrentDefs, Definitions),
-    generate(Handle, NewDefs).
-
-definitions(Source, Modules) ->
-    Blacklist = [{module_info, 0}, {module_info, 1}],
-    SrcDefs = [{M, M:module_info(exports) -- Blacklist} || M <- Modules],
-    {Source, SrcDefs}.
-
 get_current_definitions(Handle) ->
     if_exists(Handle, definitions, 0, [], fun() ->
         Handle:definitions()
@@ -249,10 +208,18 @@
         end
     ),
     Dict = lists:foldl(fun({K, V}, Acc) ->
-        dict:append(K, V, Acc)
+        dict:update(K, fun(Modules) ->
+            append_if_missing(Modules, V)
+        end, [V], Acc)
+
     end, dict:new(), Providers),
     dict:to_list(Dict).
 
+append_if_missing(List, Value) ->
+    case lists:member(Value, List) of
+        true -> List;
+        false -> [Value | List]
+    end.
 
 hash(Modules) ->
     VSNs = [couch_epi_util:module_version(M) || M <- lists:usort(Modules)],
@@ -307,18 +274,10 @@
 providers(Handle, Function, Arity, #opts{}) ->
     Handle:providers(Function, Arity).
 
-remove_from_definitions(Defs, Source) ->
-    case lists:keytake(Source, 1, Defs) of
-        {value, {Source, Value}, Rest} ->
-            {Value, Rest};
-        false ->
-            {[], Defs}
-    end.
-
 -spec modules(Handle :: atom(), Function :: atom(), Arity :: pos_integer()) ->
     list().
 modules(Handle, Function, Arity) ->
-    providers(Handle, Function, Arity, #opts{ignore_providers = true}).
+    providers(Handle, Function, Arity, #opts{}).
 
 %% ------------------------------------------------------------------
 %% Tests
@@ -334,36 +293,36 @@
     [].
 
 basic_test() ->
-    try
-        Module = foo_bar_dispatcher,
-        meck:new(couch_epi_module_keeper, [passthrough]),
-        meck:expect(couch_epi_module_keeper, save, fun
-            (Handle, Source, Modules) -> save(Handle, Source, Modules)
-        end),
+    Module = foo_bar_dispatcher,
+    Defs = [{?MODULE, [{foo, 2}, {bar, 0}]}],
 
-        add(Module, app1, [?MODULE]),
+    generate(Module, [{app1, Defs}, {app2, Defs}]),
 
-        ?assertMatch([?MODULE], modules(Module, foo, 2)),
+    Exports = lists:sort([
+          {callbacks,2},
+          {version,1},
+          {providers,2},
+          {definitions,1},
+          {module_info,0},
+          {version,0},
+          {dispatch,3},
+          {providers,0},
+          {module_info,1},
+          {definitions,0}]),
 
-        ?assert(is_list(Module:version(app1))),
+    ?assertEqual(Exports, lists:sort(Module:module_info(exports))),
+    ?assertEqual([app1, app2], lists:sort(Module:providers())),
 
-        Defs1 = lists:usort(Module:definitions()),
-        ?assertMatch([{app1, [{?MODULE, _}]}], Defs1),
-        [{app1, [{?MODULE, Exports}]}] = Defs1,
-        ?assert(lists:member({bar, 0}, Exports)),
+    ?assertEqual([?MODULE], lists:sort(Module:providers(foo, 2))),
+    ?assertEqual([?MODULE], lists:sort(Module:providers(bar, 0))),
 
-        add(Module, app2, [?MODULE]),
-        Defs2 = lists:usort(Module:definitions()),
-        ?assertMatch([{app1, [{?MODULE, _}]}, {app2, [{?MODULE, _}]}], Defs2),
+    Defs2 = lists:usort(Module:definitions()),
+    ?assertMatch([{app1, [{?MODULE, _}]}, {app2, [{?MODULE, _}]}], Defs2),
 
-        ?assertMatch([{app1, Hash}, {app2, Hash}], Module:version()),
+    ?assertMatch([{app1, Hash}, {app2, Hash}], Module:version()),
 
-        ?assertMatch([], Module:dispatch(?MODULE, bar, [])),
-        ?assertMatch({1, 2}, Module:dispatch(?MODULE, foo, [1, 2])),
-        ok
-    after
-        meck:unload(couch_epi_module_keeper)
-    end,
+    ?assertMatch([], Module:dispatch(?MODULE, bar, [])),
+    ?assertMatch({1, 2}, Module:dispatch(?MODULE, foo, [1, 2])),
 
     ok.
 
diff --git a/src/couch_epi_keeper_sup.erl b/src/couch_epi_keeper_sup.erl
deleted file mode 100644
index c342d68..0000000
--- a/src/couch_epi_keeper_sup.erl
+++ /dev/null
@@ -1,58 +0,0 @@
-% Licensed under the Apache License, Version 2.0 (the "License"); you may not
-% use this file except in compliance with the License. You may obtain a copy of
-% the License at
-%
-% http://www.apache.org/licenses/LICENSE-2.0
-%
-% Unless required by applicable law or agreed to in writing, software
-% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-% License for the specific language governing permissions and limitations under
-% the License.
-
--module(couch_epi_keeper_sup).
-
--behaviour(supervisor).
-
-%% ------------------------------------------------------------------
-%% API Function Exports
-%% ------------------------------------------------------------------
-
--export([start_link/0]).
-
--export([start_child/2, terminate_child/1]).
-
-%% ------------------------------------------------------------------
-%% supervisor Function Exports
-%% ------------------------------------------------------------------
-
--export([init/1]).
-
-%% Helper macro for declaring children of supervisor
--define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}).
-
-%% ===================================================================
-%% API functions
-%% ===================================================================
-
-start_link() ->
-    supervisor:start_link({local, ?MODULE}, ?MODULE, []).
-
-start_child(Codegen, Module) ->
-    supervisor:start_child(?MODULE, [Codegen, Module]).
-
-terminate_child(undefined) -> ok;
-terminate_child(Child) when is_atom(Child) ->
-    terminate_child(whereis(Child));
-terminate_child(ChildPid) ->
-    supervisor:terminate_child(?MODULE, ChildPid).
-
-%% ===================================================================
-%% Supervisor callbacks
-%% ===================================================================
-
-init([]) ->
-    Children = [
-        ?CHILD(couch_epi_module_keeper, worker)
-    ],
-    {ok, { {simple_one_for_one, 5, 10}, Children} }.
diff --git a/src/couch_epi_module_keeper.erl b/src/couch_epi_module_keeper.erl
index 2d8c439..36376fe 100644
--- a/src/couch_epi_module_keeper.erl
+++ b/src/couch_epi_module_keeper.erl
@@ -12,17 +12,16 @@
 
 -module(couch_epi_module_keeper).
 
+
 -behaviour(gen_server).
 
 %% ------------------------------------------------------------------
 %% API Function Exports
 %% ------------------------------------------------------------------
 
--export([maybe_start_keeper/2]).
--export([register_service/2]).
+-export([start_link/3, stop/1]).
+-export([reload/1]).
 
--export([start_link/2, save/3]).
--export([stop/1]).
 
 %% ------------------------------------------------------------------
 %% gen_server Function Exports
@@ -31,62 +30,132 @@
 -export([init/1, handle_call/3, handle_cast/2, handle_info/2,
          terminate/2, code_change/3]).
 
--record(state, {codegen, module}).
+-record(state, {
+    codegen, module, key, type, handle, hash, kind,
+    timer = {undefined, undefined}}).
 
 %% ------------------------------------------------------------------
 %% API Function Definitions
 %% ------------------------------------------------------------------
 
-register_service(Codegen, Module) ->
-    {ok, Server} = maybe_start_keeper(Codegen, Module),
-    compile_dummy_module(Server).
-
-maybe_start_keeper(Codegen, Module) ->
-    case couch_epi_keeper_sup:start_child(Codegen, Module) of
-        {ok, Pid} ->
-            {ok, Pid};
-        {error, {already_started, Pid}} ->
-            {ok, Pid}
-    end.
-
-start_link(Codegen, Module) ->
-    gen_server:start_link({local, Module}, ?MODULE, [Codegen, Module], []).
+start_link(Type, Key, Codegen) ->
+    Handle = Codegen:get_handle(Key),
+    gen_server:start_link(
+        {local, Handle}, ?MODULE, [Type, Codegen, Key, Handle], []).
 
 stop(Server) ->
     catch gen_server:call(Server, stop).
 
-save(Server, Source, Config) ->
-    gen_server:call(Server, {save, Source, Config}).
+reload(Server) ->
+    gen_server:call(Server, reload).
 
 %% ------------------------------------------------------------------
 %% gen_server Function Definitions
 %% ------------------------------------------------------------------
 
-init([Codegen, Module]) ->
-    {ok, #state{codegen = Codegen, module = Module}}.
+init([Kind, Codegen, Key, Handle]) ->
+    Type = type(Kind),
+    State = #state{
+        codegen = Codegen,
+        key = Key,
+        type = Type,
+        handle = Handle,
+        kind = Kind
+    },
+    compile_module(State).
 
-handle_call({save, Source, Config}, _From, State) ->
-    #state{codegen = Codegen, module = Module} = State,
-    Reply = Codegen:save(Module, Source, Config),
-    {reply, Reply, State};
+handle_call(reload, _From, State0) ->
+    {Reply, State1} = reload_if_updated(State0),
+    {reply, Reply, State1};
 handle_call(_Request, _From, State) ->
     {reply, ok, State}.
 
 handle_cast(_Msg, State) ->
     {noreply, State}.
 
+handle_info(tick, State0) ->
+    {_Res, State1} = reload_if_updated(State0),
+    {noreply, State1};
 handle_info(_Info, State) ->
     {noreply, State}.
 
 terminate(_Reason, _State) ->
     ok.
 
-code_change(_OldVsn, State, _Extra) ->
-    {ok, State}.
+code_change(_OldVsn, State0, _Extra) ->
+    {_Res, State1} = reload_if_updated(State0),
+    {ok, State1}.
 
 %% ------------------------------------------------------------------
 %% Internal Function Definitions
 %% ------------------------------------------------------------------
 
-compile_dummy_module(Server) ->
-    save(Server, undefined, []).
+type(data_providers) -> couch_epi_data;
+type(providers) -> couch_epi_functions;
+type(services) -> couch_epi_functions.
+
+reload_if_updated(#state{handle = Module} = State) ->
+    case couch_epi_util:module_exists(Module) of
+        true ->
+            do_reload_if_updated(State);
+        false ->
+            {ok, State}
+    end.
+
+compile_module(State) ->
+    do_reload_if_updated(State).
+
+do_reload_if_updated(#state{} = State0) ->
+    #state{
+        hash = OldHash,
+        type = Type,
+        key = Key,
+        kind = Kind
+    } = State0,
+    Defs = couch_epi_plugin:definitions(Kind, Key),
+    case Type:data(Defs) of
+        {ok, OldHash, _Data} ->
+            {ok, State0};
+        {ok, Hash, Data} ->
+            {ok, OldData, State1} = safe_set(Hash, Data, State0),
+            notify(Key, OldData, Data, Defs),
+            State2 = update_interval(Type:interval(Defs), State1),
+            {ok, State2};
+        Else ->
+            {Else, State0}
+    end.
+
+update_interval(undefined, #state{timer = Timer} = State) ->
+    State#state{timer = cancel_timer(Timer)};
+update_interval(Interval, #state{timer = Timer} = State) ->
+    State#state{timer = start_timer(Interval, Timer)}.
+
+start_timer(Interval, {undefined, undefined}) ->
+    {ok, Timer} = timer:send_interval(Interval, self(), tick),
+    {Timer, Interval};
+start_timer(Interval, {Timer, _Interval}) ->
+    start_timer(Interval, cancel_timer(Timer)).
+
+cancel_timer({undefined, undefined}) ->
+    {undefined, undefined};
+cancel_timer({Timer, _Interval}) ->
+    timer:cancel(Timer),
+    {undefined, undefined}.
+
+safe_set(Hash, Data, #state{} = State) ->
+    #state{
+        handle = Handle,
+        codegen = CodeGen
+    } = State,
+    try
+        OldData = CodeGen:get_current_definitions(Handle),
+        ok = CodeGen:generate(Handle, Data),
+        {ok, OldData, State#state{hash = Hash}}
+    catch Class:Reason ->
+        {{Class, Reason}, State}
+    end.
+
+notify(Key, OldData, NewData, Defs) ->
+    Specs = [Spec || {_App, Spec} <- Defs],
+    couch_epi_plugin:notify(Key, OldData, NewData, Specs),
+    ok.
diff --git a/src/couch_epi_plugin.erl b/src/couch_epi_plugin.erl
new file mode 100644
index 0000000..05aa591
--- /dev/null
+++ b/src/couch_epi_plugin.erl
@@ -0,0 +1,345 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_epi_plugin).
+
+-include("couch_epi.hrl").
+
+-export([
+    definitions/1,
+    definitions/2,
+    grouped_definitions/1,
+    plugin_processes/2,
+    codegen/1
+]).
+
+-export([notify/4]).
+
+%% ------------------------------------------------------------------
+%% Types Definitions
+%% ------------------------------------------------------------------
+
+-type kind()
+    :: providers
+        | data_providers
+        | services
+        | data_subscriptions
+    .
+
+-type key()
+    :: {ServiceId :: couch_epi:service_id(), Key :: couch_epi:key()}
+        | couch_epi:service_id().
+
+-callback app() -> couch_epi:app().
+-callback providers() -> [{couch_epi:service_id(), module()}].
+-callback services() -> [{couch_epi:service_id(), module()}].
+-callback data_subscriptions() -> [{couch_epi:service_id(), couch_epi:key()}].
+-callback data_providers() -> [{couch_epi:service_id(), couch_epi:data_spec()}].
+-callback processes() -> [{couch_epi:plugin_id(), [supervisor:child_spec()]}].
+-callback notify(Key :: term(), Old :: term(), New :: term()) -> ok.
+
+%% ------------------------------------------------------------------
+%% API Function Definitions
+%% ------------------------------------------------------------------
+
+definitions(Plugins) ->
+    lists:append([extract_definitions(Plugin) || Plugin <- Plugins]).
+
+plugin_processes(Plugin, Plugins) ->
+    lists:append([
+        Specs || P0 <- Plugins, {P1, Specs} <- P0:processes(), P1 =:= Plugin]).
+
+grouped_definitions(Plugins) ->
+    Defs = lists:append([extract_definitions(Plugin) || Plugin <- Plugins]),
+    group_specs(Defs).
+
+definitions(Kind, Key) ->
+    Plugins = application:get_env(couch_epi, plugins, []),
+    Definitions = definitions(Plugins),
+    Filtered = filter_by_key(Definitions, Kind, Key),
+    case group_specs(Filtered) of
+        [] -> [];
+        [{_, Defs}] -> Defs
+    end.
+
+notify(Key, OldData, NewData, Specs) ->
+    Plugins = lists:usort([Plugin || #couch_epi_spec{behaviour = Plugin} <- Specs]),
+    [notify_plugin(Plugin, Key, OldData, NewData) || Plugin <- Plugins],
+    ok.
+
+%% ------------------------------------------------------------------
+%% Internal Function Definitions
+%% ------------------------------------------------------------------
+
+notify_plugin(Plugin, Key, OldData, NewData) ->
+    App = Plugin:app(),
+    Plugin:notify(Key, app_data(App, OldData), app_data(App, NewData)).
+
+
+app_data(App, Data) ->
+    case lists:keyfind(App, 1, Data) of
+        {App, AppData} -> AppData;
+        false -> []
+    end.
+
+filter_by_key(Definitions, Kind, Key) ->
+    lists:filter(fun(Spec) -> by_key(Spec, Kind, Key) end, Definitions).
+
+by_key(#couch_epi_spec{kind = Kind, key = Key}, Kind, Key) -> true;
+by_key(_, _, _) -> false.
+
+
+extract_definitions(Plugin) ->
+    specs(Plugin, providers)
+        ++ specs(Plugin, data_providers)
+        ++ specs(Plugin, services)
+        ++ specs(Plugin, data_subscriptions).
+
+-spec group_specs(Specs :: [#couch_epi_spec{}]) -> GroupedSpecs when
+    GroupedSpecs ::
+        [{{kind(), key()}, [{couch_epi:app(), #couch_epi_spec{}}]}].
+
+group_specs(Specs) ->
+    group(
+        [{{Kind, Key}, group([{App, Spec}])}
+            || #couch_epi_spec{kind = Kind, key = Key, app = App} = Spec <- Specs]).
+
+
+group(KV) ->
+    dict:to_list(lists:foldr(fun({K,V}, D) ->
+        dict:append_list(K, V, D)
+    end, dict:new(), KV)).
+
+specs(Plugin, Kind) ->
+    [spec(parse(Spec, Kind), Plugin, Kind) || Spec <- Plugin:Kind()].
+
+spec({Key, Value, Options}, Plugin, Kind) ->
+    App = Plugin:app(),
+    #couch_epi_spec{
+        app = App,
+        behaviour = Plugin,
+        kind = Kind,
+        options = Options,
+        key = Key,
+        value = Value,
+        codegen = codegen(Kind),
+        type = type(Kind, Value)
+    }.
+
+parse({Key, Value}, Kind) ->
+    parse({Key, Value, []}, Kind);
+parse({Key, Value, Options}, data_subscriptions) ->
+    {{Key, Value}, undefined, Options};
+parse({_, _, _} = Tuple, _Kind) ->
+    Tuple.
+
+codegen(providers) -> couch_epi_functions_gen;
+codegen(services) -> couch_epi_functions_gen;
+codegen(data_providers) -> couch_epi_data_gen;
+codegen(data_subscriptions) -> couch_epi_data_gen.
+
+type(providers, _) -> couch_epi_functions;
+type(services, _) -> couch_epi_functions;
+type(data_providers, _) -> couch_epi_data;
+type(data_subscriptions, _) -> undefined.
+
+
+%% ------------------------------------------------------------------
+%% Tests
+%% ------------------------------------------------------------------
+
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+
+plugin_module(foo_epi) ->
+    "
+        -compile([export_all]).
+
+        app() -> foo.
+        providers() ->
+            [
+                {chttpd_handlers, foo_provider},
+                {bar_handlers, bar_provider}
+            ].
+
+        services() ->
+            [
+                {foo_handlers, foo_service}
+            ].
+
+        data_providers() ->
+            [
+                {{foo_service, data1}, {file, \"abs_file\"}, [{interval, 5000}]},
+                {{foo_service, data2}, {priv_file, \"priv_file\"}},
+                {{foo_service, data3}, {module, foo_data}}
+            ].
+
+        data_subscriptions() ->
+            [
+                {stats, foo_definitions}
+            ].
+
+        processes() -> [].
+
+        notify(_, _, _) -> ok.
+    ";
+plugin_module(bar_epi) ->
+    "
+        -compile([export_all]).
+
+        app() -> bar.
+        providers() ->
+            [
+                {chttpd_handlers, bar_provider},
+                {bar_handlers, bar_provider}
+            ].
+
+        services() ->
+            [
+                {bar_handlers, bar_service}
+            ].
+
+        data_providers() ->
+            [].
+
+        data_subscriptions() ->
+            [
+                {foo_service, data1}
+            ].
+
+        processes() -> [].
+
+        notify(_, _, _) -> ok.
+    ".
+
+generate_module(Name, Body) ->
+    Tokens = couch_epi_codegen:scan(Body),
+    couch_epi_codegen:generate(Name, Tokens).
+
+generate_modules(Kind, Providers) ->
+    [generate_module(P, Kind(P)) || P <- Providers].
+
+definitions_test() ->
+    Expected = lists:sort([
+        #couch_epi_spec{
+            behaviour = bar_epi,
+            app = bar,
+            kind = providers,
+            options = [],
+            key = bar_handlers,
+            value = bar_provider,
+            codegen = couch_epi_functions_gen,
+            type = couch_epi_functions
+        },
+        #couch_epi_spec{
+            behaviour = bar_epi,
+            app = bar,
+            kind = services,
+            options = [],
+            key = bar_handlers,
+            value = bar_service,
+            codegen = couch_epi_functions_gen,
+            type = couch_epi_functions
+        },
+        #couch_epi_spec{
+            behaviour = bar_epi,
+            app = bar,
+            kind = providers,
+            options = [],
+            key = chttpd_handlers,
+            value = bar_provider,
+            codegen = couch_epi_functions_gen,
+            type = couch_epi_functions
+        },
+        #couch_epi_spec{
+            behaviour = bar_epi,
+            app = bar,
+            kind = data_subscriptions,
+            options = [],
+            key = {foo_service, data1},
+            value = undefined,
+            codegen = couch_epi_data_gen
+        },
+        #couch_epi_spec{
+            behaviour = foo_epi,
+            app = foo,
+            kind = providers,
+            options = [],
+            key = bar_handlers,
+            value = bar_provider,
+            codegen = couch_epi_functions_gen,
+            type = couch_epi_functions
+        },
+        #couch_epi_spec{
+            behaviour = foo_epi,
+            app = foo,
+            kind = providers,
+            options = [],
+            key = chttpd_handlers,
+            value = foo_provider,
+            codegen = couch_epi_functions_gen,
+            type = couch_epi_functions},
+        #couch_epi_spec{
+            behaviour = foo_epi,
+            app = foo,
+            kind = services,
+            options = [],
+            key = foo_handlers,
+            value = foo_service,
+            codegen = couch_epi_functions_gen,
+            type = couch_epi_functions},
+        #couch_epi_spec{
+            behaviour = foo_epi,
+            app = foo,
+            kind = data_providers,
+            options = [{interval, 5000}],
+            key = {foo_service, data1},
+            value = {file,"abs_file"},
+            codegen = couch_epi_data_gen,
+            type = couch_epi_data
+        },
+        #couch_epi_spec{
+            behaviour = foo_epi,
+            app = foo,
+            kind = data_providers,
+            options = [],
+            key = {foo_service, data2},
+            value = {priv_file, "priv_file"},
+            codegen = couch_epi_data_gen,
+            type = couch_epi_data
+        },
+        #couch_epi_spec{
+            behaviour = foo_epi,
+            app = foo,
+            kind = data_providers,
+            options = [],
+            key = {foo_service, data3},
+            value = {module, foo_data},
+            codegen = couch_epi_data_gen,
+            type = couch_epi_data
+        },
+        #couch_epi_spec{
+            behaviour = foo_epi,
+            app = foo,
+            kind = data_subscriptions,
+            options = [],
+            key = {stats, foo_definitions},
+            value = undefined,
+            codegen = couch_epi_data_gen
+        }
+    ]),
+
+    [ok,ok] = generate_modules(fun plugin_module/1, [foo_epi, bar_epi]),
+    Tests = lists:zip(Expected, lists:sort(definitions([foo_epi, bar_epi]))),
+    [?assertEqual(Expect, Result) || {Expect, Result} <- Tests],
+    ok.
+-endif.
diff --git a/src/couch_epi_server.erl b/src/couch_epi_server.erl
deleted file mode 100644
index 19d8eb4..0000000
--- a/src/couch_epi_server.erl
+++ /dev/null
@@ -1,144 +0,0 @@
-% Licensed under the Apache License, Version 2.0 (the "License"); you may not
-% use this file except in compliance with the License. You may obtain a copy of
-% the License at
-%
-% http://www.apache.org/licenses/LICENSE-2.0
-%
-% Unless required by applicable law or agreed to in writing, software
-% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-% License for the specific language governing permissions and limitations under
-% the License.
-
--module(couch_epi_server).
--behaviour(gen_server).
--define(SERVER, ?MODULE).
-
-%% ------------------------------------------------------------------
-%% API Function Exports
-%% ------------------------------------------------------------------
-
--export([start_link/0]).
--export([subscribe/3, subscribe/4, unsubscribe/1, unsubscribe/2]).
--export([notify/4, notify/5]).
-
-%% ------------------------------------------------------------------
-%% gen_server Function Exports
-%% ------------------------------------------------------------------
-
--export([init/1, handle_call/3, handle_cast/2, handle_info/2,
-         terminate/2, code_change/3]).
-
--record(epi_server_state, {subscriptions}).
-
-%% ------------------------------------------------------------------
-%% API Function Definitions
-%% ------------------------------------------------------------------
-
-start_link() ->
-    gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
-
-subscribe(App, Key, MFA) ->
-    subscribe(?SERVER, App, Key, MFA).
-
-subscribe(Server, App, Key, {_M, _F, _A} = MFA) ->
-    gen_server:call(Server, {subscribe, App, Key, MFA}).
-
-unsubscribe(Subscription) ->
-    unsubscribe(?SERVER, Subscription).
-
-unsubscribe(Server, Subscription) ->
-    gen_server:call(Server, {unsubscribe, Subscription}).
-
-notify(App, Key, OldData, Data) ->
-    notify(?SERVER, App, Key, OldData, Data).
-
-notify(Server, App, Key, OldData, Data) ->
-    gen_server:cast(Server, {notify, App, Key, OldData, Data}).
-
-
-%% ------------------------------------------------------------------
-%% gen_server Function Definitions
-%% ------------------------------------------------------------------
-
-init(_Args) ->
-    State = #epi_server_state{subscriptions = dict:new()},
-    {ok, State}.
-
-handle_call({subscribe, App, Key, MFA}, {Pid, _Tag},
-        #epi_server_state{subscriptions = Subscriptions0} = State0) ->
-    {Subscription, Subscriptions1} = add(Pid, Subscriptions0, App, Key, MFA),
-    State1 = State0#epi_server_state{subscriptions = Subscriptions1},
-    {reply, {ok, Subscription}, State1};
-handle_call({unsubscribe, Subscription}, _From,
-        #epi_server_state{subscriptions = Subscriptions0} = State0) ->
-    Subscriptions1 = remove(Subscriptions0, Subscription),
-    State1 = State0#epi_server_state{subscriptions = Subscriptions1},
-    {reply, ok, State1};
-handle_call(_Request, _From, State) ->
-    {stop, normal, State}.
-
-handle_cast({notify, App, Key, OldData, Data},
-        #epi_server_state{subscriptions = Subscriptions} = State) ->
-    Subscribers = subscribers(Subscriptions, App, Key),
-    notify_subscribers(Subscribers, App, Key, OldData, Data),
-    {noreply, State};
-handle_cast(_Msg, State) ->
-    {noreply, State}.
-
-handle_info({'DOWN', MonitorRef, _Type, _Object, _Info},
-        #epi_server_state{subscriptions = Subscriptions0} = State0) ->
-    Subscriptions1 = remove(Subscriptions0, MonitorRef),
-    State1 = State0#epi_server_state{subscriptions = Subscriptions1},
-    {noreply, State1};
-handle_info(_Info, State) ->
-    {noreply, State}.
-
-terminate(_Reason, _State) ->
-    ok.
-
-code_change(_OldVsn, State, _Extra) ->
-    {ok, State}.
-
-%% ------------------------------------------------------------------
-%% Internal Function Definitions
-%% ------------------------------------------------------------------
-
-subscribers(Subscriptions, App, Key) ->
-    case dict:find({App, Key}, Subscriptions) of
-        error ->
-            [];
-        {ok, Subscribers} ->
-            Subscribers
-    end.
-
-add(Pid, Subscriptions, App, Key, MFA) ->
-    Subscription = erlang:monitor(process, Pid),
-    {Subscription, dict:append({App, Key}, {Subscription, MFA}, Subscriptions)}.
-
-remove(Subscriptions, SubscriptionId) ->
-    case find(Subscriptions, SubscriptionId) of
-        {App, Key} ->
-            demonitor(SubscriptionId, [flush]),
-            delete_subscriber(Subscriptions, App, Key, SubscriptionId);
-        _ ->
-            Subscriptions
-    end.
-
-find(Subscriptions, SubscriptionId) ->
-    dict:fold(fun(Key, Subscribers, Acc) ->
-        case [ok || {Id, _MFA} <- Subscribers, Id =:= SubscriptionId] of
-            [_] ->
-                Key;
-            [] ->
-                Acc
-        end
-    end, not_found, Subscriptions).
-
-delete_subscriber(Subscriptions, App, Key, SubscriptionId) ->
-    dict:update({App, Key}, fun(Subscribers) ->
-        [{Id, MFA} || {Id, MFA} <- Subscribers, Id =/= SubscriptionId]
-    end, Subscriptions).
-
-notify_subscribers(Subscribers, App, Key, OldData, Data) ->
-    [M:F(App, Key, OldData, Data, A) || {_Id, {M, F, A}} <- Subscribers].
diff --git a/src/couch_epi_sup.erl b/src/couch_epi_sup.erl
index 3c35d2d..77d81b4 100644
--- a/src/couch_epi_sup.erl
+++ b/src/couch_epi_sup.erl
@@ -12,18 +12,33 @@
 
 -module(couch_epi_sup).
 
+%% ----------------------------------
+%% Important assumption
+%% ====================
+%% Keeper and codechange_monitor childspecs relie on underdocumented feature.
+%% According to supervisor docs:
+%%    ...if the child process is a supervisor, gen_server, or gen_fsm, this
+%%    should be a list with one element [Module].
+%% However it is perfectly fine to have more than one module in the list.
+%% Modules property is used to determine if process is suspendable.
+%% Only suspendable processes are hot code upgraded, others are killed.
+%% The check looks like `lists:member(Module, Modules)`
+%% The assumption is that it is indeed underdocumented fact and not
+%% an implementation detail.
+
 -behaviour(supervisor).
 
+-include("couch_epi.hrl").
+
 %% API
 -export([start_link/0]).
+-export([plugin_childspecs/1]).
 
 %% Supervisor callbacks
 -export([init/1]).
 
 %% Helper macro for declaring children of supervisor
 -define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}).
--define(SUP(I, A),
-        {I, {I, start_link, A}, permanent, infinity, supervisor, [I]}).
 
 %% ===================================================================
 %% API functions
@@ -32,13 +47,173 @@
 start_link() ->
     supervisor:start_link({local, ?MODULE}, ?MODULE, []).
 
+plugin_childspecs(Plugin) ->
+    Plugins = application:get_env(couch_epi, plugins, []),
+    plugin_childspecs(Plugin, Plugins).
+
 %% ===================================================================
 %% Supervisor callbacks
 %% ===================================================================
 
 init([]) ->
-    Children = [
-        ?CHILD(couch_epi_server, worker),
-        ?SUP(couch_epi_keeper_sup, [])
+    {ok, { {one_for_one, 5, 10}, keepers()} }.
+
+%% ------------------------------------------------------------------
+%% Internal Function Definitions
+%% ------------------------------------------------------------------
+
+keepers() ->
+    Plugins = application:get_env(couch_epi, plugins, []),
+    Definitions = couch_epi_plugin:grouped_definitions(Plugins),
+    Children = keeper_childspecs(Definitions),
+    remove_duplicates(Children).
+
+plugin_childspecs(Plugin, Plugins) ->
+    Definitions = couch_epi_plugin:grouped_definitions([Plugin]),
+    ExtraChildren = couch_epi_plugin:plugin_processes(Plugin, Plugins),
+    ExtraChildren ++ childspecs(Definitions).
+
+childspecs(Definitions) ->
+    lists:map(fun({{Kind, Key}, Defs}) ->
+        CodeGen = couch_epi_plugin:codegen(Kind),
+        Handle = CodeGen:get_handle(Key),
+        Modules = lists:append([modules(Spec) || {_App, Spec} <- Defs]),
+        Name = service_name(Key) ++ "|" ++ atom_to_list(Kind),
+        code_monitor(Name, [Handle], [Handle|Modules])
+    end, Definitions).
+
+%% ------------------------------------------------------------------
+%% Helper Function Definitions
+%% ------------------------------------------------------------------
+
+remove_duplicates(Definitions) ->
+    lists:ukeysort(1, Definitions).
+
+keeper_childspecs(Definitions) ->
+    lists:map(fun({{Kind, Key}, _Specs}) ->
+        Name = service_name(Key) ++ "|keeper",
+        CodeGen = couch_epi_plugin:codegen(Kind),
+        Handle = CodeGen:get_handle(Key),
+        keeper(Name, [provider_kind(Kind), Key, CodeGen], [Handle])
+    end, Definitions).
+
+keeper(Name, Args, Modules) ->
+    {"couch_epi|" ++ Name, {couch_epi_module_keeper, start_link,
+        Args}, permanent, 5000, worker, Modules}.
+
+code_monitor(Name, Args, Modules0) ->
+    Modules = [couch_epi_codechange_monitor | Modules0],
+    {"couch_epi_codechange_monitor|" ++ Name,
+        {couch_epi_codechange_monitor, start_link, Args}, permanent, 5000, worker, Modules}.
+
+provider_kind(services) -> providers;
+provider_kind(data_subscriptions) -> data_providers;
+provider_kind(Kind) -> Kind.
+
+service_name({ServiceId, Key}) ->
+    atom_to_list(ServiceId) ++ ":" ++ atom_to_list(Key);
+service_name(ServiceId) ->
+    atom_to_list(ServiceId).
+
+modules(#couch_epi_spec{kind = providers, value = Module}) ->
+    [Module];
+modules(#couch_epi_spec{kind = services, value = Module}) ->
+    [Module];
+modules(#couch_epi_spec{kind = data_providers, value = Value}) ->
+    case Value of
+        {module, Module} -> [Module];
+        _ -> []
+    end;
+modules(#couch_epi_spec{kind = data_subscriptions, behaviour = Module}) ->
+    [Module].
+
+
+%% ------------------------------------------------------------------
+%% Tests
+%% ------------------------------------------------------------------
+
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+
+%% ----
+%% BEGIN couch_epi_plugin behaviour callbacks
+
+-compile([export_all]).
+
+app() -> test_app.
+providers() ->
+    [
+        {my_service, provider1},
+        {my_service, provider2}
+    ].
+
+services() ->
+    [
+        {my_service, ?MODULE}
+    ].
+
+data_providers() ->
+    [
+        {{test_app, descriptions}, {module, ?MODULE}, [{interval, 100}]}
+    ].
+
+data_subscriptions() ->
+    [
+        {test_app, descriptions}
+    ].
+
+processes() ->
+    [
+        {?MODULE, [?CHILD(extra_process, worker)]}
+    ].
+
+notify(_Key, _OldData, _NewData) ->
+    ok.
+
+%% END couch_epi_plugin behaviour callbacks
+%% ----
+
+parse_child_id(Id) when is_atom(Id) ->
+    Id;
+parse_child_id(Id) ->
+    ["couch_epi_codechange_monitor", ServiceName, KindStr] = string:tokens(Id, "|"),
+    Kind = list_to_atom(KindStr),
+    case string:tokens(ServiceName, ":") of
+        [ServiceId, Key] ->
+            {{list_to_atom(ServiceId), list_to_atom(Key)}, Kind};
+        [Key] ->
+            {list_to_atom(Key), Kind}
+    end.
+
+basic_test() ->
+    Expected = lists:sort([
+        {extra_process, [], [extra_process]},
+        {{my_service, providers},
+            [couch_epi_functions_gen_my_service],
+            [couch_epi_codechange_monitor, couch_epi_functions_gen_my_service,
+                provider1, provider2]},
+        {{my_service, services},
+            [couch_epi_functions_gen_my_service],
+            [couch_epi_codechange_monitor, couch_epi_functions_gen_my_service,
+                couch_epi_sup]},
+        {{{test_app, descriptions}, data_subscriptions},
+            [couch_epi_data_gen_test_app_descriptions],
+            [couch_epi_codechange_monitor,
+                couch_epi_data_gen_test_app_descriptions, couch_epi_sup]},
+        {{{test_app, descriptions}, data_providers},
+            [couch_epi_data_gen_test_app_descriptions],
+            [couch_epi_codechange_monitor, couch_epi_data_gen_test_app_descriptions,
+                couch_epi_sup]}
+    ]),
+    Children = lists:sort(plugin_childspecs(?MODULE, [?MODULE])),
+    Results = [
+        {parse_child_id(Id), Args, lists:sort(Modules)}
+            || {Id, {_M, _F, Args}, _, _, _, Modules} <- Children
     ],
-    {ok, { {one_for_one, 5, 10}, Children} }.
+
+    Tests = lists:zip(Expected, Results),
+    [?assertEqual(Expect, Result) || {Expect, Result} <- Tests],
+
+    ok.
+
+-endif.
diff --git a/src/couch_epi_util.erl b/src/couch_epi_util.erl
index c65ea4a..5020fba 100644
--- a/src/couch_epi_util.erl
+++ b/src/couch_epi_util.erl
@@ -12,7 +12,7 @@
 
 -module(couch_epi_util).
 
--export([module_version/1, hash/1, md5/1]).
+-export([module_version/1, hash/1, md5/1, module_exists/1]).
 
 -compile([nowarn_deprecated_function]).
 
@@ -23,7 +23,7 @@
 
 hash(Term) ->
     <<SigInt:128/integer>> = md5(term_to_binary(Term)),
-    io_lib:format("\"~.36B\"",[SigInt]).
+    lists:flatten(io_lib:format("\"~.36B\"",[SigInt])).
 
 md5(Data) ->
     case erlang:function_exported(crypto, hash, 2) of
@@ -32,3 +32,6 @@
 	false ->
 	    crypto:md5(Data)
     end.
+
+module_exists(Module) ->
+    erlang:function_exported(Module, module_info, 0).
diff --git a/test/couch_epi_tests.erl b/test/couch_epi_tests.erl
index a43ade9..fc2daa8 100644
--- a/test/couch_epi_tests.erl
+++ b/test/couch_epi_tests.erl
@@ -17,12 +17,18 @@
 -define(DATA_FILE1, ?ABS_PATH("test/fixtures/app_data1.cfg")).
 -define(DATA_FILE2, ?ABS_PATH("test/fixtures/app_data2.cfg")).
 
--export([notify_cb/5, save/3]).
+-export([notify_cb/4, save/3]).
 
--record(ctx, {file, handle, pid, kv, key}).
+-record(ctx, {file, handle, pid, kv, key, modules = []}).
 
 -define(TIMEOUT, 5000).
 
+-define(temp_atom,
+    fun() ->
+        {A, B, C} = erlang:now(),
+        list_to_atom(lists:flatten(io_lib:format("~p~p~p", [A, B, C])))
+    end).
+
 -define(MODULE1(Name), "
     -export([inc/2, fail/2]).
 
@@ -76,72 +82,142 @@
         ].
 ").
 
-notify_cb(App, Key, OldData, Data, KV) ->
-    save(KV, is_called, {App, Key, OldData, Data}).
+%% ------------------------------------------------------------------
+%% couch_epi_plugin behaviour
+%% ------------------------------------------------------------------
+
+plugin_module([KV, Spec]) ->
+    SpecStr = io_lib:format("~w", [Spec]),
+    KVStr = "'" ++ atom_to_list(KV) ++ "'",
+    "
+        -compile([export_all]).
+
+        app() -> test_app.
+        providers() ->
+            [].
+
+        services() ->
+            [].
+
+        data_providers() ->
+            [
+                {{test_app, descriptions}, " ++ SpecStr ++ ", [{interval, 100}]}
+            ].
+
+        data_subscriptions() ->
+            [
+                {test_app, descriptions}
+            ].
+
+        processes() -> [].
+
+        notify(Key, OldData, Data) ->
+            couch_epi_tests:notify_cb(Key, OldData, Data, " ++ KVStr ++ ").
+    ";
+plugin_module([KV]) ->
+    KVStr = "'" ++ atom_to_list(KV) ++ "'",
+    "
+        -compile([export_all]).
+
+        app() -> test_app.
+        providers() ->
+            [
+                {my_service, provider1},
+                {my_service, provider2}
+            ].
+
+        services() ->
+            [
+                {my_service, provider1}
+            ].
+
+        data_providers() ->
+            [].
+
+        data_subscriptions() ->
+            [].
+
+        processes() -> [].
+
+        notify(Key, OldData, Data) ->
+            couch_epi_tests:notify_cb(Key, OldData, Data, " ++ KVStr ++ ").
+    ".
 
 
-setup(couch_epi_data_source) ->
+notify_cb(Key, OldData, Data, KV) ->
+    save(KV, is_called, {Key, OldData, Data}).
+
+start_epi(Plugins) ->
+    application:load(couch_epi),
+    PluginsModules = lists:map(fun({Module, Body}) ->
+        ok = generate_module(Module, Body),
+        Module
+    end, Plugins),
+    application:set_env(couch_epi, plugins, PluginsModules),
+    application:start(couch_epi).
+
+setup(data_file) ->
     error_logger:tty(false),
 
     Key = {test_app, descriptions},
     File = ?tempfile(),
     {ok, _} = file:copy(?DATA_FILE1, File),
-    application:start(couch_epi),
-    {ok, Pid} = couch_epi_data_source:start_link(
-        test_app, {epi_key, Key}, {file, File}, [{interval, 100}]),
-    ok = couch_epi_data_source:wait(Pid),
-    KV = state_storage(),
-    ok = couch_epi:register_service(Key),
+    KV = start_state_storage(),
+
+    ok = start_epi([{provider_epi, plugin_module([KV, {file, File}])}]),
+
+    Pid = whereis(couch_epi:get_handle(Key)),
+
+
     #ctx{
         file = File,
         key = Key,
         handle = couch_epi:get_handle(Key),
         kv = KV,
         pid = Pid};
-setup(couch_epi_data) ->
+setup(data_module) ->
     error_logger:tty(false),
 
     Key = {test_app, descriptions},
-    application:start(couch_epi),
-    ok = generate_module(provider, ?DATA_MODULE1(provider)),
 
-    {ok, Pid} = couch_epi_data:start_link(
-        test_app, {epi_key, Key}, provider, []),
-    ok = couch_epi_data:wait(Pid),
-    KV = state_storage(),
-    ok = couch_epi:register_service(Key),
+    ok = generate_module(provider, ?DATA_MODULE1(provider)),
+    KV = start_state_storage(),
+
+    ok = start_epi([{provider_epi, plugin_module([KV, {module, provider}])}]),
+
+    Pid = whereis(couch_epi:get_handle(Key)),
+    Handle = couch_epi:get_handle(Key),
+
     #ctx{
         key = Key,
-        handle = couch_epi:get_handle(Key),
+        handle = Handle,
+        modules = [Handle, provider],
         kv = KV,
         pid = Pid};
-setup(couch_epi_functions) ->
+setup(functions) ->
     Key = my_service,
     error_logger:tty(false),
 
-    application:start(couch_epi),
     ok = generate_module(provider1, ?MODULE1(provider1)),
     ok = generate_module(provider2, ?MODULE2(provider2)),
 
-    {ok, Pid} = couch_epi_functions:start_link(
-        test_app, {epi_key, Key}, {modules, [provider1, provider2]},
-        [{interval, 100}]),
-    ok = couch_epi_functions:wait(Pid),
-    KV = state_storage(),
-    ok = couch_epi:register_service(Key),
+    KV = start_state_storage(),
+
+    ok = start_epi([{provider_epi, plugin_module([KV])}]),
+
+    Pid = whereis(couch_epi:get_handle(Key)),
+    Handle = couch_epi:get_handle(Key),
+
     #ctx{
         key = Key,
-        handle = couch_epi:get_handle(Key),
+        handle = Handle,
+        modules = [Handle, provider1, provider2],
         kv = KV,
         pid = Pid};
-setup(_Opts) ->
-    setup(couch_epi_functions).
+setup({options, _Opts}) ->
+    setup(functions).
 
-teardown(Module, #ctx{pid = Pid} = Ctx) when is_atom(Module) ->
-    Module:stop(Pid),
-    teardown(Ctx);
-teardown(_Opts, #ctx{pid = Pid} = Ctx) ->
-    couch_epi_functions:stop(Pid),
+teardown(_Case, #ctx{} = Ctx) ->
     teardown(Ctx).
 
 teardown(#ctx{file = File} = Ctx) when File /= undefined ->
@@ -152,26 +228,25 @@
     application:stop(couch_epi),
     ok.
 
-upgrade_release(Pid, Module) ->
+upgrade_release(Pid, Modules) ->
     sys:suspend(Pid),
-    'ok' = sys:change_code(Pid, Module, 'undefined', []),
+    [ok = sys:change_code(Pid, M, undefined, []) || M <- Modules],
     sys:resume(Pid),
     ok.
 
 epi_config_update_test_() ->
     Funs = [
         fun ensure_notified_when_changed/2,
-        fun ensure_not_notified_when_no_change/2,
-        fun ensure_not_notified_when_unsubscribed/2
+        fun ensure_not_notified_when_no_change/2
     ],
-    Modules= [
-        couch_epi_data,
-        couch_epi_data_source,
-        couch_epi_functions
+    Cases = [
+        data_file,
+        data_module,
+        functions
     ],
     {
         "config update tests",
-        [make_case("Check notifications for: ", Modules, Funs)]
+        [make_case("Check notifications for: ", Cases, Funs)]
     }.
 
 epi_data_source_test_() ->
@@ -184,13 +259,13 @@
         fun check_keys/2,
         fun check_subscribers/2
     ],
-    Modules= [
-        couch_epi_data,
-        couch_epi_data_source
+    Cases = [
+        data_file,
+        data_module
     ],
     {
         "epi data API tests",
-        [make_case("Check query API for: ", Modules, Funs)]
+        [make_case("Check query API for: ", Cases, Funs)]
     }.
 
 
@@ -199,7 +274,7 @@
         "epi dispatch tests",
         {
             foreach,
-            fun() -> setup(couch_epi_functions) end,
+            fun() -> setup(functions) end,
             fun teardown/1,
             [
                 fun check_pipe/1,
@@ -210,27 +285,11 @@
         }
     }.
 
-
-epi_subscription_test_() ->
-    Funs = [
-        fun ensure_unsubscribe_when_caller_die/2
-    ],
-    Modules= [
-        couch_epi_data,
-        couch_epi_data_source,
-        couch_epi_functions
-    ],
-    {
-        "epi subscription tests",
-        [make_case("Check subscription API for: ", Modules, Funs)]
-    }.
-
-
 epi_reload_test_() ->
-    Modules= [
-        couch_epi_data,
-        couch_epi_data_source,
-        couch_epi_functions
+    Cases = [
+        data_file,
+        data_module,
+        functions
     ],
     Funs = [
         fun ensure_reload_if_manually_triggered/2,
@@ -239,30 +298,39 @@
     ],
     {
         "epi reload tests",
-        {
-            foreachx,
-            fun setup/1,
-            fun teardown/2,
-            [{M, Fun} || M <- Modules, Fun <- Funs]
-        }
+        [make_case("Check reload for: ", Cases, Funs)]
     }.
 
-
 apply_options_test_() ->
     Funs = [fun ensure_apply_is_called/2],
-    make_case("Apply with options: ", valid_options_permutations(), Funs).
+    Setups = {options, valid_options_permutations()},
+    {
+        "apply options tests",
+        [make_case("Apply with options: ", Setups, Funs)]
+    }.
 
 
+make_case(Msg, {Tag, P}, Funs) ->
+    Cases = [{Tag, Case} || Case <- P],
+    make_case(Msg, Cases, Funs);
 make_case(Msg, P, Funs) ->
     [{format_case_name(Msg, Case), [
         {
             foreachx, fun setup/1, fun teardown/2,
             [
-                 {Case, Fun} || Fun <- Funs
+                 {Case, make_fun(Fun, 2)} || Fun <- Funs
             ]
         }
     ]} || Case <- P].
 
+make_fun(Fun, Arity) ->
+    {arity, A} = lists:keyfind(arity, 1, erlang:fun_info(Fun)),
+    make_fun(Fun, Arity, A).
+
+make_fun(Fun, A, A) -> Fun;
+make_fun(Fun, 2, 1) -> fun(_, A) -> Fun(A) end;
+make_fun(Fun, 1, 2) -> fun(A) -> Fun(undefined, A) end.
+
 format_case_name(Msg, Case) ->
     lists:flatten(Msg ++ io_lib:format("~p", [Case])).
 
@@ -276,61 +344,54 @@
         [concurrent, ignore_errors]
     ].
 
-ensure_notified_when_changed(couch_epi_functions, #ctx{key = Key} = Ctx) ->
+ensure_notified_when_changed(functions, #ctx{key = Key} = Ctx) ->
     ?_test(begin
         subscribe(Ctx, test_app, Key),
-        update(couch_epi_functions, Ctx),
+        update(functions, Ctx),
         timer:sleep(200),
         Result = get(Ctx, is_called),
-        Expected = {test_app, Key,
-            {modules, [provider1, provider2]},
-            {modules, [provider1, provider2]}},
-        ?assertMatch({ok, Expected}, Result),
+        ExpectedDefs = [
+            {provider1,[{inc,2},{fail,2}]},
+            {provider2,[{inc,2},{fail,2}]}
+        ],
+        ?assertEqual({ok, {Key, ExpectedDefs, ExpectedDefs}}, Result),
         ok
     end);
-ensure_notified_when_changed(Module, #ctx{key = Key} = Ctx) ->
+ensure_notified_when_changed(Case, #ctx{key = Key} = Ctx) ->
     ?_test(begin
         subscribe(Ctx, test_app, Key),
-        update(Module, Ctx),
+        update(Case, Ctx),
         timer:sleep(200),
         ExpectedData = lists:usort([
             {[complex, key, 1], [{type, counter}, {desc, updated_foo}]},
             {[complex, key, 2], [{type, counter}, {desc, bar}]}
         ]),
         Result = get(Ctx, is_called),
-        ?assertMatch({ok, {test_app, Key, {data, _}, {data, _}}}, Result),
-        {ok, {test_app, Key, {data, OldData}, {data, Data}}} = Result,
+        ?assertMatch({ok, {Key, _OldData, _Data}}, Result),
+        {ok, {Key, OldData, Data}} = Result,
         ?assertMatch(ExpectedData, lists:usort(Data)),
         ?assertMatch(
             [{[complex, key, 1], [{type, counter}, {desc, foo}]}],
             lists:usort(OldData))
     end).
 
-ensure_not_notified_when_no_change(_Module, #ctx{key = Key} = Ctx) ->
+ensure_not_notified_when_no_change(_Case, #ctx{key = Key} = Ctx) ->
     ?_test(begin
         subscribe(Ctx, test_app, Key),
         timer:sleep(200),
         ?assertMatch(error, get(Ctx, is_called))
     end).
 
-ensure_not_notified_when_unsubscribed(Module, #ctx{key = Key} = Ctx) ->
-    ?_test(begin
-        SubscriptionId = subscribe(Ctx, test_app, Key),
-        couch_epi:unsubscribe(SubscriptionId),
-        timer:sleep(100),
-        update(Module, Ctx),
-        timer:sleep(200),
-        ?assertMatch(error, get(Ctx, is_called))
-    end).
-
-ensure_apply_is_called(Opts, #ctx{handle = Handle, kv = KV, key = Key} = Ctx) ->
+ensure_apply_is_called({options, Opts}, #ctx{handle = Handle, kv = KV, key = Key} = Ctx) ->
     ?_test(begin
         couch_epi:apply(Handle, Key, inc, [KV, 2], Opts),
         maybe_wait(Opts),
         ?assertMatch({ok, _}, get(Ctx, inc1)),
         ?assertMatch({ok, _}, get(Ctx, inc2)),
         ok
-    end).
+    end);
+ensure_apply_is_called(undefined, #ctx{} = Ctx) ->
+    ensure_apply_is_called({options, []}, Ctx).
 
 check_pipe(#ctx{handle = Handle, kv = KV, key = Key}) ->
     ?_test(begin
@@ -361,42 +422,32 @@
         ok
     end).
 
-ensure_unsubscribe_when_caller_die(_Module, #ctx{key = Key} = Ctx) ->
-    ?_test(begin
-        spawn(fun() ->
-            subscribe(Ctx, test_app, Key)
-        end),
-        timer:sleep(200),
-        ?assertMatch(error, get(Ctx, is_called))
-    end).
-
-
 pipe_state(Ctx) ->
     Trace = [get(Ctx, inc1), get(Ctx, inc2)],
     lists:usort([State || {ok, State} <- Trace]).
 
-check_dump(_Module, #ctx{handle = Handle}) ->
+check_dump(_Case, #ctx{handle = Handle}) ->
     ?_test(begin
         ?assertMatch(
             [[{type, counter}, {desc, foo}]],
             couch_epi:dump(Handle))
     end).
 
-check_get(_Module, #ctx{handle = Handle}) ->
+check_get(_Case, #ctx{handle = Handle}) ->
     ?_test(begin
         ?assertMatch(
             [[{type, counter}, {desc, foo}]],
             couch_epi:get(Handle, [complex,key, 1]))
     end).
 
-check_get_value(_Module, #ctx{handle = Handle}) ->
+check_get_value(_Case, #ctx{handle = Handle}) ->
     ?_test(begin
         ?assertMatch(
             [{type, counter}, {desc, foo}],
             couch_epi:get_value(Handle, test_app, [complex,key, 1]))
     end).
 
-check_by_key(_Module, #ctx{handle = Handle}) ->
+check_by_key(_Case, #ctx{handle = Handle}) ->
     ?_test(begin
         ?assertMatch(
             [{[complex, key, 1],
@@ -407,7 +458,7 @@
             couch_epi:by_key(Handle, [complex, key, 1]))
     end).
 
-check_by_source(_Module, #ctx{handle = Handle}) ->
+check_by_source(_Case, #ctx{handle = Handle}) ->
     ?_test(begin
         ?assertMatch(
             [{test_app,
@@ -418,65 +469,60 @@
             couch_epi:by_source(Handle, test_app))
     end).
 
-check_keys(_Module, #ctx{handle = Handle}) ->
+check_keys(_Case, #ctx{handle = Handle}) ->
     ?_assertMatch([[complex,key,1]], couch_epi:keys(Handle)).
 
-check_subscribers(_Module, #ctx{handle = Handle}) ->
+check_subscribers(_Case, #ctx{handle = Handle}) ->
     ?_assertMatch([test_app], couch_epi:subscribers(Handle)).
 
 
-ensure_reload_if_manually_triggered(Module, #ctx{pid = Pid, key = Key} = Ctx) ->
+ensure_reload_if_manually_triggered(Case, #ctx{pid = Pid, key = Key} = Ctx) ->
     ?_test(begin
         subscribe(Ctx, test_app, Key),
-        update_definitions(Module, Ctx),
-        Module:reload(Pid),
+        update_definitions(Case, Ctx),
+        couch_epi_module_keeper:reload(Pid),
         timer:sleep(50),
-        Result = get(Ctx, is_called),
-        ?assertNotMatch(error, Result)
+        ?assertNotEqual(error, get(Ctx, is_called))
     end).
 
-ensure_reload_if_changed(couch_epi_data_source =  Module,
+ensure_reload_if_changed(data_file =  Case,
         #ctx{key = Key, handle = Handle} = Ctx) ->
     ?_test(begin
         Version = Handle:version(),
         subscribe(Ctx, test_app, Key),
-        update_definitions(Module, Ctx),
+        update_definitions(Case, Ctx),
         timer:sleep(250),
         ?assertNotEqual(Version, Handle:version()),
-        Result = get(Ctx, is_called),
-        ?assertNotMatch(error, Result)
+        ?assertNotEqual(error, get(Ctx, is_called))
     end);
-ensure_reload_if_changed(Module,
+ensure_reload_if_changed(Case,
         #ctx{key = Key, handle = Handle} = Ctx) ->
     ?_test(begin
         Version = Handle:version(),
         subscribe(Ctx, test_app, Key),
-        update(Module, Ctx),
+        update(Case, Ctx),
         ?assertNotEqual(Version, Handle:version()),
         timer:sleep(100), %% Allow some time for notify to be called
-        Result = get(Ctx, is_called),
-        ?assertNotMatch(error, Result)
+        ?assertNotEqual(error, get(Ctx, is_called))
     end).
 
-ensure_no_reload_when_no_change(couch_epi_functions = Module,
-        #ctx{pid = Pid, key = Key, handle = Handle} = Ctx) ->
+ensure_no_reload_when_no_change(functions,
+        #ctx{pid = Pid, key = Key, handle = Handle, modules = Modules} = Ctx) ->
     ?_test(begin
         Version = Handle:version(),
         subscribe(Ctx, test_app, Key),
-        upgrade_release(Pid, Module),
+        upgrade_release(Pid, Modules),
         ?assertEqual(Version, Handle:version()),
-        Result = get(Ctx, is_called),
-        ?assertMatch(error, Result)
+        ?assertEqual(error, get(Ctx, is_called))
     end);
-ensure_no_reload_when_no_change(Module,
+ensure_no_reload_when_no_change(_Case,
         #ctx{key = Key, handle = Handle} = Ctx) ->
     ?_test(begin
         Version = Handle:version(),
         subscribe(Ctx, test_app, Key),
         timer:sleep(450),
         ?assertEqual(Version, Handle:version()),
-        Result = get(Ctx, is_called),
-        ?assertMatch(error, Result)
+        ?assertEqual(error, get(Ctx, is_called))
     end).
 
 
@@ -488,24 +534,21 @@
     Tokens = couch_epi_codegen:scan(Body),
     couch_epi_codegen:generate(Name, Tokens).
 
-update(Module, #ctx{pid = Pid} = Ctx) ->
-    update_definitions(Module, Ctx),
-    upgrade_release(Pid, Module).
+update(Case, #ctx{pid = Pid, modules = Modules} = Ctx) ->
+    update_definitions(Case, Ctx),
+    upgrade_release(Pid, Modules).
 
-update_definitions(couch_epi_data_source, #ctx{file = File}) ->
+update_definitions(data_file, #ctx{file = File}) ->
     {ok, _} = file:copy(?DATA_FILE2, File),
     ok;
-update_definitions(couch_epi_data, #ctx{}) ->
+update_definitions(data_module, #ctx{}) ->
     ok = generate_module(provider, ?DATA_MODULE2(provider));
-update_definitions(couch_epi_functions, #ctx{}) ->
+update_definitions(functions, #ctx{}) ->
     ok = generate_module(provider1, ?MODULE2(provider1)).
 
-
-
-subscribe(#ctx{kv = Kv}, App, Key) ->
-    {ok, Pid} = couch_epi:subscribe(App, Key, ?MODULE, notify_cb, Kv),
+subscribe(#ctx{kv = Kv}, _App, _Key) ->
     call(Kv, empty),
-    Pid.
+    ok.
 
 maybe_wait(Opts) ->
     case lists:member(concurrent, Opts) of
@@ -537,6 +580,12 @@
 reply({Ref, From}, Msg) ->
     From ! {reply, Ref, Msg}.
 
+start_state_storage() ->
+    Pid = state_storage(),
+    Name = ?temp_atom(),
+    register(Name, Pid),
+    Name.
+
 state_storage() ->
     spawn_link(fun() -> state_storage(dict:new()) end).