Repository: hoodiehq/hoodie
Pull request #711 (2017-03-02 05:51:06)

3rd party plugins
closes https://github.com/hoodiehq/camp/issues/104

### Update

This PR is ready for review, see https://github.com/hoodiehq/hoodie/pull/711#issuecomment-304530166

gr2m: I think the next steps are these

- figure out how to load the client bit of the 3rd party plugin
- figure out to serve the contents of `/hoodie/public` folder (if existent) at `<hoodie url>/hoodie/<plugin name>/*`
- make sure configuration is passed to the plugin (see https://github.com/hoodiehq/hoodie-plugin-example/blob/36c80c1e6dfb02dfbed9ecd27731b83a4a9a88fa/hoodie/server.js#L18)
- write tests and make them pass :)

We can use this pull requests to discuss, let us know if you have any questions :)
labaikie: Thanks! I do have a lot on my plate until Wed next week but I hope to take another look at it over this weekend and let you know if I have any further questions!
labaikie: It looks like the server plugin integration is not yet complete 😞 ... 
In running a local test hoodie app with the hoodie-plugin-example included as a plugin (and after temporarily replacing `options.email.transporter` with the an email smtp string in plugin [here](https://github.com/hoodiehq/hoodie-plugin-example/blob/36c80c1e6dfb02dfbed9ecd27731b83a4a9a88fa/hoodie/server.js#L18)), the test app gets terminated with the below error.

```
Error: Plugin hoodie-email already registered in: http://localhost:8080
<     at Object.exports.assert (../hoodie/node_modules/hapi/node_modules/hoek/lib/index.js:736:11)
<     at each (../hoodie/node_modules/hapi/lib/plugin.js:288:26)
<     at iterate (../hoodie/node_modules/hapi/node_modules/items/lib/index.js:36:13)
<     at done (../hoodie/node_modules/hapi/node_modules/items/lib/index.js:28:25)
<     at Object.module.exports.register (../testapp/hoodie/server.js:6:3)
<     at Object.target [as register] (../hoodie/node_modules/hapi/node_modules/joi/lib/object.js:77:34)
<     at each (../hoodie/node_modules/hapi/lib/plugin.js:318:14)
<     at iterate (../hoodie/node_modules/hapi/node_modules/items/lib/index.js:36:13)
<     at done (../hoodie/node_modules/hapi/node_modules/items/lib/index.js:28:25)
<     at Object.register (../hoodie/server/plugins/public.js:94:10)
```

Would you by any chance have any pointers? Thank you!
gr2m: This looks like an error you get when you try to register a plugin with the same name twice with hapi.

In your test app, in the `/hoodie/server.js` file, do you register a plugin that is called `hoodie-email`, just like the plugin is called in `hoodie-plugin-example` module? That’s probably the problem :) You can rename the plugin name in `/hoodie/server.js` in your testapp, or you can remove the `hoodie/` folder altogether, it was just to show how loading the plugins should work. Now that we load the plugin from  `hoodie-example-plugin/plugin/server` we are good and don’t need it any longer for testing
labaikie: @gr2m hey so I tried removing & changing the name of the module being registered at my testapp's `/hoodie/server.js` but no luck in getting the hoodie server to not terminate. :( I'm trying to debug through the processes but having a hard time - are you by any chance available for some questions?
labaikie: hi @gr2m - so I'm trying to load the plugin's `public` contents and I'm having trouble specifying the plugin paths in `hoodie/server/public.js`. Please see the snippet below and let me know if you would have any pointers. I've tried a few variations, haha.

``
const routes = [{
    method: 'GET',
    path: '/{p*}',
    handler: {
      directory: {
        path: publicFolder,
        listing: false,
        index: true
      }
    }
  }, {
    method: 'GET',
    path: '/hoodie/{p*}',
    handler: {
      directory: {
        path: hoodiePublicPath,
        listing: false,
        index: true
      }
    }
  }, {
    method: 'GET',
    path: '/hoodie/admin/{p*}',
    handler: {
      directory: {
        path: adminPublicPath,
        listing: false,
        index: true
      }
    }
  }, {
    method: 'GET',
    path: '/hoodie/info.json',
    handler: function (request, reply) {
      reply({
        hoodie: true,
        name: options.config.name,
        version: hoodieVersion
      })
    }
  }]

  // add plugin routes
  plugins.forEach(i => routes.push({
    method: 'GET',
    path: `/hoodie/${i}/{p*}`,
    handler: {
      directory: {
        path: pathJoin(require.resolve(`@hoodie/${i}`), 'hoodie', 'public'),
        listing: false,
        index: true,
      }
    }
  }))
``
gr2m: yeah that’s a tricky one: finding the path to a node_modules package. I’m sure there is a package for that :D 

What I usually do is thi

```js
var path = require('path')
var packageName = 'my-npm-package'
var packagePath = path.dirname(require.resolve('hoodie/package.json'))
var pathToPublicFolder = path.join(packagePath, 'hoodie', 'public')
```

Note that you should check if the folder exists before defining the route.

Hope that helps :)
labaikie: @gr2m in your snippet, the variable `packageName` - did you mean to include that in `packagePath` ?
labaikie: @gr2m where do you think is best for the users of hoodie app to specify the plugin configuration? For the example email plugin, the smtp transporter, namely? I was wondering if it would be the app's `package.json`? Thanks!
gr2m: yes I would put the config into `package.json`. We can add more keys to the "hoodie" property, like

```js
{
  ...
  "hoodie": {
    "public": "dist/",
    "plugins": ["my-plugin"],
    "my-config": {...}
   }
}
```

Something like this? We need to discuss this a bit more, but for now that’s good enough. Does that work for ya?
labaikie: @gr2m, thanks so much for all your support. I've pushed up the code to include

a. server plugin load
b. client plugin load
c. public content load
d. option to pass plugin configuration

Would you be able to help me review? Thank you!
gr2m: This is great work, @labaikie 👏 

There are a few things left to do, and I’m happy to help you finish it up or do it for you.

1. We don’t yet use any of the es2015 syntax like `const` and destructing. We will at some point move to it, but have to plan it a bit, so for now it would be good if you would keep it with `var`
2. When loading the example plugin, the route for the public folder (`GET /hoodie/hoodie-plugin-example/*`) seems to override the one that is registered in the plugin (`POST /hoodie/hoodie-plugin-example/api/send`). we probably might need to change order of things to fix that.
3. I would like to introduce a new option `"hoodie": {"name": "my-custom-name"}` so that the example plugin for example could set it to `"email"`, which we then could use for the routes, so we would have `/hoodie/email/*` instead of `/hoodie/hoodie-plugin-example/*`. What do you think?
4. We will need to test it all, but I can definitely help with that :)

I’d like to move this forward, today / tomorrow. Let me know if you want to give it a go yourself
janl: Great progress here!

I started on porting to ES5: https://github.com/hoodiehq/hoodie/pull/745 and while doing so I found one tiny issue mentioned in #745 
gr2m: The current work-in-progress implementation has a configuration problem: all plugin developers can decide to add keys for their configuration to the `"hoodie"` object in `package.json`. If one day we decide to add a new key that hoodie core is using, it might conflict with 3rd party plugins out there.

This is something we should discuss with the wider community in future, but for now @janl and I think the best ways is to put all app/plugin-specific configuration into an extra key, that we suggest to call `"app"`.

So instead of 

```json
{
  ...
  "hoodie": {
    "plugins": ["hoodie-plugin-example"],
    "example": {"foo": "bar"}
  }
}
```

it would now be 

```json
{
  ...
  "hoodie": {
    "plugins": ["hoodie-plugin-example"],
    "app": {"example": {"foo": "bar"}}
  }
}
```

As a reminder: the reason why we don call `"app"` something like `"pluginOptions"` or make `"plugins"` an object where keys are the plugin package names and the values are the options is that we want hoodie plugins to be valid hoodie apps and the other way around :)
gr2m: @fozy81 Hey Tim, I run into a problem with the link checker. The PR is adding a link to the README which links to a part of the docs that is only created with this PR itself, so the link fails. For now I whitelisted docs.hood.ie in `.travis.yml`, but maybe you have a better idea?

Besides that, the link checker fails for URLs like https://pouchdb.com/2016/06/06/introducing-pouchdb-custom-builds.html, which works for me™️, any idea what that could be? https://travis-ci.org/hoodiehq/hoodie/builds/236912318#L1029
gr2m: I don’t get the error "Error: Cannot find module 'pouchdb-browser' from '/tmp/tmp.ifWeTYm9KO' " https://travis-ci.org/hoodiehq/hoodie/builds/236912318#L611 because above it shows that it was installed successfully ... https://travis-ci.org/hoodiehq/hoodie/builds/236912318#L571 ugh
gr2m: @hoodiehq/maintainers this is ready for review 👀 

Ignore the failing CI tests, they are related to docs, the tests pass and coverage is 100%

In order to test the app, create a new app as described in http://docs.hood.ie/en/latest/guides/quickstart.html. Instead of installing from npm, install hoodie from GitHub

```
npm install --save hoodiehq/hoodie#camp/104/3rd-party-plugins
```

Then add an app-specific plugin as described in https://github.com/hoodiehq/hoodie/blob/camp/104/3rd-party-plugins/docs/guides/plugins.rst and then install the [hello world plugin](https://github.com/hoodiehq/hoodie-plugin-hello-world)

When you start the app now, it should load your app specific plugin (if you haven’t changed the example code, you should see "Hello, world!" at `http://localhost:8080/hoodie/<your app name>/api`) and when the hello world plugin works it should show you a form at http://localhost:8080/hoodie/hello-world

fozy81: @gr2m Hi, a quick fix to reference documents within repo is do something like this: <CONTRIBUTING.html> or in this particular case <hapi-plugin.html> 
Here's an example .rst: https://raw.githubusercontent.com/hoodiehq/hoodie/master/docs/developers/CODING_STYLE.rst

I think this should solve the issue.

I'll need to look more deeply into the other failing links...looks like some strange truncation of the url but not sure how this is happening. 

fredguth: @gr2m Worked fine as an app-specific plugin.  But couldn't follow the explanation on how to do the same with a npm module (3rd party plugin).
gr2m: @fredguth 

> But couldn't follow the explanation on how to do the same with a npm module (3rd party plugin).

Can you tell what went wrong? 
janl: @gr2m works nearly, two snags:

1. https://github.com/hoodiehq/hoodie-plugin-hello-world#usage uses a `helloWorld` key in `package.json` `hoodie.app` which seems odd, should that be `hello-world`?

2. regardless of 1:
> you should see "Hello, world!" at http://localhost:8080/hoodie/<your app name>/api)

All variations of this throw a 404: 
- http://localhost:8080/hoodie/testapp/api (as per quickstart instructions and `package.json`
- http://localhost:8080/hoodie/hello-world/api (I would imagine the plugin name is a path prefix, so plugins don’t step over themselves?
- http://localhost:8080/hoodie/helloWorld/api (same as before, but taking 1. into consideration).

All of these show `{"statusCode":404,"error":"Not Found"}`.


* * *

Aside, I tried adding `blipp` to see the actual registered routes, but got intro trouble using the `hapiPlugins` array: https://github.com/hoodiehq/hoodie/pull/711/files#diff-c9bb31bbcc08d333b150cdd7e696ad9eR20 because when the actual `register()` is called, it gets passed the global `options` object which `blipp` doesn’t like:

```
/Users/jan/Work/tmp/testapp/node_modules/hoek/lib/index.js:731
    throw new Error(msgs.join(' ') || 'Unknown error');
    ^

Error: {
  "store" [1]: {
    "hooks": {}
  },
  "client" [2]: {},
  "admin" [3]: {},
  "account" [4]: {
    "admins": {},
    "secret": "b304c2256886b471c9d42f6148302b4d",
    "usersDb": "_users"
  },
  "db" [5]: {},
  "inMemory" [7]: false,
  "app" [8]: {
    "hello-world": {
      "greeting": "Bonjour",
      "name": "le monde"
    }
  },
  "plugins" [9]: [
    "@hoodie/plugin-hello-world"
  ],
  "paths" [10]: {
    "data": ".hoodie",
    "public": "/Users/jan/Work/tmp/testapp/node_modules/hoodie/public"
  },
  "loglevel" [11]: "warn",
  "name" [12]: "testapp"
}

[1] "name" is not allowed
[2] "loglevel" is not allowed
[3] "paths" is not allowed
[4] "plugins" is not allowed
[5] "app" is not allowed
[6] "inMemory" is not allowed
[7] "PouchDB" is not allowed
[8] "db" is not allowed
[9] "account" is not allowed
[10] "admin" is not allowed
[11] "client" is not allowed
[12] "store" is not allowed
    at Object.exports.assert (/Users/jan/Work/tmp/testapp/node_modules/hoek/lib/index.js:731:11)
    at Object.exports.register (/Users/jan/Work/tmp/testapp/node_modules/blipp/lib/index.js:20:10)
    at Object.target [as register] (/Users/jan/Work/tmp/testapp/node_modules/hapi/node_modules/joi/lib/types/object/index.js:77:34)
    at each (/Users/jan/Work/tmp/testapp/node_modules/hapi/lib/plugin.js:310:14)
    at iterate (/Users/jan/Work/tmp/testapp/node_modules/hapi/node_modules/items/lib/index.js:36:13)
    at done (/Users/jan/Work/tmp/testapp/node_modules/hapi/node_modules/items/lib/index.js:28:25)
    at each (/Users/jan/Work/tmp/testapp/node_modules/hapi/lib/plugin.js:261:28)
    at iterate (/Users/jan/Work/tmp/testapp/node_modules/hapi/node_modules/items/lib/index.js:36:13)
    at Object.exports.serial (/Users/jan/Work/tmp/testapp/node_modules/hapi/node_modules/items/lib/index.js:39:9)
    at internals.Plugin.register (/Users/jan/Work/tmp/testapp/node_modules/hapi/lib/plugin.js:313:11)
```

Of course I can register this manually and/or further up, but I’d say we should re-think that section of the code, if it only works with some hapi plugins.
gr2m: I’m not sure how to make sth like `blipp` work to be honest, I’m not sure if it’s enough to just pass in options from say `options.app.<plugin name>`. Maybe one way we could make this work is by having the global options be passed to the `Hapi.Server({app: globalOptions})`, so plugins could still access them via `server.settings.app.*`?
janl: I feel like treating hapi plugins and hoodie plugins the same leads to more trouble than is worth. E.g. if we want to lead inert, and maybe at least for debugging blipp, maybe we should load them in ./server/index.js and not here in the hoodie-plugin loader.
gr2m: @janl did you add `@hoodie/plugin-hello-world` to `hoodie.plugins[]` in your package.json file? Did you `npm link` it? If you did the latter, I just run into this myself and will pushed a fix via eb77206
gr2m: nevermind, I removed the workaround. The right way to work with linked packages like this is to start hoodie with

```
NODE_PATH='/path/my/myhoodieapp' npm start
```

Or simpler: 

```
FUNK=`pwd`/node_modules npm start
```

I’m still not sure about wether to pass only `hoodie.options['<plugin-name>']` to plugins or the entire options. The benefit if we would only pass in `hoodie.options['<plugin-name>']` then one could directly export hapi plugins at `./hoodie/server`, e.g.

```js
module.exports = require('blipp') // can be an array of plugins, too
```

I would not allow to add hapi plugins directly to the hoodie plugins array. Right now we enforce the folder structure by doing `reqire('<plugin-name>/hoodie/server')` so if we would add `blipp` to the array of plugin names it would try to `reqire('blipp/hoodie/server')` and it would fail. I’d say that’s okay. The alternative would be not to enforce the hoodie plugin / app folder structure any longer and I’d like to try to avoid that.

Something I forgot about is passing options to the client plugin code. Right now there is now way to do that. What I would suggest is to extend the `hoodie.plugin` method to pass a 2nd argument which is the options passed to the `@hoodie/client` constructor. That would allow us to do this

```js
// server/client/index.js
module.exports = function (hoodie, options) {
  hoodie.myMethod = function () {
    // do something with options here
  }
}
```

`options` would be what ever we pass as `options.client` to the `hoodie` hapi plugin (which is [currently not yet happening](https://github.com/hoodiehq/hoodie/blob/79dde6b8c4151d803aea0a1e58463d3ad6c06663/server/plugins/client/bundle.js#L85-L92)). 

I guess it makes sense to follow the same convention for client option as we do for server option. Say I’d like to set a default greeting for my app, I’d do this in package.json

```json
{
  "name": "myhoodie",
  ...
  "hoodie": {
    "plugins": [
      "@hoodie/plugin-hello-world"
    ],
    "client": {
      "app": {
        "hello-world": {
          "greeting": "Bonjour"
        }
      }
    }
  }
}
```

An alternative would be to do `hoodie.app.<plugin name>.client` instead of `hoodie.client.app.<plugin name>`

```json
{
  "name": "myhoodie",
  ...
  "hoodie": {
    "plugins": [
      "@hoodie/plugin-hello-world"
    ],
    "app": {
      "hello-world": {
        "client": {
          "greeting": "Bonjour"
        }
      }
    }
  }
}
```

I think I prefer the latter
janl: > @janl did you add @hoodie/plugin-hello-world to hoodie.plugins[] in your package.json file? Did you npm link it?

I added it to `package.json` as documented, and I did not `npm link` it
janl: > I would not allow to add hapi plugins directly to the hoodie plugins array. 

I agree, but that’s not what I meant. I was considering the scenario of adding a hapi plugin to Hoodie core.

Seeing https://github.com/hoodiehq/hoodie/pull/711/files#diff-c9bb31bbcc08d333b150cdd7e696ad9eL24 I’d assume I can just add one there (which is what I tried), but that fails, because hapi plugins validate the options they are passed and the register code just throws in the whole Hoodie options object, which then makes the e.g. blipp registration fail.

`inert` just happens to work here.


gr2m: That should no longer be a problem via https://github.com/hoodiehq/hoodie/pull/711/commits/3c160a9dd586bdf2fec1a6515c20f655c62dbc52. We don’t pass the app options to hapi plugins any longer
janl: > That is no longer the case via 3c160a9

@gr2m sounds good, thanks!

What about my earlier observations (copied from https://github.com/hoodiehq/hoodie/pull/711#issuecomment-305977797)

1. https://github.com/hoodiehq/hoodie-plugin-hello-world#usage uses a `helloWorld` key in `package.json` `hoodie.app` which seems odd, should that be `hello-world`?

2. regardless of 1:
> you should see "Hello, world!" at http://localhost:8080/hoodie/\<your app name\>/api)

All variations of this throw a 404: 
- http://localhost:8080/hoodie/testapp/api (as per quickstart instructions and `package.json`
- http://localhost:8080/hoodie/hello-world/api (I would imagine the plugin name is a path prefix, so plugins don’t step over themselves?
- http://localhost:8080/hoodie/helloWorld/api (same as before, but taking 1. into consideration).

All of these show `{"statusCode":404,"error":"Not Found"}`.


gr2m: > All variations of this throw a 404:

have you seen https://github.com/hoodiehq/hoodie/pull/711#issuecomment-306033050

Can you push your test app somewhere? It all works for me

> uses a helloWorld key in package.json hoodie.app which seems odd, should that be hello-world?

the keys on `hoodie.app` don’t really matter if we just pass the whole options object, and `options.app.helloWorld` is easier to access than `options.app['hello-world']`
janl: > have you seen #711 (comment)

yes: https://github.com/hoodiehq/hoodie/pull/711#issuecomment-306054759
gr2m: let’s pair on this because I’m not able to reproduce the issue you are describing 
gr2m: can you show me the `package.json` file of your app and the contents of `hoodie/server.js` or `hoodie/server/index.js` depending on what you have?
gr2m: regarding https://github.com/hoodiehq/hoodie/pull/711#issuecomment-305977797

> All variations of this throw a 404: 
>
> - http://localhost:8080/hoodie/testapp/api (as per quickstart instructions and `package.json`
> - http://localhost:8080/hoodie/hello-world/api (I would imagine the plugin name is a path prefix, so plugins don’t step over themselves?
> - http://localhost:8080/hoodie/helloWorld/api (same as before, but taking 1. into consideration).

The reason why `http://localhost:8080/hoodie/testapp/api` returned a 404 is that there was no `hoodie/server.js` file in the app. `http://localhost:8080/hoodie/hello-world/api` didn’t work because it should have been without the `/api`. I think when Jan tested it there was a typo in the instructions that has meanwhile been fixed.

So this all seems to work and is ready to be merged, I think. Needs a review accepting it from one of the other maintainers.
gr2m: wheeeee I’m gonna merge this but would not yet publicly announce it. As we people can now technically start making their own plugins we should put some thought into that and give guidance

Patch:
diff --git a/README.md b/README.md
index f53153e0..33881e61 100644
--- a/README.md
+++ b/README.md
@@ -41,150 +41,19 @@ Next install hoodie and save it as dependency
 npm install --save hoodie
 ```
 
-Now simply use `npm start` to start up your Hoodie app!
-
-You can find a more thorough description in our [Getting Started Guide](http://docs.hood.ie/en/latest/guides/quickstart.html).
-
-## Usage
-
-`hoodie` can be used as as CLI (Command Line Interface) or as [hapi plugin](http://hapijs.com/tutorials/plugins).
-The options are slightly different, see below
-
-### CLI
-
-Once you finished the [setup](#setup), you can start your hoodie server with
+Now start up your Hoodie app
 
 ```
 npm start
 ```
 
-To pass CLI options to Hoodie, you have to separate them with `--`, for example:
-
-```
-npm start -- --port=8090 --inMemory
-```
-
-Available CLI options are
-
-option                    | default              | description
-------------------------- | -------------------- | -------------
-`--address`               | `'127.0.0.1'`        | Address to which Hoodie binds
-`--data`                  | `'.hoodie'`          | Data path
-`--dbUrl`                 | -                    | If provided, uses external CouchDB listening at the given URL. (Can contain auth credentials)
-`--dbUrlUsername`         | -                    | Username to authenticate with the external CouchDB (requires --dbUrl and --dbUrlPassword to be set).
-`--dbUrlPassword`         | -                    | Password to authenticate with the external CouchDB (requires --dbUrl and/or --dbUrlUsername to be set).
-`--dbAdapter`             | `pouchdb-adapter-fs` | Default [PouchDB adapter](https://pouchdb.com/adapters.html). Ignored if `dbUrl` or `inMemory` set
-`--adminPassword`         | -                    | Password to login to Admin Dashboard. Login is not possible if `adminPassword` option is not set
-`--loglevel`              | `'warn'`             | One of: silent, error, warn, http, info, verbose, silly
-`-m`, `--inMemory`        | `false`              | Whether to start the PouchDB Server in memory
-`--port`                  | `8080`               | Port-number to run the Hoodie App on
-`--public`                | `'public'`           | path to static assets
-`--url`                   | -                    | Optional: external URL at which Hoodie Server is accessible (e.g. `http://myhoodieapp.com`)
-`-h`, `--help`, `--usage` | -                    | Prints help and available options
-`-v`, `--version`         | -                    | Shows Hoodie version
-
-Hoodie CLI is using [rc](https://www.npmjs.com/package/rc) for configuration, so the same options can be set with environment variables and config files. Environment variables are prefixed with `hoodie_`. Examples: `hoodie_port=8090` or `hoodie_inMemory=true`. Configuration files can be in INI or JSON format and [can be placed at different locations](https://www.npmjs.com/package/rc#standards). Most commonly you would place a `.hoodierc` file in your app’s directory, and it can look like this
-
-```json
-{
-  "port": 8090,
-  "inMemory": true,
-  "adminPassword": "secret"
-}
-```
-
-The priority of configuration:
-
-1. command line arguments
-2. Environment variables
-3. `.hoodierc` files
-4. Your app’s defaults form `"hoodie"` key in `"package.json"`
-5. Hoodie’s defaults as shown in table above
-
-### hapi plugin
-
-You can load `hoodie` as hapi plugin to use it in your existing hapi application:
-
-```js
-var Hapi = require('hapi')
-var hoodie = require('hoodie').register
-
-var server = new Hapi.Server()
-server.connection({
-  host: 'localhost',
-  port: 8000
-})
-
-server.register({
-  register: hoodie,
-  options: { // pass options here
-    inMemory: true,
-    public: 'dist'
-  }
-}, function (error) {
-  if (error) {
-    throw error
-  }
-
-  server.start(function (error) {
-    if (error) {
-      throw error
-    }
-
-    console.log(('Server running at:', server.info.uri)
-  })
-})
-```
-
-The available options are
-
-option                    | default      | description
-------------------------- | ------------ | -------------
-**PouchDB**               | –            | [PouchDB constructor](https://pouchdb.com/api.html#defaults). See also [custom PouchDB builds](https://pouchdb.com/2016/06/06/introducing-pouchdb-custom-builds.html)
-**paths.data**            | `'.hoodie'`  | Data path
-**paths.public**          | `'public'`   | Public path
-**adminPassword**         | –            | Password to login to Admin Dashboard. Login is not possible if `adminPassword` option is not set
-**inMemory**              | `false`      | If set to true, configuration and other files will not be read from / written to the file system
-**client**                | `{}`         | [Hoodie Client](https://github.com/hoodiehq/hoodie-client#constructor) options. `client.url` is set based on hapi’s `server.info.host`
-**account**               | `{}`         | [Hoodie Account Server](https://github.com/hoodiehq/hoodie-account-server/tree/master/plugin#options) options. `account.admins`, `account.secret` and `account.usersDb` are set based on `db` option above
-**store**                 | `{}`         | [Hoodie Store Server](https://github.com/hoodiehq/hoodie-store-server#options) options. `store.couchdb`, `store.PouchDB` are set based on `db` option above. `store.hooks.onPreAuth` is set to bind user authentication for Hoodie Account to Hoodie Store
-
-## Extending Hoodie for your app
-
-You can add custom behavior to your hoodie-based application in a few ways:
-
-1. The server-side functionality can be extended by adding a module constructed as a [hapi plugin](http://hapijs.com/tutorials/plugins)
- to the path `hoodie/server`. E.g:
-
-```js
-exports.register = function (server, options, next) {
-  // server-relevant code here
-  next();
-}
-
-exports.register.attributes = {
-  'name': 'my-app'
-}
-```
-
-2. The client-side functionality can be extended by adding a module that can be interpreted as a [hoodie plugin](https://github.com/hoodiehq/hoodie-client#hoodieplugin)
- to the path `hoodie/client`, which by default will be built into the client.js package by browserify.
-
-```js
-module.exports = {
-  demonstratePlugin: function () { // will be attached as hoodie.demonstratePlugin()
-    // client-relevant code here
-  }
-}
-```
+You can find a more thorough description in our [Getting Started Guide](http://docs.hood.ie/en/latest/guides/quickstart.html).
 
-Both of the above paths will be imported as if Node modules, so the path to the entry file for each module can be
-`hoodie/{server,client}.js` or `hoodie/{server,client}/index.js`.
+## Usage
 
-Note that for now only the file modification time of the entry file of the client module is used to determine the
-freshness of the client bundle and therefore whether it should be rebuilt on the first request after a server restart.
-If your client code stretches to many files, you may need to update the `mtime` of the client entry file by using
-`touch` or a similar method before restarting your server to ensure that the client bundle is rebuilt.
+`hoodie` can be used standalone or as [hapi plugin](http://hapijs.com/tutorials/plugins).
+The options are slightly different. For the standalone usage, see [Hoodie’s configuration guide](http://docs.hood.ie/en/latest/guides/configuration.html).
+For the hapi plugin usage, see [Hoodie’s hapi plugin usage guide](http://docs.hood.ie/en/latest/guides/hapi-plugin.html)
 
 ## Testing
 
diff --git a/cli/app-defaults.js b/cli/app-defaults.js
index 67f8fdac..0c818480 100644
--- a/cli/app-defaults.js
+++ b/cli/app-defaults.js
@@ -6,7 +6,9 @@ function getAppDefaults (projectPath) {
   var pkg = require(join(projectPath, 'package.json'))
   var appOptions = pkg.hoodie || {}
 
-  appOptions.name = pkg.name
+  if (!appOptions.name) {
+    appOptions.name = pkg.name
+  }
 
   return appOptions
 }
diff --git a/cli/hoodie-defaults.js b/cli/hoodie-defaults.js
index 72b158e1..63a0225c 100644
--- a/cli/hoodie-defaults.js
+++ b/cli/hoodie-defaults.js
@@ -2,6 +2,7 @@ module.exports = getHoodieDefaults
 
 function getHoodieDefaults () {
   return {
+    name: undefined,
     address: '127.0.0.1',
     port: 8080,
     data: '.hoodie',
@@ -14,6 +15,8 @@ function getHoodieDefaults () {
     inMemory: false,
     loglevel: 'warn',
     url: undefined,
-    adminPassword: undefined
+    adminPassword: undefined,
+    plugins: [],
+    app: {}
   }
 }
diff --git a/cli/options.js b/cli/options.js
index a86b4d7f..229506bb 100644
--- a/cli/options.js
+++ b/cli/options.js
@@ -1,5 +1,8 @@
 module.exports = getCliOptions
 
+var fs = require('fs')
+var path = require('path')
+
 var log = require('npmlog')
 var pick = require('lodash').pick
 var rc = require('rc')
@@ -119,9 +122,17 @@ function getCliOptions (projectPath) {
     options.address = options.bindAddress
   }
 
+  options.name = defaults.name
   options.public = webrootLocator(options.public)
+  options.plugins = defaults.plugins
+  options.app = defaults.app
   options.client = defaults.client
 
+  // If app has a hoodie folder, add it to the list of plugins
+  if (fs.existsSync(path.join(projectPath, 'hoodie'))) {
+    options.plugins.push(projectPath)
+  }
+
   // rc & yargs are setting keys we are not interested in, like in-memory or _
   // so we only pick the relevant ones based on they keys of the default options.
   return pick(options, Object.keys(hoodieDefaults))
diff --git a/cli/parse-options.js b/cli/parse-options.js
index a4f81829..6929226a 100644
--- a/cli/parse-options.js
+++ b/cli/parse-options.js
@@ -30,11 +30,14 @@ function parseOptions (options) {
   var dbOptions = {}
 
   var config = {
+    name: options.name,
     loglevel: options.loglevel,
     paths: {
       data: options.data,
       public: options.public
     },
+    plugins: options.plugins,
+    app: options.app,
     inMemory: Boolean(options.inMemory),
     client: options.client
   }
diff --git a/docs/guides/hapi-plugin.rst b/docs/guides/hapi-plugin.rst
new file mode 100644
index 00000000..318fe455
--- /dev/null
+++ b/docs/guides/hapi-plugin.rst
@@ -0,0 +1,67 @@
+Using Hoodie as hapi plugin
+===========================
+
+Here is an example usage of Hoodie as a hapi plugin:
+
+.. code:: js
+    var Hapi = require('hapi')
+    var hoodie = require('hoodie').register
+
+    var server = new Hapi.Server()
+    server.connection({
+      host: 'localhost',
+      port: 8000
+    })
+
+    server.register({
+      register: hoodie,
+      options: { // pass options here
+        inMemory: true,
+        public: 'dist'
+      }
+    }, function (error) {
+      if (error) {
+        throw error
+      }
+
+      server.start(function (error) {
+        if (error) {
+          throw error
+        }
+
+        console.log(('Server running at:', server.info.uri)
+      })
+    })
+
+The available options are
+
++-------------------------+---------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+| option                  | default       | description                                                                                                                                                                                                 |
++=========================+===============+=============================================================================================================================================================================================================+
+| **PouchDB**             | –             | `PouchDB constructor`_. See also `custom PouchDB builds`_                                                                                                                                                   |
++-------------------------+---------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+| **paths.data**          | ``'.hoodie'`` | Data path                                                                                                                                                                                                   |
++-------------------------+---------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+| **paths.public**        | ``'public'``  | Public path                                                                                                                                                                                                 |
++-------------------------+---------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+| **adminPassword**       | –             | Password to login to Admin Dashboard. Login is not possible if ``adminPassword`` option is not set                                                                                                          |
++-------------------------+---------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+| **inMemory**            | ``false``     | If set to true, configuration and other files will not be read from / written to the file system                                                                                                            |
++-------------------------+---------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+| **client**              | ``{}``        | `Hoodie Client`_ options. ``client.url `` is set based on hapi’s ``server.info.host``                                                                                                                       |
++-------------------------+---------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+| **account**             | ``{}``        | `Hoodie Account Server`_ options. ``account.admins``, ``account.secret`` and ``account.usersDb`` are set based on ``db`` option above                                                                       |
++-------------------------+---------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+| **store**               | ``{}``        | `Hoodie Store Server`_ options. ``store.couchdb``, ``store.PouchDB`` are set based on ``db`` option above. ``store.hooks.onPreAuth` ` is set to bind user authentication for Hoodie Account to Hoodie Store |
++-------------------------+---------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+| **plugins**             | ``[]``        | Array of npm names or paths of locations containing plugins. See also `Hoodie plugins docs`_                                                                                                                |
++-------------------------+---------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+| **app**                 | ``{}``        | App specific options for plugins                                                                                                                                                                            |
++-------------------------+---------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+
+.. _PouchDB constructor: https://pouchdb.com/api.html#defaults
+.. _custom PouchDB builds: https://pouchdb.com/2016/06/06/introducing-pouchdb-custom-builds.html
+.. _Hoodie Client: https://github.com/hoodiehq/hoodie-client#constructor
+.. _Hoodie Account Server: https://github.com/hoodiehq/hoodie-account-server/tree/master/plugin#options
+.. _Hoodie Store Server: https://github.com/hoodiehq/hoodie-store-server#options
+.. _Hoodie plugins docs: http://docs.hood.ie/en/latest/guides/plugins.html
diff --git a/docs/guides/plugins.rst b/docs/guides/plugins.rst
index 9996e7e4..69586e54 100644
--- a/docs/guides/plugins.rst
+++ b/docs/guides/plugins.rst
@@ -1,7 +1,7 @@
 Plugins
 =======
 
-You can extend your Hoodie backend in two ways
+You can extend your Hoodie app in two ways
 
 1. App-specific plugins
 2. 3rd party plugins
@@ -10,17 +10,92 @@ App-specific plugins
 ~~~~~~~~~~~~~~~~~~~~
 
 You can extend your Hoodie’s client by creating the file ``hoodie/client/index.js``
-in your app’s repository, which should export a `Hoodie Client plugin <https://github.com/hoodiehq/hoodie-client#hoodieplugin>`.
+in your app’s repository, which should export a `Hoodie Client plugin <../api/client/hoodie.html#hoodie-plugin>`.
 It will dynamically be bundled into your client accessible at the ``/hoodie/client.js`` route.
 
+Example
+
+.. code:: js
+
+    // /hoodie/client.js
+    module.exports = function (hoodie) {
+      hoodie.hello = function (what) {
+        return Promise.resolve('Hello, ' + (what || 'world') + '!')
+      }
+    }
+
 You can extend your Hoodie’s server routes and API by creating ``hoodie/server/index.js``
 in your app’s, which should export a `hapi plugin <https://hapijs.com/tutorials/plugins>`.
+All server routes defined in the plugin will be prefixed with ``/hoodie/<app name>``
+
+Example
+
+.. code:: js
+
+    module.exports.register = register
+    module.exports.register.attributes = {
+      name: 'hoodie-app-plugin'
+    }
+
+    function register (server, options, next) {
+      server.route({
+        method: 'GET',
+        path: '/api',
+        handler: function (request, reply) {
+          reply('Hello, world!')
+        }
+      })
+
+      next()
+    }
+
 
 3rd party plugins
 ~~~~~~~~~~~~~~~~~
 
-Hoodie will soon support loading 3rd party plugins from npm packages. You can
-watch `this pull request <https://github.com/hoodiehq/hoodie/pull/482>`__ for updates.
+Hoodie plugins are npm modules. We recommend to prefix your plugin names with
+``hoodie-plugin-``, but it’s not required. The folder structure is the same as
+for app-specific plugins:
+
+.. code:: bash
+    ├── package.json
+    └── hoodie
+        ├── client.js    # or: hoodie/client/index.js
+        ├── server.js    # or: hoodie/server/index.js
+        └── public
+            └── index.html
+
+The server plugin must be loadable via ``require('hoodie-plugin-foo/hoodie/server')``.
+A Hoodie server plugin is a [hapi plugin](http://hapijs.com/tutorials/plugins).
+The client plugin must be loadable via ``require('hoodie-plugin-foo/hoodie/client')``
+A Hoodie client plugin can be a function or an object,
+it will be passed into `hoodie.plugin() <../api/client/hoodie.html#hoodie-plugin>`
+
+Hoodie plugins can extend the Hoodie client, the Hoodie server and provide a
+web UI for `/hoodie/<plugin name>`. All extension points are optional.
+The ``hoodie/public`` folder will be exposed at `/hoodie/<plugin name>` by the
+server if it exists. All server routes will be prefixed with `/hoodie/<plugin name>`.
+
+``<plugin name>`` is the name property in your ``package.json`` file, but can be
+overridden with the ``hoodie.name`` property.
+
+After installing and adding a Hoodie plugin to your app’s dependencies, you also
+have to enable it by adding it to the ``hoodie.plugins`` array in your app’s
+`package.json` file. The names are the npm package names.
+
+.. code:: json
+    {
+      "name": "my-app",
+      ...
+      "hoodie": {
+        "plugins": ["hoodie-plugin-hello-world"]
+      }
+    }
+
+The order in which server/client plugins are loaded is
+
+1. core modules (account, store, task)
+2. 3rd party plugins (npm dependencies)
+3. app plugins
 
-To get an idea how 3rd party plugins will work and look like, have a look at
-https://github.com/hoodiehq/hoodie-plugin-example
+For an example plugin, have a look at Hoodie’s `Hello, world! plugin <https://github.com/hoodiehq/hoodie-plugin-hello-world>`
diff --git a/docs/index.rst b/docs/index.rst
index 0cbaa64b..4e4e6700 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -14,6 +14,7 @@ Hoodie Documentation
    guides/configuration
    guides/plugins
    guides/deployment
+   guides/hapi-plugin
 
 .. toctree::
    :maxdepth: 1
@@ -24,7 +25,7 @@ Hoodie Documentation
 .. toctree::
    :maxdepth: 1
    :caption: Developers
-   
+
    developers/CONTRIBUTING
    developers/CODING_STYLE
    developers/TRIAGING
diff --git a/server/index.js b/server/index.js
index 51deb992..a12b0647 100644
--- a/server/index.js
+++ b/server/index.js
@@ -14,7 +14,9 @@ function register (server, options, next) {
   _.defaultsDeep(options, {
     paths: {
       public: 'public'
-    }
+    },
+    plugins: [],
+    app: {}
   })
 
   server.ext('onPreResponse', corsHeaders)
@@ -23,6 +25,7 @@ function register (server, options, next) {
     if (error) {
       return next(error)
     }
+
     registerPlugins(server, options, function (error) {
       if (error) {
         return next(error)
diff --git a/server/plugins/client/bundle-handler-factory.js b/server/plugins/client/bundle-handler-factory.js
index 304b1111..796c740b 100644
--- a/server/plugins/client/bundle-handler-factory.js
+++ b/server/plugins/client/bundle-handler-factory.js
@@ -23,7 +23,6 @@ function createBundleHandler (hoodieClientPath, bundleTargetPath, bundleConfig)
         })
       })
     }
-
     bundlePromise.then(function (buffer) {
       reply(buffer).bytes(buffer.length).type('application/javascript')
     }).catch(reply)
diff --git a/server/plugins/client/bundle.js b/server/plugins/client/bundle.js
index 8a31c1a3..dbda65d8 100644
--- a/server/plugins/client/bundle.js
+++ b/server/plugins/client/bundle.js
@@ -27,10 +27,15 @@ function checkModule (module) {
  * client, so we need to browserify on-the-fly to avoid dependency duplication,
  * and avoiding unneeded bundling with browserify saves a significant time.
  */
-function bundleClient (hoodieClientPath, bundleTargetPath, config, callback) {
-  var plugins = [
-    path.resolve('hoodie/client')
-  ].filter(checkModule)
+function bundleClient (hoodieClientPath, bundleTargetPath, options, callback) {
+  var pluginPaths = options.plugins.map(function (pluginName) {
+    return pluginName + '/hoodie/client'
+  })
+
+  var plugins = [path.resolve('hoodie/client')]
+    .concat(pluginPaths)
+    .filter(checkModule)
+
   var getPluginsModifiedTimes = plugins.map(function (pluginPath) {
     return getModifiedTime.bind(null, requireResolve(pluginPath))
   })
@@ -48,7 +53,7 @@ function bundleClient (hoodieClientPath, bundleTargetPath, config, callback) {
     var sourceTime = Math.max.apply(null, results)
     var hasUpdate = sourceTime > targetTime
 
-    var get = hasUpdate ? buildBundle.bind(null, config, plugins) : fs.readFile.bind(null, bundleTargetPath)
+    var get = hasUpdate ? buildBundle.bind(null, options, plugins) : fs.readFile.bind(null, bundleTargetPath)
 
     get(function (error, buffer) {
       if (error) {
@@ -71,7 +76,7 @@ function getModifiedTime (path, callback) {
   })
 }
 
-function buildBundle (config, plugins, callback) {
+function buildBundle (options, plugins, callback) {
   var ReadableStream = require('stream').Readable
   var browserify = require('browserify')
   var stream = new ReadableStream()
@@ -84,14 +89,14 @@ function buildBundle (config, plugins, callback) {
   hoodieBundleSource += 'var Hoodie = require("@hoodie/client")\n'
   hoodieBundleSource += 'var options = {\n'
 
-  if (config.client) {
-    Object.keys(config.client).forEach(function (key) {
-      hoodieBundleSource += '  "' + key + '": ' + JSON.stringify(config.client[key]) + ',\n'
+  if (options.client) {
+    Object.keys(options.client).forEach(function (key) {
+      hoodieBundleSource += '  "' + key + '": ' + JSON.stringify(options.client[key]) + ',\n'
     })
   }
 
-  if (config.url) {
-    hoodieBundleSource += '  url: "' + config.url + '",\n'
+  if (options.url) {
+    hoodieBundleSource += '  url: "' + options.url + '",\n'
   } else {
     hoodieBundleSource += '  url: location.origin,\n'
   }
diff --git a/server/plugins/client/index.js b/server/plugins/client/index.js
index d81d54b2..3b1b9bb7 100644
--- a/server/plugins/client/index.js
+++ b/server/plugins/client/index.js
@@ -11,7 +11,7 @@ var createBundleHandler = require('./bundle-handler-factory')
 function register (server, options, next) {
   var hoodieClientModulePath = path.dirname(require.resolve('@hoodie/client/package.json'))
   var hoodieClientPath = path.join(hoodieClientModulePath, 'index.js')
-  var bundleTargetPath = path.join(options.config.data || '.hoodie', 'client.js')
+  var bundleTargetPath = path.join(options.data || '.hoodie', 'client.js')
 
   // TODO: add /hoodie/client.min.js path
   // https://github.com/hoodiehq/hoodie-client/issues/34
@@ -19,7 +19,7 @@ function register (server, options, next) {
   server.route([{
     method: 'GET',
     path: '/hoodie/client.js',
-    handler: createBundleHandler(hoodieClientPath, bundleTargetPath, options.config)
+    handler: createBundleHandler(hoodieClientPath, bundleTargetPath, options)
   }])
 
   next()
diff --git a/server/plugins/index.js b/server/plugins/index.js
index 89f3384c..882b1280 100644
--- a/server/plugins/index.js
+++ b/server/plugins/index.js
@@ -1,12 +1,11 @@
 module.exports = registerPlugins
 
 var log = require('npmlog')
-var path = require('path')
 var requireResolve = require('./resolver')
 
-function checkModule (module) {
+function checkModule (modulePath) {
   try {
-    requireResolve(module)
+    requireResolve(modulePath)
     return true
   } catch (err) {
     if (err.code !== 'MODULE_NOT_FOUND') {
@@ -16,10 +15,7 @@ function checkModule (module) {
   }
 }
 
-function registerPlugins (server, config, callback) {
-  var options = {
-    config: config
-  }
+function registerPlugins (server, options, callback) {
   var hapiPlugins = [
     require('inert')
   ]
@@ -29,22 +25,37 @@ function registerPlugins (server, config, callback) {
     './logger',
     './maybe-force-gzip',
     './public'
-  ]
-    .concat(
-  [
-    path.resolve('hoodie/server')
-  ]
-    .filter(checkModule)
-    )
-    .map(function (register) {
-      return {
-        options: options,
-        register: require(register)
+  ].map(function (pluginPath) {
+    return {
+      options: options,
+      register: require(pluginPath)
+    }
+  })
+
+  var externalPlugins = options.plugins
+    .filter(function (pluginPath) {
+      return checkModule(pluginPath + '/hoodie/server')
+    })
+    .map(function (pluginPath) {
+      var pkg = require(pluginPath + '/package.json')
+      var pluginName = pkg.hoodie ? pkg.hoodie.name || pkg.name : pkg.name
+      var hapiPluginOptions = require(pluginPath + '/hoodie/server')
+
+      if (!hapiPluginOptions.register) {
+        hapiPluginOptions = {register: hapiPluginOptions}
       }
+
+      hapiPluginOptions.options = options
+      hapiPluginOptions.routes = { prefix: '/hoodie/' + pluginName }
+
+      return hapiPluginOptions
     })
 
-  log.silly('hapi', 'Registering internal plugins')
-  server.register(hapiPlugins.concat(localPlugins), function (error) {
+  var plugins = hapiPlugins.concat(localPlugins, externalPlugins)
+
+  log.silly('hapi', 'Registering plugins')
+
+  server.register(plugins, function (error) {
     if (error) {
       return callback(error)
     }
diff --git a/server/plugins/public.js b/server/plugins/public.js
index 6c1c76ae..a0f249ed 100644
--- a/server/plugins/public.js
+++ b/server/plugins/public.js
@@ -4,11 +4,15 @@ module.exports.register.attributes = {
   dependencies: 'inert'
 }
 
+var path = require('path')
+var requireResolve = require('./resolver')
 var createReadStream = require('fs').createReadStream
-var pathJoin = require('path').join
 
 function register (server, options, next) {
-  var publicFolder = options.config.paths.public
+  var paths = options.paths
+  var plugins = options.plugins
+  var publicFolder = paths.public
+
   var hoodieVersion
   try {
     hoodieVersion = require('hoodie/package.json').version
@@ -16,10 +20,9 @@ function register (server, options, next) {
     hoodieVersion = 'development'
   }
 
-  var hoodiePublicPath = pathJoin(require.resolve('../../package.json'), '..', 'public')
-  var adminPublicPath = pathJoin(require.resolve('@hoodie/admin/package.json'), '..', 'dist')
-
-  server.route([{
+  var hoodiePublicPath = path.join(requireResolve('../../package.json'), '..', 'public')
+  var adminPublicPath = path.join(requireResolve('@hoodie/admin/package.json'), '..', 'dist')
+  var routes = [{
     method: 'GET',
     path: '/{p*}',
     handler: {
@@ -55,15 +58,49 @@ function register (server, options, next) {
     handler: function (request, reply) {
       reply({
         hoodie: true,
-        name: options.config.name,
+        name: options.name,
         version: hoodieVersion
       })
     }
-  }])
+  }]
+
+  // add plugin routes
+  plugins.forEach(function (pluginPath) {
+    // check if module directory exists
+    try {
+      var pluginPackagePath = requireResolve(pluginPath + '/package.json')
+    } catch (error) {
+      if (error.code !== 'MODULE_NOT_FOUND') {
+        throw error
+      }
+    }
+
+    if (!pluginPackagePath) {
+      return
+    }
+
+    var pkg = require(pluginPackagePath)
+
+    var name = pkg.hoodie ? pkg.hoodie.name || pkg.name : pkg.name
+
+    routes.push({
+      method: 'GET',
+      path: '/hoodie/' + name + '/{p*}',
+      handler: {
+        directory: {
+          path: path.join(path.dirname(pluginPackagePath), 'hoodie', 'public'),
+          listing: false,
+          index: true
+        }
+      }
+    })
+  })
+
+  server.route(routes)
 
   // serve app whenever an html page is requested
   // and no other document is available
-  var app = pathJoin(publicFolder, 'index.html')
+  var app = path.join(publicFolder, 'index.html')
   server.ext('onPostHandler', function (request, reply) {
     var response = request.response
 
@@ -77,7 +114,7 @@ function register (server, options, next) {
     var isAdminPublicPath = /^\/hoodie\/admin\//.test(request.path) && !(/^\/hoodie\/admin\/api\//).test(request.path)
 
     if (isAdminPublicPath && isHtmlRequest) {
-      return reply(createReadStream(pathJoin(adminPublicPath, 'index.html')))
+      return reply(createReadStream(path.join(adminPublicPath, 'index.html')))
     }
 
     if (isHoodiePath) {


Test patch:
diff --git a/test/integration/force-gzip-test.js b/test/integration/force-gzip-test.js
index 24c426b2..7ae17ba8 100644
--- a/test/integration/force-gzip-test.js
+++ b/test/integration/force-gzip-test.js
@@ -19,7 +19,9 @@ var hapiPluginOptions = {
     inMemory: true,
     loglevel: 'error',
     paths: {},
-    PouchDB: PouchDB
+    PouchDB: PouchDB,
+    plugins: [],
+    app: {}
   }
 }
 
diff --git a/test/integration/handle-404-test.js b/test/integration/handle-404-test.js
index 6f243a12..ee4df639 100644
--- a/test/integration/handle-404-test.js
+++ b/test/integration/handle-404-test.js
@@ -11,7 +11,9 @@ var hapiPluginOptions = {
     inMemory: true,
     loglevel: 'error',
     paths: {},
-    PouchDB: PouchDB
+    PouchDB: PouchDB,
+    plugins: [],
+    app: {}
   }
 }
 
diff --git a/test/integration/handle-hoodie-request-test.js b/test/integration/handle-hoodie-request-test.js
index 6495446d..fdc09898 100644
--- a/test/integration/handle-hoodie-request-test.js
+++ b/test/integration/handle-hoodie-request-test.js
@@ -11,7 +11,9 @@ var hapiPluginOptions = {
     inMemory: true,
     loglevel: 'error',
     paths: {},
-    PouchDB: PouchDB
+    PouchDB: PouchDB,
+    plugins: [],
+    app: {}
   }
 }
 
diff --git a/test/integration/plugins-test.js b/test/integration/plugins-test.js
new file mode 100644
index 00000000..4dcf9999
--- /dev/null
+++ b/test/integration/plugins-test.js
@@ -0,0 +1,23 @@
+var simple = require('simple-mock')
+var test = require('tap').test
+
+var hapiPlugin = require('../../server')
+var serverMock = {
+  register: simple.stub().callbackWith(null),
+  ext: simple.stub()
+}
+
+require('npmlog').level = 'error'
+
+test('plugins', function (t) {
+  hapiPlugin.register(serverMock, {
+    paths: {
+      data: '.'
+    },
+    plugins: ['hoodie-plugin-foobar'],
+    app: {}
+  }, function (error) {
+    t.error(error)
+    t.end()
+  })
+})
diff --git a/test/integration/server-test.js b/test/integration/server-test.js
index b492782c..26fe9d91 100644
--- a/test/integration/server-test.js
+++ b/test/integration/server-test.js
@@ -14,18 +14,28 @@ test('server', function (t) {
   hapiPlugin.register(serverMock, {
     paths: {
       data: '.'
-    }
-  }, function (error, config) {
+    },
+    plugins: [],
+    app: {}
+  }, function (error) {
     t.error(error)
     t.end()
   })
 })
 
-test('server with options without options.path (#554)', function (t) {
-  hapiPlugin.register(serverMock, {}, function (error, config) {
-    t.error(error)
-    t.end()
-  })
+test('does not modify the passed options object', function (t) {
+  var options = {
+    db: {
+      url: 'http://admin:admin@localhost:5984'
+    },
+    plugins: [],
+    app: {}
+  }
+
+  hapiPlugin.register(serverMock, options, function () {})
+
+  t.is(options.db.url, 'http://admin:admin@localhost:5984')
+  t.end()
 })
 
 test('error on register is passed to callback', function (t) {
@@ -37,7 +47,9 @@ test('error on register is passed to callback', function (t) {
   hapiPlugin.register(serverErrorMock, {
     paths: {
       data: '.'
-    }
+    },
+    plugins: [],
+    app: {}
   }, function (error, server, options) {
     t.ok(error)
     t.end()
@@ -48,7 +60,9 @@ test('application root with valid server module', function (t) {
   var options = {
     paths: {
       public: 'public'
-    }
+    },
+    plugins: [],
+    app: {}
   }
   var savedPath = process.cwd()
   t.tearDown(function () {
diff --git a/test/integration/smoke-test.js b/test/integration/smoke-test.js
index 3257796a..00c5fe12 100644
--- a/test/integration/smoke-test.js
+++ b/test/integration/smoke-test.js
@@ -18,7 +18,9 @@ var hapiPluginOptions = {
     inMemory: true,
     loglevel: 'error',
     paths: {},
-    PouchDB: PouchDB
+    PouchDB: PouchDB,
+    plugins: [],
+    app: {}
   }
 }
 
diff --git a/test/unit/cli/app-defaults-test.js b/test/unit/cli/app-defaults-test.js
index 0c9d1bcd..ac46fb8a 100644
--- a/test/unit/cli/app-defaults-test.js
+++ b/test/unit/cli/app-defaults-test.js
@@ -37,5 +37,22 @@ test('app options', function (group) {
     t.end()
   })
 
+  group.test('with hoodie.name', function (t) {
+    var getAppDefaults = proxyquire('../../../cli/app-defaults', {
+      'path': pathMock,
+      './package.json': {
+        name: 'pkg-name',
+        hoodie: {
+          name: 'foo'
+        }
+      }
+    })
+    var options = getAppDefaults()
+
+    t.deepEqual(options.name, 'foo', 'sets name from pkg.hoodie.name')
+
+    t.end()
+  })
+
   group.end()
 })
diff --git a/test/unit/cli/hoodie-default-test.js b/test/unit/cli/hoodie-default-test.js
index 00fe6afb..367aeb6b 100644
--- a/test/unit/cli/hoodie-default-test.js
+++ b/test/unit/cli/hoodie-default-test.js
@@ -8,6 +8,7 @@ test('hoodie defaults test', function (group) {
     t.deepEqual(defaults, {
       address: '127.0.0.1',
       adminPassword: undefined,
+      app: {},
       client: {},
       data: '.hoodie',
       dbAdapter: 'pouchdb-adapter-fs',
@@ -16,7 +17,9 @@ test('hoodie defaults test', function (group) {
       dbUrlUsername: undefined,
       inMemory: false,
       loglevel: 'warn',
+      name: undefined,
       port: 8080,
+      plugins: [],
       public: 'public',
       url: undefined
     }, 'setting hoodie defaults')
diff --git a/test/unit/cli/options-test.js b/test/unit/cli/options-test.js
index 76916a85..cea659e0 100644
--- a/test/unit/cli/options-test.js
+++ b/test/unit/cli/options-test.js
@@ -38,17 +38,20 @@ var packageJsonMock = {
 
 var mockWebrootLocator = simple.stub()
 
+var hoodieDefaults = {
+  port: 'hoodie-default-port',
+  public: 'hoodie-default-public',
+  dbUrl: 'hoodie-default-dbUrl',
+  dbAdapter: 'hoodie-default-dbAdapter',
+  address: 'hoodie-localhost',
+  plugins: []
+}
+
 function createCliOptionsProxy (yargsApi) {
   return proxyquire('../../../cli/options', {
     'npmlog': { warn: simple.spy() },
     './hoodie-defaults': function () {
-      return {
-        port: 'hoodie-default-port',
-        public: 'hoodie-default-public',
-        dbUrl: 'hoodie-default-dbUrl',
-        dbAdapter: 'hoodie-default-dbAdapter',
-        address: 'hoodie-localhost'
-      }
+      return hoodieDefaults
     },
     './app-defaults': mockAppDefaults(),
     './webroot-locator': mockWebrootLocator,
@@ -127,3 +130,24 @@ test('bindAddress', function (t) {
 
   t.end()
 })
+
+test('app plugin', function (t) {
+  var getOptions = proxyquire('../../../cli/options', {
+    'fs': {
+      existsSync: simple.stub().returnWith(true)
+    },
+    'npmlog': { warn: simple.spy() },
+    './hoodie-defaults': function () {
+      return hoodieDefaults
+    },
+    './app-defaults': mockAppDefaults(),
+    './webroot-locator': mockWebrootLocator,
+    'yargs': createYargsMock({}),
+    '../package.json': packageJsonMock
+  })
+
+  var options = getOptions('project-path')
+  t.deepEqual(options.plugins, ['project-path'])
+
+  t.end()
+})
diff --git a/test/unit/client/bundle-handler-factory-test.js b/test/unit/client/bundle-handler-factory-test.js
index 26558244..16dc3769 100644
--- a/test/unit/client/bundle-handler-factory-test.js
+++ b/test/unit/client/bundle-handler-factory-test.js
@@ -29,12 +29,12 @@ test('bundle-handler-factory', function (group) {
     done()
   })
 
-  group.test('passes clientPath, targetPath and config to bundleClient', function (t) {
+  group.test('passes clientPath, targetPath and options to bundleClient', function (t) {
     var clientPath = '/client/path'
     var targetPath = '/target/path'
-    var config = {}
+    var options = {}
 
-    var handler = createHandler(clientPath, targetPath, config)
+    var handler = createHandler(clientPath, targetPath, options)
     handler()
 
     t.is(bundleStub.callCount, 1, 'bundleClient gets called')
@@ -42,7 +42,7 @@ test('bundle-handler-factory', function (group) {
 
     t.is(bundleStubArgs[0], clientPath, 'passes clientPath as first argument')
     t.is(bundleStubArgs[1], targetPath, 'passes targetPath as second argument')
-    t.equals(bundleStubArgs[2], config, 'passes config as third argument')
+    t.equals(bundleStubArgs[2], options, 'passes options as third argument')
 
     t.end()
   })
diff --git a/test/unit/plugins-client-bundle-test.js b/test/unit/client/bundle-test.js
similarity index 80%
rename from test/unit/plugins-client-bundle-test.js
rename to test/unit/client/bundle-test.js
index 2672829f..acb42f8b 100644
--- a/test/unit/plugins-client-bundle-test.js
+++ b/test/unit/client/bundle-test.js
@@ -8,14 +8,16 @@ require('npmlog').level = 'error'
 test('bundle client', function (group) {
   group.test('client & bundle with same mtime', function (t) {
     var readFileMock = simple.stub().callbackWith(null, Buffer.from('bundle content'))
-    var bundleClient = proxyquire('../../server/plugins/client/bundle', {
+    var bundleClient = proxyquire('../../../server/plugins/client/bundle', {
       fs: {
         readFile: readFileMock,
         stat: simple.stub().callbackWith(null, {mtime: new Date()})
       }
     })
 
-    bundleClient('client.js', 'bundle.js', {}, function (error) {
+    bundleClient('client.js', 'bundle.js', {plugins: [
+      'hoodie-plugin-foobar'
+    ]}, function (error) {
       t.error(error)
 
       t.is(readFileMock.callCount, 1, 'readFile called once')
@@ -32,7 +34,7 @@ test('bundle client', function (group) {
     var streamStub = {
       push: simple.stub()
     }
-    var bundleClient = proxyquire('../../server/plugins/client/bundle', {
+    var bundleClient = proxyquire('../../../server/plugins/client/bundle', {
       browserify: function () {
         return {
           bundle: bundleMock,
@@ -53,7 +55,7 @@ test('bundle client', function (group) {
       }
     })
 
-    bundleClient('client.js', 'bundle.js', {}, function (error, buffer) {
+    bundleClient('client.js', 'bundle.js', {plugins: []}, function (error, buffer) {
       t.error(error)
 
       t.is(bundleMock.callCount, 1, 'bundle called once')
@@ -77,7 +79,7 @@ test('bundle client', function (group) {
     var streamStub = {
       push: simple.stub()
     }
-    var bundleClient = proxyquire('../../server/plugins/client/bundle', {
+    var bundleClient = proxyquire('../../../server/plugins/client/bundle', {
       browserify: function () {
         return {
           bundle: bundleMock,
@@ -99,10 +101,11 @@ test('bundle client', function (group) {
     })
 
     bundleClient('client.js', 'bundle.js', {
-      url: 'https://myapp.com',
       client: {
         foo: 'bar'
-      }
+      },
+      plugins: [],
+      url: 'https://myapp.com'
     }, function (error, buffer) {
       t.error(error)
 
@@ -120,14 +123,14 @@ test('bundle client', function (group) {
   })
 
   group.test('fs.readFile fails', function (t) {
-    var bundleClient = proxyquire('../../server/plugins/client/bundle', {
+    var bundleClient = proxyquire('../../../server/plugins/client/bundle', {
       fs: {
         readFile: simple.stub().callbackWith(new Error('boom')),
         stat: simple.stub().callbackWith(null, {mtime: new Date()})
       }
     })
 
-    bundleClient('client.js', 'bundle.js', {}, function (error) {
+    bundleClient('client.js', 'bundle.js', {plugins: []}, function (error) {
       t.is(error.message, 'boom', 'passes error')
 
       t.end()
@@ -137,7 +140,7 @@ test('bundle client', function (group) {
   group.test('browserify.build failing throws error', function (t) {
     var testError = new Error('TestError')
 
-    var bundleClient = proxyquire('../../server/plugins/client/bundle', {
+    var bundleClient = proxyquire('../../../server/plugins/client/bundle', {
       browserify: function () {
         return {
           bundle: simple.stub().callbackWith(testError),
@@ -149,7 +152,7 @@ test('bundle client', function (group) {
       }
     })
 
-    bundleClient('client.js', 'bundle.js', {}, function (error) {
+    bundleClient('client.js', 'bundle.js', {plugins: []}, function (error) {
       t.ok(error)
       t.equal(error, testError)
       t.end()
@@ -162,11 +165,11 @@ test('bundle client', function (group) {
     var unspecifiedError = new Error('UNSPECIFIED_ERROR')
     var savedPath = process.cwd()
     var resolverMock = simple.stub().callFn(function (path) {
-      if (path === pathResolve(__dirname, '../fixture/app-dir-with-server/hoodie/client')) {
-        return pathResolve(__dirname, '../fixture/app-dir-with-server/hoodie/client.js')
+      if (path === pathResolve(__dirname, '../../fixture/app-dir-with-server/hoodie/client')) {
+        return pathResolve(__dirname, '../../fixture/app-dir-with-server/hoodie/client.js')
       }
 
-      if (path === pathResolve(__dirname, '../fixture/hoodie/client')) {
+      if (path === pathResolve(__dirname, '../../fixture/hoodie/client')) {
         throw unspecifiedError
       }
 
@@ -183,7 +186,7 @@ test('bundle client', function (group) {
       readFile: undefined,
       stat: undefined
     }
-    var bundleClient = proxyquire('../../server/plugins/client/bundle', {
+    var bundleClient = proxyquire('../../../server/plugins/client/bundle', {
       browserify: function () {
         return {
           bundle: bundleMock,
@@ -204,7 +207,7 @@ test('bundle client', function (group) {
     t.test('client module exists and is newer', function (t) {
       streamStub.push = simple.stub()
       fsMock.stat = simple.stub().callFn(function (path, callback) {
-        if (path === pathResolve(__dirname, '../fixture/app-dir-with-server/hoodie/client.js')) {
+        if (path === pathResolve(__dirname, '../../fixture/app-dir-with-server/hoodie/client.js')) {
           return callback(null, {mtime: currentDate})
         }
         if (path === 'client.js') {
@@ -214,8 +217,9 @@ test('bundle client', function (group) {
         return callback(null, {mtime: oneHourAgo})
       })
       fsMock.readFile = simple.stub()
-      process.chdir(pathResolve(__dirname, '../fixture/app-dir-with-server/'))
+      process.chdir(pathResolve(__dirname, '../../fixture/app-dir-with-server/'))
       bundleClient('client.js', 'bundle.js', {
+        plugins: [],
         url: 'https://myapp.com'
       }, function (error, buffer) {
         t.error(error)
@@ -229,7 +233,7 @@ test('bundle client', function (group) {
           '  PouchDB: require("pouchdb-browser")\n' +
           '}\n' +
           'module.exports = new Hoodie(options)\n' +
-          '  .plugin(require("' + pathResolve(__dirname, '../fixture/app-dir-with-server/hoodie/client') + '"))\n'
+          '  .plugin(require("' + pathResolve(__dirname, '../../fixture/app-dir-with-server/hoodie/client') + '"))\n'
         t.is(streamStub.push.calls[0].arg, expected)
 
         t.end()
@@ -239,7 +243,7 @@ test('bundle client', function (group) {
     t.test('client module exists and is older', function (t) {
       streamStub.push = simple.stub()
       fsMock.stat = simple.stub().callFn(function (path, callback) {
-        if (path === pathResolve(__dirname, '../fixture/app-dir-with-server/hoodie/client.js')) {
+        if (path === pathResolve(__dirname, '../../fixture/app-dir-with-server/hoodie/client.js')) {
           return callback(null, {mtime: oneHourAgo})
         }
         if (path === 'client.js') {
@@ -249,8 +253,9 @@ test('bundle client', function (group) {
         return callback(null, {mtime: currentDate})
       })
       fsMock.readFile = simple.stub().callbackWith(null, Buffer.from('bundle content'))
-      process.chdir(pathResolve(__dirname, '../fixture/app-dir-with-server/'))
+      process.chdir(pathResolve(__dirname, '../../fixture/app-dir-with-server/'))
       bundleClient('client.js', 'bundle.js', {
+        plugins: [],
         url: 'https://myapp.com'
       }, function (error, buffer) {
         t.error(error)
@@ -266,7 +271,7 @@ test('bundle client', function (group) {
     t.test('client module doesn\'t exist', function (t) {
       streamStub.push = simple.stub()
       fsMock.stat = simple.stub().callFn(function (path, callback) {
-        if (path === pathResolve(__dirname, '../fixture/app-dir-without-server/hoodie/client.js')) {
+        if (path === pathResolve(__dirname, '../../fixture/app-dir-without-server/hoodie/client.js')) {
           throw new Error('Boom')
         }
         if (path === 'client.js') {
@@ -276,8 +281,9 @@ test('bundle client', function (group) {
         callback(new Error('Boom'))
       })
       fsMock.readFile = simple.stub()
-      process.chdir(pathResolve(__dirname, '../fixture/app-dir-without-server/'))
+      process.chdir(pathResolve(__dirname, '../../fixture/app-dir-without-server/'))
       bundleClient('client.js', 'bundle.js', {
+        plugins: [],
         url: 'https://myapp.com'
       }, function (error, buffer) {
         t.error(error)
@@ -299,9 +305,10 @@ test('bundle client', function (group) {
     t.test('require.resolve rethrows other errors', function (t) {
       streamStub.push = simple.stub()
       fsMock.readFile = simple.stub()
-      process.chdir(pathResolve(__dirname, '../fixture/'))
+      process.chdir(pathResolve(__dirname, '../../fixture/'))
       t.throws(function () {
         bundleClient('client.js', 'bundle.js', {
+          plugins: [],
           url: 'https://myapp.com'
         }, function () {
           t.fail()
diff --git a/test/unit/client/client-test.js b/test/unit/client/client-test.js
index d3142653..f815c206 100644
--- a/test/unit/client/client-test.js
+++ b/test/unit/client/client-test.js
@@ -26,25 +26,25 @@ test('client', function (group) {
 
   group.test('always calls next()', function (t) {
     var next = simple.stub()
-    client.register(mockServer, {config: {}}, next)
+    client.register(mockServer, {}, next)
 
     t.is(next.callCount, 1, 'Calls next')
     t.end()
   })
 
-  group.test('passes options.config on to the handlerFactory', function (t) {
-    var configMock = {}
+  group.test('passes options on to the handlerFactory', function (t) {
+    var optionsMock = {}
     var next = simple.stub()
-    client.register(mockServer, {config: configMock}, next)
+    client.register(mockServer, optionsMock, next)
 
     var handlerArgs = createBundleHandlerStub.lastCall.args
 
-    t.equals(handlerArgs[2], configMock)
+    t.equals(handlerArgs[2], optionsMock)
     t.end()
   })
 
-  group.test('builds targetPath from config.data + "client.js"', function (t) {
-    var optionsMock = {config: {data: '/example/path'}}
+  group.test('builds targetPath from options.data + "client.js"', function (t) {
+    var optionsMock = {data: '/example/path'}
     client.register(mockServer, optionsMock, simple.stub())
     var handlerArgs = createBundleHandlerStub.lastCall.args
 
@@ -53,7 +53,7 @@ test('client', function (group) {
   })
 
   group.test('builds targetPath from folder ".hoodie" by default', function (t) {
-    client.register(mockServer, {config: {}}, simple.stub())
+    client.register(mockServer, {}, simple.stub())
 
     var handlerArgs = createBundleHandlerStub.lastCall.args
     t.is(handlerArgs[1], path.join('.hoodie', 'client.js'))
diff --git a/test/unit/server/loader-test.js b/test/unit/server/loader-test.js
deleted file mode 100644
index b03a8586..00000000
--- a/test/unit/server/loader-test.js
+++ /dev/null
@@ -1,46 +0,0 @@
-var simple = require('simple-mock')
-var test = require('tap').test
-var proxyquire = require('proxyquire').noCallThru()
-
-var mockResolver = simple.stub()
-var loader = proxyquire('../../../server/plugins', {
-  './resolver': mockResolver
-})
-var registerPluginsError = new Error('Plugin Register Error')
-var serverMock = {
-  register: simple.stub().callbackWith(registerPluginsError)
-}
-
-test('when require.resolve errors', function (t) {
-  var options = {
-    paths: {
-      public: 'public'
-    }
-  }
-
-  var error = new Error('Unspecified error')
-  mockResolver.throwWith(error)
-
-  t.throws(function () {
-    loader(serverMock, options, function () {
-      t.fail('the error has not been rethrown')
-    })
-  }, error, 'the error is rethrown')
-  t.end()
-})
-
-test('when registerPlugins errors', function (t) {
-  var options = {
-    paths: {
-      public: 'public'
-    }
-  }
-  var error = new Error('Module Not Found Error')
-  error.code = 'MODULE_NOT_FOUND'
-  mockResolver.throwWith(error)
-  loader(serverMock, options, function (error) {
-    t.ok(error)
-    t.equal(error, registerPluginsError)
-    t.end()
-  })
-})
diff --git a/test/unit/server/plugins-test.js b/test/unit/server/plugins-test.js
new file mode 100644
index 00000000..65efa759
--- /dev/null
+++ b/test/unit/server/plugins-test.js
@@ -0,0 +1,158 @@
+var simple = require('simple-mock')
+var test = require('tap').test
+var proxyquire = require('proxyquire').noCallThru()
+
+var mockResolver = simple.stub()
+var registerPlugins = proxyquire('../../../server/plugins', {
+  './resolver': mockResolver,
+  'hoodie-plugin-exists/package.json': {
+    name: 'exists'
+  },
+  'hoodie-plugin-exists/hoodie/server': {},
+  'hoodie-plugin-exists-with-hoodie-option/package.json': {
+    name: 'exists-with-hoodie-option',
+    hoodie: {}
+  },
+  'hoodie-plugin-exists-with-hoodie-option/hoodie/server': {},
+  'hoodie-plugin-exists-with-custom-name/package.json': {
+    name: 'exists-with-hoodie-option',
+    hoodie: {
+      name: 'custom-name'
+    }
+  },
+  'hoodie-plugin-exists-with-custom-name/hoodie/server': {},
+  'hoodie-plugin-exists-with-register/hoodie/server': {
+    register: function () {}
+  },
+  'hoodie-plugin-exists-with-register/package.json': {
+    name: 'exists'
+  }
+})
+var registerPluginsError = new Error('Plugin Register Error')
+var serverMock = {
+  register: simple.stub().callbackWith(registerPluginsError)
+}
+
+test('when require.resolve errors', function (t) {
+  var options = {
+    paths: {
+      public: 'public'
+    },
+    plugins: [
+      'hoodie-plugin-foobar'
+    ]
+  }
+
+  var error = new Error('Unspecified error')
+  mockResolver.throwWith(error)
+
+  t.throws(function () {
+    registerPlugins(serverMock, options, function () {
+      t.fail('the error has not been rethrown')
+    })
+  }, error, 'the error is rethrown')
+  t.end()
+})
+
+test('when requiring plugin succeeds', function (t) {
+  var options = {
+    paths: {
+      public: 'public'
+    },
+    plugins: [
+      'hoodie-plugin-exists'
+    ]
+  }
+
+  mockResolver.callFn(function (path) {
+    return path
+  })
+
+  registerPlugins(serverMock, options, function (error) {
+    t.ok(error)
+    t.equal(error, registerPluginsError)
+    t.end()
+  })
+})
+
+test('when requiring plugin with hoodie', function (t) {
+  var options = {
+    paths: {
+      public: 'public'
+    },
+    plugins: [
+      'hoodie-plugin-exists-with-hoodie-option'
+    ]
+  }
+
+  mockResolver.callFn(function (path) {
+    return path
+  })
+
+  registerPlugins(serverMock, options, function (error) {
+    t.ok(error)
+    t.equal(error, registerPluginsError)
+    t.end()
+  })
+})
+
+test('when requiring plugin with custom name', function (t) {
+  var options = {
+    paths: {
+      public: 'public'
+    },
+    plugins: [
+      'hoodie-plugin-exists-with-custom-name'
+    ]
+  }
+
+  mockResolver.callFn(function (path) {
+    return path
+  })
+
+  registerPlugins(serverMock, options, function (error) {
+    t.ok(error)
+    t.equal(error, registerPluginsError)
+    t.end()
+  })
+})
+
+test('when requiring plugin errors', function (t) {
+  var options = {
+    paths: {
+      public: 'public'
+    },
+    plugins: [
+      'hoodie-plugin-error'
+    ]
+  }
+  var error = new Error('Module Not Found Error')
+  error.code = 'MODULE_NOT_FOUND'
+  mockResolver.throwWith(error)
+  registerPlugins(serverMock, options, function (error) {
+    t.ok(error)
+    t.equal(error, registerPluginsError)
+    t.end()
+  })
+})
+
+test('when requiring plugin which exports {register: plugin}', function (t) {
+  var options = {
+    paths: {
+      public: 'public'
+    },
+    plugins: [
+      'hoodie-plugin-exists-with-register'
+    ]
+  }
+
+  mockResolver.callFn(function (path) {
+    return path
+  })
+
+  registerPlugins(serverMock, options, function (error) {
+    t.ok(error)
+    t.equal(error, registerPluginsError)
+    t.end()
+  })
+})
diff --git a/test/unit/server/public-test.js b/test/unit/server/public-test.js
index 4d9fa763..821b34f9 100644
--- a/test/unit/server/public-test.js
+++ b/test/unit/server/public-test.js
@@ -1,33 +1,139 @@
+var _ = require('lodash')
+var proxyquire = require('proxyquire').noCallThru()
 var simple = require('simple-mock')
 var test = require('tap').test
 
+var serverMock = {
+  register: simple.stub(),
+  route: simple.stub(),
+  ext: simple.stub()
+}
+
+function loadPublicPlugin (options, resolver, done) {
+  var mergedOptions = _.merge({
+    paths: {
+      public: 'my-custom-public-directory'
+    },
+    plugins: []
+  }, options)
+
+  proxyquire('../../../server/plugins/public', {
+    './resolver': resolver,
+    'hoodie-plugin-with-public/package.json': {
+      name: 'plugin-public1'
+    },
+    'hoodie-plugin-with-public-and-custom-name/package.json': {
+      name: 'plugin-public2',
+      hoodie: {
+        name: 'custom-name'
+      }
+    },
+    'hoodie-plugin-with-public-and-hoodie-option/package.json': {
+      name: 'plugin-public3',
+      hoodie: {}
+    }
+  }).register(serverMock, mergedOptions, done)
+}
+
 test('public', function (group) {
-  var serverMock
+  group.test('configured public route', function (t) {
+    function resolver (path) {
+      return path
+    }
+
+    loadPublicPlugin({}, resolver, function (error) {
+      t.error(error)
+      t.equal(serverMock.route.callCount, 1, 'route was called')
+      t.type(serverMock.route.lastCall.arg, Array, 'route was called with an array')
+      t.contains(serverMock.route.lastCall.arg[0],
+        {'handler': {'directory': {'path': /^my-custom-public-directory$/}}},
+        'route was configured to correct public directory')
+      t.end()
+    })
+  })
+
+  group.test('plugin with public path', function (t) {
+    function resolver (path) {
+      return path
+    }
+
+    loadPublicPlugin({
+      plugins: ['hoodie-plugin-with-public']
+    }, resolver, function (error) {
+      t.error(error)
+
+      var pluginRouteOptions = serverMock.route.lastCall.arg.pop()
+      t.is(pluginRouteOptions.path, '/hoodie/plugin-public1/{p*}')
+      t.end()
+    })
+  })
+
+  group.test('plugin with custom name and public path', function (t) {
+    function resolver (path) {
+      return path
+    }
+
+    loadPublicPlugin({
+      plugins: ['hoodie-plugin-with-public-and-custom-name']
+    }, resolver, function (error) {
+      t.error(error)
+
+      var pluginRouteOptions = serverMock.route.lastCall.arg.pop()
+      t.is(pluginRouteOptions.path, '/hoodie/custom-name/{p*}')
+      t.end()
+    })
+  })
 
-  group.beforeEach(function (done) {
-    serverMock = {
-      register: simple.stub(),
-      route: simple.stub(),
-      ext: simple.stub()
+  group.test('plugin with custom name and public path', function (t) {
+    function resolver (path) {
+      return path
     }
 
-    require('../../../server/plugins/public').register(serverMock, {
-      config: {
-        paths: {
-          public: 'my-custom-public-directory'
-        }
+    loadPublicPlugin({
+      plugins: ['hoodie-plugin-with-public-and-hoodie-option']
+    }, resolver, function (error) {
+      t.error(error)
+
+      var pluginRouteOptions = serverMock.route.lastCall.arg.pop()
+      t.is(pluginRouteOptions.path, '/hoodie/plugin-public3/{p*}')
+      t.end()
+    })
+  })
+
+  group.test('plugin without public path', function (t) {
+    var error = new Error('Module Not Found Error')
+    error.code = 'MODULE_NOT_FOUND'
+
+    function resolver (path) {
+      if (/hoodie-plugin-without-public/.test(path)) {
+        throw error
       }
-    }, function () { })
 
-    done()
+      return path
+    }
+
+    loadPublicPlugin({
+      plugins: ['hoodie-plugin-without-public']
+    }, resolver, function (error) {
+      t.error(error)
+      t.end()
+    })
   })
 
-  group.test('configured public route', function (t) {
-    t.equal(serverMock.route.callCount, 1, 'route was called')
-    t.type(serverMock.route.lastCall.args[0], Array, 'route was called with an array')
-    t.contains(serverMock.route.lastCall.args[0][0],
-      {'handler': {'directory': {'path': /^my-custom-public-directory$/}}},
-      'route was configured to correct public directory')
+  group.test('require error', function (t) {
+    function resolver (path) {
+      if (/hoodie-plugin-foobar/.test(path)) {
+        throw new Error('Oops')
+      }
+
+      return path
+    }
+
+    t.throws(function () {
+      loadPublicPlugin({
+        plugins: ['hoodie-plugin-foobar']
+      }, resolver, function () {})
+    })
     t.end()
   })