diff --git a/.gitignore b/.gitignore
index 9fad0c2..bff8718 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,4 @@
node_modules
checkstyle.xml
loopback-boot-*.tgz
+/test/sandbox/
diff --git a/.jshintignore b/.jshintignore
index 25fbf5a..d8f8557 100644
--- a/.jshintignore
+++ b/.jshintignore
@@ -1,2 +1,3 @@
node_modules/
coverage/
+test/sandbox/
diff --git a/CHANGES.md b/CHANGES.md
new file mode 100644
index 0000000..c62e362
--- /dev/null
+++ b/CHANGES.md
@@ -0,0 +1,14 @@
+## Changes in version 1.0
+
+ - New options: `modelsRootDir`, `dsRootDir`
+
+ - Load configuration from files, support dynamic (scripted) options
+
+ ```sh
+ app.json, app.local.*, app.{env}.*
+ datasources.json, datasources.local.*, datasources.{env}.*
+ ```
+
+ - Scripts in `models/` and `boot/` can export `function(app)`,
+ this function is then called by the bootstrapper. The existing code
+ using `var app = require('../app')` will continue to work.
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..2b309e7
--- /dev/null
+++ b/index.js
@@ -0,0 +1,313 @@
+var assert = require('assert');
+var fs = require('fs');
+var path = require('path');
+var _ = require('underscore');
+var loopback = require('loopback');
+var ConfigLoader = require('./lib/config-loader');
+var debug = require('debug')('loopback:boot');
+
+/**
+ * Initialize an application from an options object or
+ * a set of JSON and JavaScript files.
+ *
+ * This function takes an optional argument that is either a string
+ * or an object.
+ *
+ * If the argument is a string, then it sets the application root directory
+ * based on the string value. Then it:
+ *
+ * 1. Creates DataSources from the `datasources.json` file in the application
+ * root directory.
+ *
+ * 2. Creates Models from the `models.json` file in the application
+ * root directory.
+ *
+ * If the argument is an object, then it looks for `model`, `dataSources`,
+ * and `appRootDir` properties of the object.
+ * If the object has no `appRootDir` property then it sets the current working
+ * directory as the application root directory.
+ * Then it:
+ *
+ * 1. Creates DataSources from the `options.dataSources` object.
+ *
+ * 2. Creates Models from the `options.models` object.
+ *
+ * In both cases, the function loads JavaScript files in the `/models` and
+ * `/boot` subdirectories of the application root directory with `require()`.
+ *
+ * **NOTE:** mixing `app.boot()` and `app.model(name, config)` in multiple
+ * files may result in models being **undefined** due to race conditions.
+ * To avoid this when using `app.boot()` make sure all models are passed
+ * as part of the `models` definition.
+ *
+ * Throws an error if the config object is not valid or if boot fails.
+ *
+ *
+ * **Model Definitions**
+ *
+ * The following is example JSON for two `Model` definitions:
+ * "dealership" and "location".
+ *
+ * ```js
+ * {
+ * "dealership": {
+ * // a reference, by name, to a dataSource definition
+ * "dataSource": "my-db",
+ * // the options passed to Model.extend(name, properties, options)
+ * "options": {
+ * "relations": {
+ * "cars": {
+ * "type": "hasMany",
+ * "model": "Car",
+ * "foreignKey": "dealerId"
+ * }
+ * }
+ * },
+ * // the properties passed to Model.extend(name, properties, options)
+ * "properties": {
+ * "id": {"id": true},
+ * "name": "String",
+ * "zip": "Number",
+ * "address": "String"
+ * }
+ * },
+ * "car": {
+ * "dataSource": "my-db"
+ * "properties": {
+ * "id": {
+ * "type": "String",
+ * "required": true,
+ * "id": true
+ * },
+ * "make": {
+ * "type": "String",
+ * "required": true
+ * },
+ * "model": {
+ * "type": "String",
+ * "required": true
+ * }
+ * }
+ * }
+ * }
+ * ```
+ *
+ * @param app LoopBack application created by `loopback()`.
+ * @options {String|Object} options Boot options; If String, this is
+ * the application root directory; if object, has below properties.
+ * @property {String} appRootDir Directory to use when loading JSON and
+ * JavaScript files (optional).
+ * Defaults to the current directory (`process.cwd()`).
+ * @property {Object} models Object containing `Model` definitions (optional).
+ * @property {Object} dataSources Object containing `DataSource`
+ * definitions (optional).
+ * @property {String} modelsRootDir Directory to use when loading `models.json`
+ * and `models/*.js`. Defaults to `appRootDir`.
+ * @property {String} datasourcesRootDir Directory to use when loading
+ * `datasources.json`. Defaults to `appRootDir`.
+ * @end
+ *
+ * @header boot(app, [options])
+ */
+
+exports = module.exports = function bootLoopBackApp(app, options) {
+ /*jshint camelcase:false */
+ options = options || {};
+
+ if(typeof options === 'string') {
+ options = { appRootDir: options };
+ }
+ var appRootDir = options.appRootDir = options.appRootDir || process.cwd();
+ var env = app.get('env');
+
+ var appConfig = options.app || ConfigLoader.loadAppConfig(appRootDir, env);
+
+ var modelsRootDir = options.modelsRootDir || appRootDir;
+ var modelConfig = options.models ||
+ ConfigLoader.loadModels(modelsRootDir, env);
+
+ var dsRootDir = options.dsRootDir || appRootDir;
+ var dataSourceConfig = options.dataSources ||
+ ConfigLoader.loadDataSources(dsRootDir, env);
+
+ assertIsValidConfig('app', appConfig);
+ assertIsValidConfig('model', modelConfig);
+ assertIsValidConfig('data source', dataSourceConfig);
+
+ appConfig.host =
+ process.env.npm_config_host ||
+ process.env.OPENSHIFT_SLS_IP ||
+ process.env.OPENSHIFT_NODEJS_IP ||
+ process.env.HOST ||
+ appConfig.host ||
+ process.env.npm_package_config_host ||
+ app.get('host');
+
+ appConfig.port = _.find([
+ process.env.npm_config_port,
+ process.env.OPENSHIFT_SLS_PORT,
+ process.env.OPENSHIFT_NODEJS_PORT,
+ process.env.PORT,
+ appConfig.port,
+ process.env.npm_package_config_port,
+ app.get('port'),
+ 3000
+ ], _.isFinite);
+
+ appConfig.restApiRoot =
+ appConfig.restApiRoot ||
+ app.get('restApiRoot') ||
+ '/api';
+
+ if(appConfig.host !== undefined) {
+ assert(typeof appConfig.host === 'string', 'app.host must be a string');
+ app.set('host', appConfig.host);
+ }
+
+ if(appConfig.port !== undefined) {
+ var portType = typeof appConfig.port;
+ assert(portType === 'string' || portType === 'number',
+ 'app.port must be a string or number');
+ app.set('port', appConfig.port);
+ }
+
+ assert(appConfig.restApiRoot !== undefined, 'app.restBasePath is required');
+ assert(typeof appConfig.restApiRoot === 'string',
+ 'app.restBasePath must be a string');
+ assert(/^\//.test(appConfig.restApiRoot),
+ 'app.restBasePath must start with "/"');
+ app.set('restApiRoot', appConfig.restBasePath);
+
+ for(var configKey in appConfig) {
+ var cur = app.get(configKey);
+ if(cur === undefined || cur === null) {
+ app.set(configKey, appConfig[configKey]);
+ }
+ }
+
+ // instantiate data sources
+ forEachKeyedObject(dataSourceConfig, function(key, obj) {
+ app.dataSource(key, obj);
+ });
+
+ // instantiate models
+ forEachKeyedObject(modelConfig, function(key, obj) {
+ app.model(key, obj);
+ });
+
+ // try to attach models to dataSources by type
+ try {
+ loopback.autoAttach();
+ } catch(e) {
+ if(e.name === 'AssertionError') {
+ console.warn(e);
+ } else {
+ throw e;
+ }
+ }
+
+ // disable token requirement for swagger, if available
+ var swagger = app.remotes().exports.swagger;
+ var requireTokenForSwagger = appConfig.swagger &&
+ appConfig.swagger.requireToken;
+ if(swagger) {
+ swagger.requireToken = requireTokenForSwagger || false;
+ }
+
+ // require directories
+ requireDir(path.join(modelsRootDir, 'models'), app);
+ requireDir(path.join(appRootDir, 'boot'), app);
+};
+
+function assertIsValidConfig(name, config) {
+ if(config) {
+ assert(typeof config === 'object',
+ name + ' config must be a valid JSON object');
+ }
+}
+
+function forEachKeyedObject(obj, fn) {
+ if(typeof obj !== 'object') return;
+
+ Object.keys(obj).forEach(function(key) {
+ fn(key, obj[key]);
+ });
+}
+
+function requireDir(dir, app) {
+ assert(dir, 'cannot require directory contents without directory name');
+
+ var requires = {};
+
+ // require all javascript files (except for those prefixed with _)
+ // and all directories
+
+ var files = tryReadDir(dir);
+
+ // sort files in lowercase alpha for linux
+ files.sort(function(a, b) {
+ a = a.toLowerCase();
+ b = b.toLowerCase();
+
+ if (a < b) {
+ return -1;
+ } else if (b < a) {
+ return 1;
+ } else {
+ return 0;
+ }
+ });
+
+ files.forEach(function(filename) {
+ // ignore index.js and files prefixed with underscore
+ if ((filename === 'index.js') || (filename[0] === '_')) {
+ return;
+ }
+
+ var filepath = path.resolve(path.join(dir, filename));
+ var ext = path.extname(filename);
+ var stats = fs.statSync(filepath);
+
+ // only require files supported by require.extensions (.txt .md etc.)
+ if (stats.isFile() && !(ext in require.extensions)) {
+ return;
+ }
+
+ var exports = tryRequire(filepath);
+ if (isFunctionNotModelCtor(exports))
+ exports(app);
+
+ var basename = path.basename(filename, ext);
+ requires[basename] = exports;
+ });
+
+ return requires;
+}
+
+function tryRequire(modulePath) {
+ try {
+ return require.apply(this, arguments);
+ } catch(e) {
+ if(e.code === 'MODULE_NOT_FOUND') {
+ debug('Warning: cannot require %s - module not found.', modulePath);
+ return undefined;
+ }
+ console.error('failed to require "%s"', modulePath);
+ throw e;
+ }
+}
+
+function tryReadDir() {
+ try {
+ return fs.readdirSync.apply(fs, arguments);
+ } catch(e) {
+ return [];
+ }
+}
+
+function isFunctionNotModelCtor(fn) {
+ return typeof fn === 'function' &&
+ !(fn.prototype instanceof loopback.Model);
+}
+
+exports.ConfigLoader = ConfigLoader;
diff --git a/lib/config-loader.js b/lib/config-loader.js
new file mode 100644
index 0000000..7cafb35
--- /dev/null
+++ b/lib/config-loader.js
@@ -0,0 +1,154 @@
+var fs = require('fs');
+var path = require('path');
+
+var ConfigLoader = exports;
+
+/**
+ * Load application config from `app.json` and friends.
+ * @param {String} rootDir Directory where to look for files.
+ * @param {String} env Environment, usually `process.env.NODE_ENV`
+ * @returns {Object}
+ */
+ConfigLoader.loadAppConfig = function(rootDir, env) {
+ return loadNamed(rootDir, env, 'app', mergeAppConfig);
+};
+
+/**
+ * Load data-sources config from `datasources.json` and friends.
+ * @param {String} rootDir Directory where to look for files.
+ * @param {String} env Environment, usually `process.env.NODE_ENV`
+ * @returns {Object}
+ */
+ConfigLoader.loadDataSources = function(rootDir, env) {
+ return loadNamed(rootDir, env, 'datasources', mergeDataSourceConfig);
+};
+
+/**
+ * Load models config from `models.json` and friends.
+ * @param {String} rootDir Directory where to look for files.
+ * @param {String} env Environment, usually `process.env.NODE_ENV`
+ * @returns {Object}
+ */
+ConfigLoader.loadModels = function(rootDir, env) {
+ /*jshint unused:false */
+ return tryReadJsonConfig(rootDir, 'models') || {};
+};
+
+/*-- Implementation --*/
+
+/**
+ * Load named configuration.
+ * @param {String} rootDir Directory where to look for files.
+ * @param {String} env Environment, usually `process.env.NODE_ENV`
+ * @param {String} name
+ * @param {function(target:Object, config:Object, filename:String)} mergeFn
+ * @returns {Object}
+ */
+function loadNamed(rootDir, env, name, mergeFn) {
+ var files = findConfigFiles(rootDir, env, name);
+ var configs = loadConfigFiles(files);
+ return mergeConfigurations(configs, mergeFn);
+}
+
+/**
+ * Search `appRootDir` for all files containing configuration for `name`.
+ * @param {String} appRootDir
+ * @param {String} env Environment, usually `process.env.NODE_ENV`
+ * @param {String} name
+ * @returns {Array.} Array of absolute file paths.
+ */
+function findConfigFiles(appRootDir, env, name) {
+ var master = ifExists(name + '.json');
+ if (!master) return [];
+
+ var candidates = [
+ master,
+ ifExistsWithAnyExt(name + '.local'),
+ ifExistsWithAnyExt(name + '.' + env)
+ ];
+
+ return candidates.filter(function(c) { return c !== undefined; });
+
+ function ifExists(fileName) {
+ var filepath = path.resolve(appRootDir, fileName);
+ return fs.existsSync(filepath) ? filepath : undefined;
+ }
+
+ function ifExistsWithAnyExt(fileName) {
+ return ifExists(fileName + '.js') || ifExists(fileName + '.json');
+ }
+}
+
+/**
+ * Load configuration files into an array of objects.
+ * Attach non-enumerable `_filename` property to each object.
+ * @param {Array.} files
+ * @returns {Array.