diff --git a/app/lib/auth.coffee b/app/lib/auth.coffee
index d46733089..4fa94e61f 100644
--- a/app/lib/auth.coffee
+++ b/app/lib/auth.coffee
@@ -11,7 +11,6 @@ init = ->
     me.set 'testGroupNumber', Math.floor(Math.random() * 256)
     me.save()
 
-  me.loadGravatarProfile() if me.get('email')
   Backbone.listenTo(me, 'sync', Backbone.Mediator.publish('me:synced', {me:me}))
 
 module.exports.createUser = (userObject, failure=backboneFailure, nextURL=null) ->
@@ -52,4 +51,3 @@ trackFirstArrival = ->
   storage.save(BEEN_HERE_BEFORE_KEY, true)
 
 init()
-
diff --git a/app/locale/en.coffee b/app/locale/en.coffee
index cd6367464..b963b86e3 100644
--- a/app/locale/en.coffee
+++ b/app/locale/en.coffee
@@ -142,9 +142,6 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr
     password_tab: "Password"
     emails_tab: "Emails"
     admin: "Admin"
-    gravatar_select: "Select which Gravatar photo to use"
-    gravatar_add_photos: "Add thumbnails and photos to a Gravatar account for your email to choose an image."
-    gravatar_add_more_photos: "Add more photos to your Gravatar account to access them here."
     wizard_color: "Wizard Clothes Color"
     new_password: "New Password"
     new_password_verify: "Verify"
@@ -166,17 +163,6 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr
     edit_settings: "Edit Settings"
     profile_for_prefix: "Profile for "
     profile_for_suffix: ""
-    profile: "Profile"
-    user_not_found: "No user found. Check the URL?"
-    gravatar_not_found_mine: "We couldn't find your profile associated with:"
-    gravatar_not_found_email_suffix: "."
-    gravatar_signup_prefix: "Sign up at "
-    gravatar_signup_suffix: " to get set up!"
-    gravatar_not_found_other: "Alas, there's no profile associated with this person's email address."
-    gravatar_contact: "Contact"
-    gravatar_websites: "Websites"
-    gravatar_accounts: "As Seen On"
-    gravatar_profile_link: "Full Gravatar Profile"
 
   play_level:
     level_load_error: "Level could not be loaded: "
@@ -628,3 +614,4 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr
     gplus_friend_sessions: "G+ Friend Sessions"
     leaderboard: "Leaderboard"
     user_schema: "User Schema"
+    user_profile: "User Profile"
diff --git a/app/models/User.coffee b/app/models/User.coffee
index c3bf71146..d81eae86e 100644
--- a/app/models/User.coffee
+++ b/app/models/User.coffee
@@ -8,53 +8,23 @@ module.exports = class User extends CocoModel
 
   initialize: ->
     super()
-    @on 'change:emailHash', ->
-      @gravatarProfile = null
-      @loadGravatarProfile()
 
   isAdmin: ->
     permissions = @attributes['permissions'] or []
     return 'admin' in permissions
 
-  gravatarAvatarURL: ->
-    avatar_url = GRAVATAR_URL + 'avatar/'
-    return avatar_url if not @emailHash
-    return avatar_url + @emailHash
-
-  loadGravatarProfile: ->
-    emailHash = @get('emailHash')
-    return if not emailHash
-    functionName = 'gotProfile'+emailHash
-    profileUrl = "#{GRAVATAR_URL}#{emailHash}.json?callback=#{functionName}"
-    script = $("<script src='#{profileUrl}' type='text/javascript'></script>")
-    $('head').append(script)
-    window[functionName] = (profile) =>
-      @gravatarProfile = profile
-      @trigger('change', @)
-
-    func = => @gravatarProfile = null unless @gravatarProfile
-    setTimeout(func, 1000)
-
   displayName: ->
-    @get('name') or @gravatarName() or "Anoner"
+    @get('name') or "Anoner"
 
   lang: ->
     @get('preferredLanguage') or "en-US"
 
-  gravatarName: ->
-    @gravatarProfile?.entry[0]?.name?.formatted or ''
-
-  gravatarPhotoURLs: ->
-    photos = @gravatarProfile?.entry[0]?.photos
-    return if not photos
-    (photo.value for photo in photos)
-
-  getPhotoURL: ->
-    photoURL = @get('photoURL')
-    validURLs = @gravatarPhotoURLs()
-    return @gravatarAvatarURL() unless validURLs and validURLs.length
-    return validURLs[0] unless photoURL in validURLs
-    return photoURL
+  getPhotoURL: (size=80) ->
+    if photoURL = @get('photoURL')
+      prefix = if photoURL.search(/\?/) is -1 then "?" else "&"
+      return "#{photoURL}#{prefix}s=#{size}" if photoURL.search('http') isnt -1  # legacy
+      return "/file/#{photoURL}#{prefix}s=#{size}"
+    return "/db/user/#{@id}/avatar?s=#{size}"
 
   @getByID = (id, properties, force) ->
     {me} = require('lib/auth')
@@ -66,7 +36,8 @@ module.exports = class User extends CocoModel
         success: ->
           user.loading = false
           Backbone.Mediator.publish('user:fetched')
-          user.loadGravatarProfile()
+          console.log 'triggering sync'
+          user.trigger 'sync'
       )
     cache[id] = user
     user
diff --git a/app/styles/account/profile.sass b/app/styles/account/profile.sass
index d54e0e834..7ca283ea6 100644
--- a/app/styles/account/profile.sass
+++ b/app/styles/account/profile.sass
@@ -8,10 +8,6 @@
       margin: 2px
       i
         margin-right: 5px
-
-  
-  img.img-thumbnail
-    margin: 5px 20px 20px 20px
     
   .approved, .not-approved
     display: none
@@ -27,6 +23,13 @@
       border-radius: 0
       padding: 10px
 
+    .public-profile-container
+      padding: 20px
+
+      img.profile-photo
+        width: 256px
+        border-radius: 6px
+
     .job-profile-container
       width: 100%
       height: 100%
@@ -112,6 +115,7 @@
             margin-top: 10px
             img
               max-width: 524px - 60px
+              max-height: 200px
 
           .header-icon
             margin-right: 10px
diff --git a/app/styles/account/settings.sass b/app/styles/account/settings.sass
index 7c41cf895..6d033d98a 100644
--- a/app/styles/account/settings.sass
+++ b/app/styles/account/settings.sass
@@ -11,12 +11,8 @@
   #save-button
     float: right
 
-  .thumbnails
-    text-align: center
-    .thumbnail
-      margin-bottom: 30px
-      margin-right: 20px
-      float: left
+  .gravatar-fallback
+    margin-top: 10px
       
   input.range
     position: relative
diff --git a/app/templates/account/profile.jade b/app/templates/account/profile.jade
index 3d63858ba..837aa703e 100644
--- a/app/templates/account/profile.jade
+++ b/app/templates/account/profile.jade
@@ -21,7 +21,7 @@ block content
       .job-profile-row
         .left-column.full-height-column
           .profile-photo-container
-            img.profile-photo(src=photoURL)
+            img.profile-photo(src=user.getPhotoURL(240))
             .profile-caption= profile.jobTitle || 'Software Developer'
 
           if profileLinks.length
@@ -89,65 +89,13 @@ block content
                   a(href=project.link).btn.btn-large.btn-inverse.flat-button Check it out
 
   else
-    h2 
-      if grav && grav.name && grav.name.formatted
+    .public-profile-container
+      h2 
         span(data-i18n="account_profile.profile_for_prefix") Profile for 
-        span= grav.name.formatted
+        span= user.get('name')
         span(data-i18n="account_profile.profile_for_suffix") 
-      else
-        span(data-i18n="account_profile.profile") Profile
   
-    if loadingProfile
-      p(data-i18n="common.loading") Loading...
-
-    else if !user.get('emailHash')
-      p(data-i18n="account_profile.user_not_found") No user found. Check the URL?
-
-    else if !user.gravatarProfile
-      if myProfile
-        p
-          span(data-i18n="account_profile.gravatar_not_found_mine") We couldn't find your profile associated with:
-          strong  "#{me.get('email')}"
-          span(data-i18n="account_profile.gravatar_not_found_email_suffix") .
-          span  
-          span(data-i18n="account_profile.gravatar_signup_prefix") Sign up at 
-          a(href="http://en.gravatar.com/") Gravatar
-          span(data-i18n="account_profile.gravatar_signup_suffix")  to get set up!
-      else
-          p(data-i18n="account_profile.gravatar_not_found_other")
-            | Alas, there's no profile associated with this person's email address.
-
-    else
-      .container
-        div.row
-          div.col-xs-3
-            img(src=photoURL).img-thumbnail
-            
-            p.about-me #{grav.aboutMe}
-    
-          if grav.emails
-            div.col-xs-3
-              h3(data-i18n="account_profile.gravatar_contact") Contact
-              ul
-                each email in grav.emails
-                  li #{email.value}
-                    
-          if grav.urls && grav.urls.length
-            div.col-xs-3
-              h3(data-i18n="account_profile.gravatar_websites") Websites
-              ul
-                each url in grav.urls
-                  li
-                    a(href="#{url.value}") #{url.title} 
-                                  
-          if grav.accounts
-            div.col-xs-3
-              h3(data-i18n="account_profile.gravatar_accounts") As Seen On
-              ul
-                each account in grav.accounts
-                  li
-                    a(href="#{account.url}") #{account.domain}
-       
-      hr
-      p
-        a(href="#{grav.profileUrl}", data-i18n="account_profile.gravatar_profile_link") Full Gravatar Profile
+      img.profile-photo(src=user.getPhotoURL(256))
+  
+      h2 TODO
+      p Public user profiles are not ready yet.
\ No newline at end of file
diff --git a/app/templates/account/settings.jade b/app/templates/account/settings.jade
index 044ba6fda..770ee6fb4 100644
--- a/app/templates/account/settings.jade
+++ b/app/templates/account/settings.jade
@@ -31,7 +31,7 @@ block content
           .form
             .form-group
               label.control-label(for="name", data-i18n="general.name") Name
-              input#name.form-control(name="name", type="text", value="#{me.get('name')||''}", placeholder="#{gravatarName}")
+              input#name.form-control(name="name", type="text", value="#{me.get('name') || ''}")
             .form-group
               label.control-label(for="email", data-i18n="general.email") Email
               input#email.form-control(name="email", type="text", value="#{me.get('email')}")
@@ -42,23 +42,11 @@ block content
               
   
       #picture-pane.tab-pane
-        h3(data-i18n="account_settings.gravatar_select") Select which Gravatar photo to use
-        p
-          if !photos
-            span(data-i18n="account_settings.gravatar_add_photos") Add thumbnails and photos to a Gravatar account for your email to choose an image.
-          
-          else
-            .thumbnails
-              each photo, i in photos
-                .thumbnail
-                  label(for="photo-#{i}")
-                    img(src=photo)
-                  br
-                  input(type="radio", name="photoURL", value="#{photo}", id="photo-#{i}", checked=photo==chosenPhoto)
-            .clearfix
-            p
-              a(href="http://en.gravatar.com/profiles/edit/?noclose#your-images", target="_blank", data-i18n="account_settings.gravatar_add_more_photos") Add more photos to your Gravatar account to access them here.
-      
+        h3(data-i18n="account_settings.upload_picture") Upload a picture
+        #picture-treema
+        .gravatar-fallback
+          img(src=me.getPhotoURL(256), alt="Gravatar", title="Gravatar fallback image")
+
       #wizard-pane.tab-pane
         #wizard-settings-view
 
diff --git a/app/templates/base.jade b/app/templates/base.jade
index 3aec9624e..b568ccbde 100644
--- a/app/templates/base.jade
+++ b/app/templates/base.jade
@@ -33,7 +33,7 @@ body
 
         if me.get('anonymous') === false
           button.btn.btn-primary.navbuttontext.header-font#logout-button(data-i18n="login.log_out") Log Out
-          a.btn.btn-primary.navbuttontext.header-font(href="/account/profile/#{me.id}")
+          a.btn.btn-primary.navbuttontext.header-font(href=me.get('jobProfile') ? "/account/profile/#{me.id}" : "/account/settings")
             div.navbuttontext-user-name
               | #{me.displayName()}
             i.icon-cog.icon-white.big
diff --git a/app/templates/employers.jade b/app/templates/employers.jade
index 0acbdc34e..d242a7d77 100644
--- a/app/templates/employers.jade
+++ b/app/templates/employers.jade
@@ -54,11 +54,10 @@ block content
           tr(data-candidate-id=candidate.id)
             td
               if authorized
-                // Want image, but it doesn't work without loading every Gravatar profile
-                //img(src=candidate.getPhotoURL(), alt=profile.name, title=profile.name, width=50)
+                img(src=candidate.getPhotoURL(50), alt=profile.name, title=profile.name, width=50)
                 p= profile.name
               else
-                //img(src="/images/pages/contribute/archmage.png", alt="", title="Sign up as an employer to see our candidates", width=50)
+                img(src="/images/pages/contribute/archmage.png", alt="", title="Sign up as an employer to see our candidates", width=50)
                 p Developer ##{index + 1}
             if profile.country == 'USA'
               td= profile.city
diff --git a/app/views/account/profile_view.coffee b/app/views/account/profile_view.coffee
index 8e6302707..fea7a7c49 100644
--- a/app/views/account/profile_view.coffee
+++ b/app/views/account/profile_view.coffee
@@ -5,7 +5,6 @@ User = require 'models/User'
 module.exports = class ProfileView extends View
   id: "profile-view"
   template: template
-  loadingProfile: true
 
   events:
     'click #toggle-job-profile-approved': 'toggleJobProfileApproved'
@@ -14,30 +13,16 @@ module.exports = class ProfileView extends View
   constructor: (options, @userID) ->
     @onJobProfileNotesChanged = _.debounce @onJobProfileNotesChanged, 1000
     super options
-    @user = User.getByID(@userID)
-    @loadingProfile = false if 'gravatarProfile' of @user
-    @listenTo(@user, 'change', @userChanged)
-    @listenTo(@user, 'error', @userError)
-
-  userChanged: (user) ->
-    @loadingProfile = false if 'gravatarProfile' of user
-    @render()
-
-  userError: (user) ->
-    @loadingProfile = false
-    @render()
+    if @userID is me.id
+      @user = me
+    else
+      @user = User.getByID(@userID)
+      @addResourceToLoad @user, 'user_profile'
 
   getRenderData: ->
     context = super()
-    grav = @user.gravatarProfile
-    grav = grav.entry[0] if grav
-    addedContext =
-      user: @user
-      loadingProfile: @loadingProfile
-      myProfile: @user.id is context.me.id
-      grav: grav
-      photoURL: @user.getPhotoURL()
-    context[key] = addedContext[key] for key of addedContext
+    context.user = @user
+    context.myProfile = @user.id is context.me.id
     context.marked = marked
     context.moment = moment
     context.iconForLink = @iconForLink
diff --git a/app/views/account/settings_view.coffee b/app/views/account/settings_view.coffee
index b042b34ef..9a151f6d4 100644
--- a/app/views/account/settings_view.coffee
+++ b/app/views/account/settings_view.coffee
@@ -20,18 +20,7 @@ module.exports = class SettingsView extends View
     @save =  _.debounce(@save, 200)
     super options
     return unless me
-    @listenTo(me, 'change', @refreshPicturePane) # depends on gravatar load
     @listenTo(me, 'invalid', (errors) -> forms.applyErrorsToForm(@$el, me.validationError))
-    window.f = @getSubscriptions
-
-  refreshPicturePane: ->
-    h = $(@template(@getRenderData()))
-    newPane = $('#picture-pane', h)
-    oldPane = $('#picture-pane')
-    active = oldPane.hasClass('active')
-    oldPane.replaceWith(newPane)
-    newPane.i18n()
-    newPane.addClass('active') if active
 
   afterRender: ->
     super()
@@ -55,6 +44,11 @@ module.exports = class SettingsView extends View
     @listenTo @jobProfileView, 'change', @save
     @insertSubView @jobProfileView
 
+    if me.schema().loaded
+      @buildPictureTreema()
+    else
+      @listenToOnce me, 'schema-loaded', @buildPictureTreema
+
   chooseTab: (category) ->
     id = "##{category}-pane"
     pane = $(id, @$el)
@@ -68,9 +62,6 @@ module.exports = class SettingsView extends View
   getRenderData: ->
     c = super()
     return c unless me
-    c.gravatarName = c.me?.gravatarName()
-    c.photos = me.gravatarPhotoURLs()
-    c.chosenPhoto = me.getPhotoURL()
     c.subs = {}
     c.subs[sub] = 1 for sub in c.me.get('emailSubscriptions') or ['announcement', 'notification', 'tester', 'level_creator', 'developer']
     c.showsJobProfileTab = me.isAdmin() or me.get('jobProfile') or location.hash.search('job-profile-') isnt -1
@@ -88,6 +79,30 @@ module.exports = class SettingsView extends View
     $('#email-pane input[type="checkbox"]', @$el).prop('checked', not Boolean(subs.length))
     @save()
 
+  buildPictureTreema: ->
+    data = photoURL: me.get('photoURL')
+    if data.photoURL?.search('gravatar') isnt -1
+      # Old style
+      data.photoURL = null
+    schema = _.cloneDeep me.schema().attributes
+    schema.properties = _.pick me.schema().get('properties'), 'photoURL'
+    schema.required = ['photoURL']
+    console.log 'schema is', schema
+    treemaOptions =
+      filePath: "db/user/#{me.id}"
+      schema: schema
+      data: data
+      callbacks: {change: @onPictureChanged}
+
+    @pictureTreema = @$el.find('#picture-treema').treema treemaOptions
+    @pictureTreema.build()
+    @pictureTreema.open()
+    @$el.find('.gravatar-fallback').toggle not me.get 'photoURL'
+
+  onPictureChanged: (e) =>
+    @trigger 'change'
+    @$el.find('.gravatar-fallback').toggle not me.get 'photoURL'
+
   save: ->
     forms.clearFormAlerts(@$el)
     @grabData()
@@ -127,9 +142,10 @@ module.exports = class SettingsView extends View
       me.set('password', password1)
 
   grabOtherData: ->
-    me.set('name', $('#name', @$el).val())
-    me.set('email', $('#email', @$el).val())
-    me.set('emailSubscriptions', @getSubscriptions())
+    me.set 'name', $('#name', @$el).val()
+    me.set 'email', $('#email', @$el).val()
+    me.set 'emailSubscriptions', @getSubscriptions()
+    me.set 'photoURL', @pictureTreema.get('/photoURL')
 
     adminCheckbox = @$el.find('#admin')
     if adminCheckbox.length
diff --git a/app/views/kinds/CocoView.coffee b/app/views/kinds/CocoView.coffee
index 768072a26..d064f9ab6 100644
--- a/app/views/kinds/CocoView.coffee
+++ b/app/views/kinds/CocoView.coffee
@@ -104,15 +104,15 @@ module.exports = class CocoView extends Backbone.View
     context
 
   afterRender: ->
-    
+
   # Resource and request loading management for any given view
-    
+
   addResourceToLoad: (modelOrCollection, name, value=1) ->
     @loadProgress.resources.push {resource:modelOrCollection, value:value, name:name}
     @listenToOnce modelOrCollection, 'sync', @updateProgress
     @listenTo modelOrCollection, 'error', @onResourceLoadFailed
     @updateProgress()
-    
+
   addRequestToLoad: (jqxhr, name, retryFunc, value=1) ->
     @loadProgress.requests.push {request:jqxhr, value:value, name: name, retryFunc: retryFunc}
     jqxhr.done @updateProgress
@@ -152,7 +152,7 @@ module.exports = class CocoView extends Backbone.View
     num += r.value for r in @loadProgress.requests when r.request.status
     num += r.value for r in @loadProgress.somethings when r.loaded
     #console.log 'update progress', @, num, denom, arguments
-    
+
     progress = if denom then num / denom else 0
     # sometimes the denominator isn't known from the outset, so make sure the overall progress only goes up
     @loadProgress.progress = progress if progress > @loadProgress.progress
@@ -160,7 +160,7 @@ module.exports = class CocoView extends Backbone.View
     if num is denom and not @loaded
       @loaded = true
       @onLoaded()
-      
+
   updateProgressBar: =>
     prog = "#{parseInt(@loadProgress.progress*100)}%"
     @$el.find('.loading-screen .progress-bar').css('width', prog)
@@ -169,7 +169,7 @@ module.exports = class CocoView extends Backbone.View
     @render()
 
   # Error handling for loading
-  
+
   onResourceLoadFailed: (resource, jqxhr) ->
     for r, index in @loadProgress.resources
       break if r.resource is resource
@@ -179,12 +179,12 @@ module.exports = class CocoView extends Backbone.View
       resourceIndex: index,
       responseText: jqxhr.responseText
     })).i18n()
-  
+
   onRetryResource: (e) ->
     r = @loadProgress.resources[$(e.target).data('resource-index')]
     r.resource.fetch()
     $(e.target).closest('.loading-error-alert').remove()
-    
+
   onRequestLoadFailed: (jqxhr) =>
     for r, index in @loadProgress.requests
       break if r.request is jqxhr
@@ -194,7 +194,7 @@ module.exports = class CocoView extends Backbone.View
       requestIndex: index,
       responseText: jqxhr.responseText
     }))
-    
+
   onRetryRequest: (e) ->
     r = @loadProgress.requests[$(e.target).data('request-index')]
     @[r.retryFunc]?()
diff --git a/server/users/user_handler.coffee b/server/users/user_handler.coffee
index 4fb0f1515..11038b9fa 100644
--- a/server/users/user_handler.coffee
+++ b/server/users/user_handler.coffee
@@ -9,6 +9,7 @@ errors = require '../commons/errors'
 async = require 'async'
 log = require 'winston'
 LevelSession = require('../levels/sessions/LevelSession')
+LevelSessionHandler = require '../levels/sessions/level_session_handler'
 
 serverProperties = ['passwordHash', 'emailLower', 'nameLower', 'passwordReset']
 privateProperties = [
@@ -48,18 +49,8 @@ UserHandler = class UserHandler extends Handler
     delete obj[prop] for prop in privateProperties unless includePrivates
     includeCandidate = includePrivates or (obj.jobProfileApproved and req.user and ('employer' in (req.user.permissions ? [])))
     delete obj[prop] for prop in candidateProperties unless includeCandidate
-    obj.emailHash = @buildEmailHash document
     return obj
 
-  buildEmailHash: (user) ->
-    # emailHash is used by gravatar
-    hash = crypto.createHash('md5')
-    if user.get('email')
-      hash.update(_.trim(user.get('email')).toLowerCase())
-    else
-      hash.update(user.get('_id') + '')
-    hash.digest('hex')
-
   waterfallFunctions: [
     # FB access token checking
     # Check the email is the same as FB reports
@@ -126,7 +117,7 @@ UserHandler = class UserHandler extends Handler
 
   getById: (req, res, id) ->
     if req.user?._id.equals(id)
-      return @sendSuccess(res, @formatEntity(req, req.user))
+      return @sendSuccess(res, @formatEntity(req, req.user, 256))
     super(req, res, id)
 
   getNamesByIds: (req, res) ->
@@ -203,9 +194,11 @@ UserHandler = class UserHandler extends Handler
           @sendSuccess(res, {result:'success'})
 
   avatar: (req, res, id) ->
-    @modelClass.findById(id).exec (err, document) ->
+    @modelClass.findById(id).exec (err, document) =>
       return @sendDatabaseError(res, err) if err
-      res.redirect(document?.get('photoURL') or '/images/generic-wizard-icon.png')
+      photoURL = document?.get('photoURL')
+      photoURL ||= @buildGravatarURL document
+      res.redirect photoURL
       res.end()
 
   getLevelSessions: (req, res, userID) ->
@@ -217,7 +210,7 @@ UserHandler = class UserHandler extends Handler
       projection[field] = 1 for field in req.query.project.split(',')
     LevelSession.find(query).select(projection).exec (err, documents) =>
       return @sendDatabaseError(res, err) if err
-      documents = (@formatEntity(req, doc) for doc in documents)
+      documents = (LevelSessionHandler.formatEntity(req, doc) for doc in documents)
       @sendSuccess(res, documents)
 
   getCandidates: (req, res) ->
@@ -235,13 +228,27 @@ UserHandler = class UserHandler extends Handler
       @sendSuccess(res, candidates)
 
   formatCandidate: (authorized, document) ->
-    fields = if authorized then ['jobProfile', 'jobProfileApproved', '_id'] else ['jobProfile']
+    fields = if authorized then ['jobProfile', 'jobProfileApproved', 'photoURL', '_id'] else ['jobProfile']
     obj = _.pick document.toObject(), fields
-    obj.emailHash = @buildEmailHash document
+    obj.photoURL ||= @buildGravatarURL document if authorized
     subfields = ['country', 'city', 'lookingFor', 'skills', 'experience', 'updated']
     if authorized
       subfields = subfields.concat ['name', 'work']
     obj.jobProfile = _.pick obj.jobProfile, subfields
     obj
 
+  buildGravatarURL: (user) ->
+    emailHash = @buildEmailHash user
+    defaultAvatar = "http://codecombat.com/file/db/thang.type/52a00d55cf1818f2be00000b/portrait.png"
+    "https://www.gravatar.com/avatar/#{emailHash}?default=#{defaultAvatar}"
+
+  buildEmailHash: (user) ->
+    # emailHash is used by gravatar
+    hash = crypto.createHash('md5')
+    if user.get('email')
+      hash.update(_.trim(user.get('email')).toLowerCase())
+    else
+      hash.update(user.get('_id') + '')
+    hash.digest('hex')
+
 module.exports = new UserHandler()
diff --git a/server/users/user_schema.coffee b/server/users/user_schema.coffee
index f4517db99..5b9605f44 100644
--- a/server/users/user_schema.coffee
+++ b/server/users/user_schema.coffee
@@ -9,7 +9,7 @@ UserSchema = c.object {},
   gender: {type: 'string', 'enum': ['male', 'female']}
   password: {type: 'string', maxLength: 256, minLength: 2, title:'Password'}
   passwordReset: {type: 'string'}
-  photoURL: {type: 'string', format: 'url', required: false}
+  photoURL: {type: 'string', format: 'image-file', title: 'Profile Picture', description: 'Upload a 256x256px or larger image to serve as your profile picture.'}
 
   facebookID: c.shortString({title: 'Facebook ID'})
   gplusID: c.shortString({title: 'G+ ID'})
@@ -36,7 +36,6 @@ UserSchema = c.object {},
   passwordHash: {type: 'string', maxLength: 256}
 
   # client side
-  #gravatarProfile: {} (should only ever be kept locally)
   emailHash: {type: 'string'}
 
   #Internationalization stuff