diff --git a/assets/scripts/app/app.coffee b/assets/scripts/app/app.coffee
index f866d741..9e0d17cd 100644
--- a/assets/scripts/app/app.coffee
+++ b/assets/scripts/app/app.coffee
@@ -22,10 +22,12 @@ unless window.TravisApplication
       annotations:  Travis.Annotation
       request:      Travis.Request
       requests:     Travis.Request
+      env_var:      Travis.EnvVar
+      env_vars:     Travis.EnvVar
     ).property()
 
     modelClasses: (->
-      [Travis.User, Travis.Build, Travis.Job, Travis.Repo, Travis.Commit, Travis.Worker, Travis.Account, Travis.Broadcast, Travis.Hook, Travis.Annotation, Travis.Request]
+      [Travis.User, Travis.Build, Travis.Job, Travis.Repo, Travis.Commit, Travis.Worker, Travis.Account, Travis.Broadcast, Travis.Hook, Travis.Annotation, Travis.Request, Travis.EnvVar]
     ).property()
 
     setup: ->
@@ -33,6 +35,9 @@ unless window.TravisApplication
         klass.adapter = Travis.Adapter.create()
         klass.url = "/#{klass.pluralName()}"
 
+      Travis.EnvVar.url = "/settings/env_vars"
+      Travis.EnvVar.adapter = Travis.EnvVarsAdapter.create()
+
       @slider = new Travis.Slider()
       @pusher = new Travis.Pusher(Travis.config.pusher_key) if Travis.config.pusher_key
       @tailing = new Travis.Tailing($(window), '#tail', '#log')
diff --git a/assets/scripts/app/controllers.coffee b/assets/scripts/app/controllers.coffee
index 46e535a8..41b312bc 100644
--- a/assets/scripts/app/controllers.coffee
+++ b/assets/scripts/app/controllers.coffee
@@ -66,3 +66,6 @@ require 'controllers/stats'
 require 'controllers/current_user'
 require 'controllers/request'
 require 'controllers/requests'
+require 'controllers/env_var'
+require 'controllers/env_vars'
+require 'controllers/env_var_new'
diff --git a/assets/scripts/app/controllers/env_var.coffee b/assets/scripts/app/controllers/env_var.coffee
new file mode 100644
index 00000000..f2c55777
--- /dev/null
+++ b/assets/scripts/app/controllers/env_var.coffee
@@ -0,0 +1,30 @@
+Travis.EnvVarController = Ember.ObjectController.extend
+  isEditing: false
+
+  value: ( (key, value) ->
+    if arguments.length == 2
+      @get('model').set('value', value)
+      value
+    else if @get('public')
+      @get('model.value')
+    else
+      '****************'
+  ).property('model.value')
+
+  actions:
+    delete: ->
+      @get('model').deleteRecord()
+
+    edit: ->
+      @set('isEditing', true)
+
+    cancel: ->
+      @set('isEditing', false)
+      @get('model').revert()
+
+    save: ->
+      env_var = @get('model')
+
+      # TODO: handle errors
+      env_var.save().then =>
+        @set('isEditing', false)
diff --git a/assets/scripts/app/controllers/env_var_new.coffee b/assets/scripts/app/controllers/env_var_new.coffee
new file mode 100644
index 00000000..b65c3543
--- /dev/null
+++ b/assets/scripts/app/controllers/env_var_new.coffee
@@ -0,0 +1,20 @@
+Travis.EnvVarsNewController = Travis.Controller.extend
+  needs: ['repoSettings']
+  repo: Ember.computed.alias('controllers.repoSettings.model')
+
+  actions:
+    cancel: ->
+      @transitionToRoute('env_vars')
+
+    save: ->
+      console.log(@get('repo.id'))
+      env_var = Travis.EnvVar.create(
+        name: @get('name')
+        value: @get('value')
+        public: @get('public')
+        repo: @get('repo')
+      )
+
+      self = this
+      env_var.save().then ->
+        self.transitionToRoute('env_vars')
diff --git a/assets/scripts/app/controllers/env_vars.coffee b/assets/scripts/app/controllers/env_vars.coffee
new file mode 100644
index 00000000..5c6d4d65
--- /dev/null
+++ b/assets/scripts/app/controllers/env_vars.coffee
@@ -0,0 +1 @@
+Travis.EnvVarsController = Ember.ArrayController.extend()
diff --git a/assets/scripts/app/models.coffee b/assets/scripts/app/models.coffee
index c2efe38d..93fa69b4 100644
--- a/assets/scripts/app/models.coffee
+++ b/assets/scripts/app/models.coffee
@@ -13,4 +13,4 @@ require 'models/repo'
 require 'models/request'
 require 'models/user'
 require 'models/worker'
-
+require 'models/env_var'
diff --git a/assets/scripts/app/models/env_var.coffee b/assets/scripts/app/models/env_var.coffee
new file mode 100644
index 00000000..5ddd444c
--- /dev/null
+++ b/assets/scripts/app/models/env_var.coffee
@@ -0,0 +1,16 @@
+require 'travis/model'
+
+Travis.EnvVar = Travis.Model.extend
+  name: Ember.attr('string')
+  value: Ember.attr('string')
+  public: Ember.attr('boolean')
+
+  repo: Ember.belongsTo('Travis.Repo', key: 'repository_id')
+
+  isPropertyLoaded: (key) ->
+    if key == 'value'
+      return true
+    else
+      @_super(key)
+
+
diff --git a/assets/scripts/app/models/repo.coffee b/assets/scripts/app/models/repo.coffee
index 3f94fc93..ce3bc3b8 100644
--- a/assets/scripts/app/models/repo.coffee
+++ b/assets/scripts/app/models/repo.coffee
@@ -23,6 +23,26 @@ require 'travis/model'
     }
   ).property('lastBuildId', 'lastBuildNumber')
 
+  envVars: (->
+    id = @get('id')
+    envVars = Travis.EnvVar.find repository_id: id
+
+    # TODO: move to controller
+    array  = Travis.ExpandableRecordArray.create
+      type: Travis.EnvVar
+      content: Ember.A([])
+
+    array.load(envVars)
+
+    globalEnvVars = Ember.RecordArray.create({ modelClass: Travis.EnvVar, content: Ember.A([]) })
+    Travis.EnvVar.registerRecordArray(globalEnvVars)
+
+    array.observe(globalEnvVars, (envVar) -> envVar.get('isLoaded') && envVar.get('repo.id') == id )
+
+    array
+  ).property()
+
+
   allBuilds: (->
     recordArray = Ember.RecordArray.create({ modelClass: Travis.Build, content: Ember.A([]) })
     Travis.Build.registerRecordArray(recordArray)
diff --git a/assets/scripts/app/routes.coffee b/assets/scripts/app/routes.coffee
index c0f35df4..d1aee059 100644
--- a/assets/scripts/app/routes.coffee
+++ b/assets/scripts/app/routes.coffee
@@ -65,6 +65,9 @@ Travis.Router.map ->
     # templates rendered for settings (for example no "current", "builds", ... tabs)
     @resource 'repo.settings', path: '/:owner/:name/settings', ->
       @route 'index', path: '/'
+      @resource 'env_vars', ->
+        @route 'new'
+      @route 'ssh_key'
 
   @route 'first_sync'
   @route 'insufficient_oauth_permissions'
@@ -361,3 +364,8 @@ Travis.RepoSettingsIndexRoute = Travis.Route.extend
     repo = @modelFor('repo_settings')
     repo.fetchSettings().then (settings) ->
       repo.set('settings', settings)
+
+Travis.EnvVarsRoute = Travis.Route.extend
+  model: (params) ->
+    repo = @modelFor('repo_settings')
+    repo.get('envVars')
diff --git a/assets/scripts/app/templates/env_vars.hbs b/assets/scripts/app/templates/env_vars.hbs
new file mode 100644
index 00000000..aba20052
--- /dev/null
+++ b/assets/scripts/app/templates/env_vars.hbs
@@ -0,0 +1,27 @@
+<div>
+  {{outlet}}
+</div>
+
+{{#link-to "env_vars.new"}}Add a new variable{{/link-to}}
+
+{{#each controller itemController="envVar"}}
+  {{#if isEditing}}
+    {{! TODO: reuse it with "new", it's almost identical }}
+    <form {{action "save" on="submit"}}>
+      <p>Name: {{input value=name}}</p>
+      {{#if public}}<p>Value: {{input value=value}}</p>{{/if}}
+      <p>Public: {{input type="checkbox" checked=public}}</p>
+
+      <p>
+        {{input type="submit" value="Save"}} or <a href="#" {{action "cancel"}}>Cancel</a>
+      </p>
+    </form>
+  {{else}}
+    <p>
+      Name: {{name}}<br/>
+      Value: {{value}}<br/>
+      <a href="#" {{action "edit"}}>Edit</a>
+      <a href="#" {{action "delete"}}>Delete</a>
+    </p>
+  {{/if}}
+{{/each}}
diff --git a/assets/scripts/app/templates/env_vars/new.hbs b/assets/scripts/app/templates/env_vars/new.hbs
new file mode 100644
index 00000000..b6b64934
--- /dev/null
+++ b/assets/scripts/app/templates/env_vars/new.hbs
@@ -0,0 +1,9 @@
+<form {{action "save" on="submit"}}>
+  <p>Name: {{input value=name}}</p>
+  <p>Value: {{input value=value}}</p>
+  <p>Public: {{input type="checkbox" checked=public}}</p>
+
+  <p>
+    {{input type="submit" value="Add"}} or <a href="#" {{action "cancel"}}>Cancel</a>
+  </p>
+</form>
diff --git a/assets/scripts/app/templates/repo/settings/ssh_key.hbs b/assets/scripts/app/templates/repo/settings/ssh_key.hbs
new file mode 100644
index 00000000..257cc564
--- /dev/null
+++ b/assets/scripts/app/templates/repo/settings/ssh_key.hbs
@@ -0,0 +1 @@
+foo
diff --git a/assets/scripts/lib/travis/adapter.coffee b/assets/scripts/lib/travis/adapter.coffee
index 95aae5aa..3ea2e657 100644
--- a/assets/scripts/lib/travis/adapter.coffee
+++ b/assets/scripts/lib/travis/adapter.coffee
@@ -88,3 +88,14 @@ Travis.Adapter = Ember.RESTAdapter.extend
     @ajax(url, record.toJSON(), "DELETE").then (data) -> # TODO: Some APIs may or may not return data
       self.didDeleteRecord record, data
       return
+
+  saveRecord: (record) ->
+    primaryKey = get(record.constructor, 'primaryKey')
+    url = this.buildURL(record.constructor, get(record, primaryKey), record)
+    self = this
+
+    return this.ajax(url, record.toJSON(), "PATCH").then (data) ->
+      self.didSaveRecord(record, data)
+      return record
+
+
diff --git a/assets/scripts/lib/travis/adapters/env_vars.coffee b/assets/scripts/lib/travis/adapters/env_vars.coffee
new file mode 100644
index 00000000..00eba0df
--- /dev/null
+++ b/assets/scripts/lib/travis/adapters/env_vars.coffee
@@ -0,0 +1,10 @@
+require 'travis/adapter'
+get = Ember.get
+
+Travis.EnvVarsAdapter = Travis.Adapter.extend
+  buildURL: (klass, id, record) ->
+    url = @_super.apply this, arguments
+    if record && (repo_id = get(record, 'repository_id') || get(record, 'repo.id'))
+      url = "#{url}?repository_id=#{repo_id}"
+
+    url
diff --git a/assets/scripts/lib/travis/expandable_record_array.coffee b/assets/scripts/lib/travis/expandable_record_array.coffee
index 778b9f6b..c19a6043 100644
--- a/assets/scripts/lib/travis/expandable_record_array.coffee
+++ b/assets/scripts/lib/travis/expandable_record_array.coffee
@@ -25,11 +25,17 @@ Travis.ExpandableRecordArray = Ember.RecordArray.extend
       willChange: 'observedArrayWillChange'
       didChange: 'observedArraydidChange'
 
-  observedArrayWillChange: (->)
+  observedArrayWillChange: (array, index, removedCount, addedCount) ->
+    removedObjects = array.slice index, index + removedCount
+    for object in removedObjects
+      @removeObject(object)
+
   observedArraydidChange: (array, index, removedCount, addedCount) ->
     addedObjects = array.slice index, index + addedCount
     for object in addedObjects
-      if @get('filterWith').call this, object
+      # TODO: I'm not sure why deleted objects get here, but I'll just filter them
+      # for now
+      if !object.get('isDeleted') && @get('filterWith').call(this, object)
         @pushObject(object) unless @contains(object)
 
   pushObject: (record) ->
diff --git a/assets/scripts/travis.coffee b/assets/scripts/travis.coffee
index f209166a..a7e68b46 100644
--- a/assets/scripts/travis.coffee
+++ b/assets/scripts/travis.coffee
@@ -162,6 +162,7 @@ Ember.LinkView.reopen
 
 require 'travis/ajax'
 require 'travis/adapter'
+require 'travis/adapters/env_vars'
 require 'routes'
 require 'auth'
 require 'controllers'