diff --git a/core/java/src/net/i2p/app/ClientApp.java b/core/java/src/net/i2p/app/ClientApp.java
new file mode 100644
index 0000000000000000000000000000000000000000..8900630bcf532e41b32bb5503b2a5cc085f29dcc
--- /dev/null
+++ b/core/java/src/net/i2p/app/ClientApp.java
@@ -0,0 +1,58 @@
+package net.i2p.app;
+
+import net.i2p.I2PAppContext;
+
+/**
+ *  If a class started via clients.config implements this interface,
+ *  it will be used to manage the client, instead of starting with main()
+ *
+ *  Clients implementing this interface MUST provide the following constructor:
+ *
+ *  public MyClientApp(I2PAppContext context, ClientAppManager listener, String[] args) {...}
+ *
+ *  All parameters are non-null.
+ *  This constructor is for instantiation only.
+ *  Do not take a long time. Do not block. Never start threads or processes in it.
+ *  The ClientAppState of the returned object must be INITIALIZED,
+ *  or else throw something.
+ *  The startup() method will be called next.
+ *
+ *  Never ever hold a static reference to the context or anything derived from it.
+ *
+ *  @since 0.9.4
+ */
+public interface ClientApp {
+
+    /**
+     *  Do not take a long time. Do not block. Start threads here if necessary.
+     *  Client must call ClientAppManager.notify() at least once within this
+     *  method to change the state from INITIALIZED to something else.
+     *  Will not be called multiple times on the same object.
+     */
+    public void startup() throws Throwable;
+
+    /**
+     *  Do not take a long time. Do not block. Use a thread if necessary.
+     *  If previously running, client must call ClientAppManager.notify() at least once within this
+     *  method to change the state to STOPPING or STOPPED.
+     *  May be called multiple times on the same object, in any state.
+     */
+    public void shutdown(String[] args) throws Throwable;
+
+    /**
+     *  The current state of the ClientApp.
+     */
+    public ClientAppState getState();
+
+    /**
+     *  The generic name of the ClientApp, used for registration,
+     *  e.g. "console". Do not translate.
+     */
+    public String getName();
+
+    /**
+     *  The dislplay name of the ClientApp, used in user interfaces.
+     *  The app must translate.
+     */
+    public String getDisplayName();
+}
diff --git a/core/java/src/net/i2p/app/ClientAppManager.java b/core/java/src/net/i2p/app/ClientAppManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..e246254b17fa3fdbc3c2fbf91c81e3108c1b12ea
--- /dev/null
+++ b/core/java/src/net/i2p/app/ClientAppManager.java
@@ -0,0 +1,51 @@
+package net.i2p.app;
+
+/**
+ *  Notify the router of events, and provide methods for
+ *  client apps to find each other.
+ *
+ *  @since 0.9.4
+ */
+public interface ClientAppManager {
+    
+    /**
+     *  Must be called on all state transitions except
+     *  from UNINITIALIZED to INITIALIZED.
+     *
+     *  @param app non-null
+     *  @param state non-null
+     *  @param message may be null
+     *  @param e may be null
+     */
+    public void notify(ClientApp app, ClientAppState state, String message, Exception e);
+    
+    /**
+     *  Register with the manager under the given name,
+     *  so that other clients may find it.
+     *  Only required for apps used by other apps.
+     *
+     *  @param app non-null
+     *  @param name non-null
+     *  @return true if successful, false if duplicate name
+     */
+    public boolean register(ClientApp app);
+    
+    /**
+     *  Unregister with the manager. Name must be the same as that from register().
+     *  Only required for apps used by other apps.
+     *
+     *  @param app non-null
+     *  @param name non-null
+     */
+    public void unregister(ClientApp app);
+    
+    /**
+     *  Get a registered app.
+     *  Only used for apps finding other apps.
+     *
+     *  @param app non-null
+     *  @param name non-null
+     *  @return client app or null
+     */
+    public ClientApp getRegisteredApp(String name);
+}
diff --git a/core/java/src/net/i2p/app/ClientAppState.java b/core/java/src/net/i2p/app/ClientAppState.java
new file mode 100644
index 0000000000000000000000000000000000000000..e4563aabfd10189ad7699a3f20bd1dd9210264bd
--- /dev/null
+++ b/core/java/src/net/i2p/app/ClientAppState.java
@@ -0,0 +1,25 @@
+package net.i2p.app;
+
+/**
+ *  Status of a client application.
+ *  ClientAppManager.notify() must be called on all state transitions except
+ *  from UNINITIALIZED to INITIALIZED.
+ *
+ *  @since 0.9.4
+ */
+public enum ClientAppState {
+    /** initial value */
+    UNINITIALIZED,
+    /** after constructor is complete */
+    INITIALIZED,
+    STARTING,
+    START_FAILED,
+    RUNNING,
+    STOPPING,
+    /** stopped normally */
+    STOPPED,
+    /** stopped abnormally */
+    CRASHED,
+    /** forked as a new process, status unknown from now on */
+    FORKED
+}
diff --git a/core/java/src/net/i2p/app/package.html b/core/java/src/net/i2p/app/package.html
new file mode 100644
index 0000000000000000000000000000000000000000..1be4e8331e53f5d65d201e5c4d331618b3680b88
--- /dev/null
+++ b/core/java/src/net/i2p/app/package.html
@@ -0,0 +1,18 @@
+<html>
+<body>
+<p>
+Interfaces for classes to be started and stopped via clients.config.
+Classes implementing the ClientApp interface will be controlled with
+the that interface instead of being started with main().
+</p>
+<p>
+The benefits for clients using this interface:
+<ul>
+<li>Get the current context via the constructor
+<li>Complete life cycle management by the router
+<li>Avoid the need for static references
+<li>Ability to find other clients without using static references
+</ul>
+</p>
+</body>
+</html>
diff --git a/router/java/src/net/i2p/router/RouterContext.java b/router/java/src/net/i2p/router/RouterContext.java
index 421539ce3fe1e69e42bc380e2a5412065cd87e62..aff11b0527439381e638c2a3a7195b2ec1f560be 100644
--- a/router/java/src/net/i2p/router/RouterContext.java
+++ b/router/java/src/net/i2p/router/RouterContext.java
@@ -18,6 +18,7 @@ import net.i2p.router.networkdb.kademlia.FloodfillNetworkDatabaseFacade;
 import net.i2p.router.peermanager.PeerManagerFacadeImpl;
 import net.i2p.router.peermanager.ProfileManagerImpl;
 import net.i2p.router.peermanager.ProfileOrganizer;
+import net.i2p.router.startup.RouterAppManager;
 import net.i2p.router.transport.CommSystemFacadeImpl;
 import net.i2p.router.transport.FIFOBandwidthLimiter;
 import net.i2p.router.transport.OutboundMessageRegistry;
@@ -58,14 +59,22 @@ public class RouterContext extends I2PAppContext {
     private MessageValidator _messageValidator;
     //private MessageStateMonitor _messageStateMonitor;
     private RouterThrottle _throttle;
+    private RouterAppManager _appManager;
     private final Set<Runnable> _finalShutdownTasks;
     // split up big lock on this to avoid deadlocks
+    private volatile boolean _initialized;
     private final Object _lock1 = new Object(), _lock2 = new Object();
 
     private static final List<RouterContext> _contexts = new CopyOnWriteArrayList();
     
+    /**
+     *  Caller MUST call initAll() after instantiation.
+     */
     public RouterContext(Router router) { this(router, null); }
 
+    /**
+     *  Caller MUST call initAll() after instantiation.
+     */
     public RouterContext(Router router, Properties envProps) { 
         super(filterProps(envProps));
         _router = router;
@@ -141,7 +150,9 @@ public class RouterContext extends I2PAppContext {
     }
 
 
-    public void initAll() {
+    public synchronized void initAll() {
+        if (_initialized)
+            throw new IllegalStateException();
         if (getBooleanProperty("i2p.dummyClientFacade"))
             System.err.println("i2p.dummyClientFacade currently unsupported");
         _clientManagerFacade = new ClientManagerFacadeImpl(this);
@@ -182,6 +193,8 @@ public class RouterContext extends I2PAppContext {
         _messageValidator = new MessageValidator(this);
         _throttle = new RouterThrottleImpl(this);
         //_throttle = new RouterDoSThrottle(this);
+        _appManager = new RouterAppManager(this);
+        _initialized = true;
     }
     
     /**
@@ -495,4 +508,13 @@ public class RouterContext extends I2PAppContext {
     public InternalClientManager internalClientManager() {
         return _clientManagerFacade;
     }
+
+    /**
+     *  The RouterAppManager.
+     *  @return the manager
+     *  @since 0.9.4
+     */
+    public RouterAppManager clientAppManager() {
+        return _appManager;
+    }
 }
diff --git a/router/java/src/net/i2p/router/app/RouterApp.java b/router/java/src/net/i2p/router/app/RouterApp.java
new file mode 100644
index 0000000000000000000000000000000000000000..84cf6856291a53ea41a632b857ebd9764d2201c4
--- /dev/null
+++ b/router/java/src/net/i2p/router/app/RouterApp.java
@@ -0,0 +1,24 @@
+package net.i2p.router.app;
+
+import net.i2p.app.ClientApp;
+
+/**
+ *  If a class started via clients.config implements this interface,
+ *  it will be used to manage the client, instead of starting with main()
+ *
+ *  Clients implementing this interface MUST provide the following constructor:
+ *
+ *  public MyClientApp(RouterContext context, ClientAppManager listener, String[] args) {...}
+ *
+ *  All parameters are non-null.
+ *  This constructor is for instantiation only.
+ *  Do not take a long time. Do not block. Never start threads or processes in it.
+ *  The ClientAppState of the returned object must be INITIALIZED,
+ *  or else throw something.
+ *  The startup() method will be called next.
+ *
+ *  Never ever hold a static reference to the context or anything derived from it.
+ *
+ *  @since 0.9.4
+ */
+public interface RouterApp extends ClientApp {}
diff --git a/router/java/src/net/i2p/router/app/package.html b/router/java/src/net/i2p/router/app/package.html
new file mode 100644
index 0000000000000000000000000000000000000000..e35eea22f6db64f2eaafa8c2ac0736474994b31d
--- /dev/null
+++ b/router/java/src/net/i2p/router/app/package.html
@@ -0,0 +1,18 @@
+<html>
+<body>
+<p>
+Interface for classes to be started and stopped via clients.config.
+Classes implementing the RouterApp interface will be controlled with
+the that interface instead of being started with main().
+</p>
+<p>
+The benefits for clients using this interface:
+<ul>
+<li>Get the current context via the constructor
+<li>Complete life cycle management by the router
+<li>Avoid the need for static references
+<li>Ability to find other clients without using static references
+</ul>
+</p>
+</body>
+</html>
diff --git a/router/java/src/net/i2p/router/startup/LoadClientAppsJob.java b/router/java/src/net/i2p/router/startup/LoadClientAppsJob.java
index 3f3f9d5e6a2008412a648395f1fbf797e4536d16..9f0ce8c8296a9ee937d71567e6e5c1f8cbe52e56 100644
--- a/router/java/src/net/i2p/router/startup/LoadClientAppsJob.java
+++ b/router/java/src/net/i2p/router/startup/LoadClientAppsJob.java
@@ -1,12 +1,17 @@
 package net.i2p.router.startup;
 
+import java.lang.reflect.Constructor;
 import java.lang.reflect.Method;
 import java.util.Arrays;
 import java.util.ArrayList;
 import java.util.List;
 
+import net.i2p.I2PAppContext;
+import net.i2p.app.ClientApp;
+import net.i2p.app.ClientAppManager;
 import net.i2p.router.JobImpl;
 import net.i2p.router.RouterContext;
+import net.i2p.router.app.RouterApp;
 import net.i2p.util.I2PThread;
 import net.i2p.util.Log;
 
@@ -24,6 +29,7 @@ public class LoadClientAppsJob extends JobImpl {
         super(ctx);
         _log = ctx.logManager().getLog(LoadClientAppsJob.class);
     }
+
     public void runJob() {
         synchronized (LoadClientAppsJob.class) {
             if (_loaded) return;
@@ -42,7 +48,7 @@ public class LoadClientAppsJob extends JobImpl {
             String argVal[] = parseArgs(app.args);
             if (app.delay <= 0) {
                 // run this guy now
-                runClient(app.className, app.clientName, argVal, _log);
+                runClient(app.className, app.clientName, argVal, getContext(), _log);
             } else {
                 // wait before firing it up
                 getContext().jobQueue().addJob(new DelayedRunClient(getContext(), app.className, app.clientName, argVal, app.delay));
@@ -73,9 +79,11 @@ public class LoadClientAppsJob extends JobImpl {
             _cl = cl;
             getTiming().setStartAfter(getContext().clock().now() + delay);
         }
+
         public String getName() { return "Delayed client job"; }
+
         public void runJob() {
-            runClient(_className, _clientName, _args, _log, _threadGroup, _cl);
+            runClient(_className, _clientName, _args, getContext(), _log, _threadGroup, _cl);
         }
     }
     
@@ -179,8 +187,8 @@ public class LoadClientAppsJob extends JobImpl {
      *  @param clientName can be null
      *  @param args can be null
      */
-    public static void runClient(String className, String clientName, String args[], Log log) {
-        runClient(className, clientName, args, log, null, null);
+    public static void runClient(String className, String clientName, String args[], RouterContext ctx, Log log) {
+        runClient(className, clientName, args, ctx, log, null, null);
     }
     
     /**
@@ -192,15 +200,15 @@ public class LoadClientAppsJob extends JobImpl {
      *  @param cl can be null
      *  @since 0.7.13
      */
-    public static void runClient(String className, String clientName, String args[], Log log,
+    public static void runClient(String className, String clientName, String args[], RouterContext ctx, Log log,
                                  ThreadGroup threadGroup, ClassLoader cl) {
         if (log.shouldLog(Log.INFO))
             log.info("Loading up the client application " + clientName + ": " + className + " " + Arrays.toString(args));
         I2PThread t;
         if (threadGroup != null)
-            t = new I2PThread(threadGroup, new RunApp(className, clientName, args, log, cl));
+            t = new I2PThread(threadGroup, new RunApp(className, clientName, args, ctx, log, cl));
         else
-            t = new I2PThread(new RunApp(className, clientName, args, log, cl));
+            t = new I2PThread(new RunApp(className, clientName, args, ctx, log, cl));
         if (clientName == null) 
             clientName = className + " client";
         t.setName(clientName);
@@ -214,16 +222,18 @@ public class LoadClientAppsJob extends JobImpl {
         private final String _className;
         private final String _appName;
         private final String _args[];
+        private final RouterContext _ctx;
         private final Log _log;
         private final ClassLoader _cl;
 
-        public RunApp(String className, String appName, String args[], Log log, ClassLoader cl) { 
+        public RunApp(String className, String appName, String args[], RouterContext ctx, Log log, ClassLoader cl) { 
             _className = className; 
             _appName = appName;
             if (args == null)
                 _args = new String[0];
             else
                 _args = args;
+            _ctx = ctx;
             _log = log;
             if (cl == null)
                 _cl = ClassLoader.getSystemClassLoader();
@@ -234,14 +244,53 @@ public class LoadClientAppsJob extends JobImpl {
         public void run() {
             try {
                 Class cls = Class.forName(_className, true, _cl);
-                Method method = cls.getMethod("main", new Class[] { String[].class });
-                method.invoke(cls, new Object[] { _args });
+                if (isRouterApp(cls)) {
+                    Constructor con = cls.getConstructor(RouterContext.class, ClientAppManager.class, String[].class);
+                    RouterAppManager mgr = _ctx.clientAppManager();
+                    Object[] conArgs = new Object[] {_ctx, _ctx.clientAppManager(), _args};
+                    RouterApp app = (RouterApp) con.newInstance(conArgs);
+                    mgr.addAndStart(app);
+                } else if (isClientApp(cls)) {
+                    Constructor con = cls.getConstructor(I2PAppContext.class, ClientAppManager.class, String[].class);
+                    RouterAppManager mgr = _ctx.clientAppManager();
+                    Object[] conArgs = new Object[] {_ctx, _ctx.clientAppManager(), _args};
+                    ClientApp app = (ClientApp) con.newInstance(conArgs);
+                    mgr.addAndStart(app);
+                } else {
+                    Method method = cls.getMethod("main", new Class[] { String[].class });
+                    method.invoke(cls, new Object[] { _args });
+                }
             } catch (Throwable t) {
                 _log.log(Log.CRIT, "Error starting up the client class " + _className, t);
             }
             if (_log.shouldLog(Log.INFO))
                 _log.info("Done running client application " + _appName);
         }
+
+        private static boolean isRouterApp(Class cls) {
+            return isInterface(cls, RouterApp.class);
+        }
+
+        private static boolean isClientApp(Class cls) {
+            return isInterface(cls, ClientApp.class);
+        }
+
+        private static boolean isInterface(Class cls, Class intfc) {
+            try {
+                Class[] intfcs = cls.getInterfaces();
+                for (int i = 0; i < intfcs.length; i++) {
+                    if (intfcs[i] == intfc)
+                        return true;
+                }
+            } catch (Throwable t) {}
+            return false;
+        }
+
+
+
+
+
+
     }
 
     public String getName() { return "Load up any client applications"; }
diff --git a/router/java/src/net/i2p/router/startup/RouterAppManager.java b/router/java/src/net/i2p/router/startup/RouterAppManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..45f27fefbb4d704f2d312198207fcf6a314b40a6
--- /dev/null
+++ b/router/java/src/net/i2p/router/startup/RouterAppManager.java
@@ -0,0 +1,131 @@
+package net.i2p.router.startup;
+
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import net.i2p.app.*;
+import static net.i2p.app.ClientAppState.*;
+import net.i2p.router.RouterContext;
+import net.i2p.util.ConcurrentHashSet;
+import net.i2p.util.Log;
+
+/**
+ *  Notify the router of events, and provide methods for
+ *  client apps to find each other.
+ *
+ *  @since 0.9.4
+ */
+public class RouterAppManager implements ClientAppManager {
+    
+    private final RouterContext _context;
+    private final Log _log;
+    private final Set<ClientApp> _clients;
+    private final ConcurrentHashMap<String, ClientApp> _registered;
+
+    public RouterAppManager(RouterContext ctx) {
+        _context = ctx;
+        _log = ctx.logManager().getLog(RouterAppManager.class);
+        _clients = new ConcurrentHashSet(16);
+        _registered = new ConcurrentHashMap(8);
+    }
+
+    public void addAndStart(ClientApp app) {
+        _clients.add(app);
+        try {
+            app.startup();
+        } catch (Throwable t) {
+            _clients.remove(app);
+            _log.error("Client " + app + " failed to start");
+        }
+    }
+
+    // ClientAppManager methods
+
+    /**
+     *  Must be called on all state transitions except
+     *  from UNINITIALIZED to INITIALIZED.
+     *
+     *  @param app non-null
+     *  @param state non-null
+     *  @param message may be null
+     *  @param e may be null
+     */
+    public void notify(ClientApp app, ClientAppState state, String message, Exception e) {
+        switch(state) {
+          case UNINITIALIZED:
+          case INITIALIZED:
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Client " + app.getDisplayName() + " called notify for" + state);
+            break;
+
+          case STARTING:
+          case RUNNING:
+            if (_log.shouldLog(Log.INFO))
+                _log.info("Client " + app.getDisplayName() + " called notify for" + state);
+            break;
+
+          case FORKED:
+          case STOPPING:
+          case STOPPED:
+            _clients.remove(app);
+            _registered.remove(app.getName(), app);
+            if (message == null)
+                message = "";
+            if (_log.shouldLog(Log.INFO))
+                _log.info("Client " + app.getDisplayName() + " called notify for" + state +
+                          ' ' + message, e);
+            break;
+
+          case CRASHED:
+          case START_FAILED:
+            _clients.remove(app);
+            _registered.remove(app.getName(), app);
+            if (message == null)
+                message = "";
+            _log.log(Log.CRIT, "Client " + app.getDisplayName() + ' ' + state +
+                               ' ' + message, e);
+            break;
+        }
+    }
+    
+    /**
+     *  Register with the manager under the given name,
+     *  so that other clients may find it.
+     *  Only required for apps used by other apps.
+     *
+     *  @param app non-null
+     *  @param name non-null
+     *  @return true if successful, false if duplicate name
+     */
+    public boolean register(ClientApp app) {
+        if (!_clients.contains(app))
+            return false;
+        // TODO if old app in there is not running and != this app, allow replacement
+        return _registered.putIfAbsent(app.getName(), app) == null;
+    }
+    
+    /**
+     *  Unregister with the manager. Name must be the same as that from register().
+     *  Only required for apps used by other apps.
+     *
+     *  @param app non-null
+     *  @param name non-null
+     */
+    public void unregister(ClientApp app) {
+        _registered.remove(app.getName(), app);
+    }
+    
+    /**
+     *  Get a registered app.
+     *  Only used for apps finding other apps.
+     *  Do not hold a static reference.
+     *  If you only need to find a port, use the PortMapper instead.
+     *
+     *  @param app non-null
+     *  @param name non-null
+     *  @return client app or null
+     */
+    public ClientApp getRegisteredApp(String name) {
+        return _registered.get(name);
+    }
+}