diff --git a/collections_.py b/collections_.py
index b05f442d228b3531c8ce4d69e8b401e1cc4092a4..11df8ce342f3e75f13f679c1848af860c825bd49 100644
--- a/collections_.py
+++ b/collections_.py
@@ -61,7 +61,18 @@ class Collections(nimsapiutil.NIMSRequestHandler):
 
     def post(self):
         """Create a new Collection."""
-        self.response.write('collections post\n')
+        if not self.valid_parameters(): # FIXME: move to superclass init
+            self.abort(400, 'invalid parameters')
+        name = self.request.get('name') or 'innominate'
+        epoch_ids = [bson.ObjectId(eid) for eid in self.request.get_all('epochs[]', [])]
+        epochs = [self.app.db.epochs.find_one({'_id': eid}, ['session']) for eid in epoch_ids]
+        if not all(epochs):
+            self.abort(400, 'some Epoch IDs are invalid')
+        if not all([self.user_access_epoch(epoch) for epoch in epochs]):
+            self.abort(403, 'user does not have access to all Epochs')
+        cid = self.app.db.collections.insert({'curator': self.userid, 'name': name, 'permissions': {self.userid: 'admin'}})
+        for eid in epoch_ids:
+            self.app.db.epochs.update({'_id': eid}, {'$push': {'collections': cid}})
 
     def get(self):
         """Return the list of Collections."""
@@ -123,17 +134,44 @@ class Collection(nimsapiutil.NIMSRequestHandler):
         if not self.user_is_superuser:
             if self.userid not in collection['permissions']:
                 self.abort(403)
-            if collection['permissions'][self.userid] != 'admin' and collection['permissions'][self.userid] != 'pi':
+            if collection['permissions'][self.userid] != 'admin': # mask other users' permissions
                 collection['permissions'] = {self.userid: collection['permissions'][self.userid]}
         self.response.write(json.dumps(collection, default=bson.json_util.default))
 
     def put(self, cid):
         """Update an existing Collection."""
-        self.response.write('collection %s put, %s\n' % (exp_id, self.request.params))
+        cid = bson.ObjectId(cid)
+        if not self.valid_parameters(): # FIXME: move to superclass init
+            self.abort(400, 'invalid parameters')
+        collection = self.app.db.collections.find_one({'_id': cid})
+        if not collection:
+            self.abort(404, 'Collection not found')
+        if not self.user_is_superuser and collection['permissions'].get(self.userid) != 'admin':
+            self.abort(403, 'user must be admin on Collection to modify')
+        add_epoch_ids = [bson.ObjectId(eid) for eid in self.request.get_all('add_epochs[]', [])]
+        add_epochs = [self.app.db.epochs.find_one({'_id': eid}, ['session']) for eid in add_epoch_ids]
+        del_epoch_ids = [bson.ObjectId(eid) for eid in self.request.get_all('del_epochs[]', [])]
+        del_epochs = [self.app.db.epochs.find_one({'_id': eid}, ['session']) for eid in del_epoch_ids]
+        if not all(add_epochs + del_epochs):
+            self.abort(400, 'some Epoch IDs are invalid')
+        if not all([self.user_access_epoch(epoch) for epoch in add_epochs]):
+            self.abort(403, 'user does not have access to all Epochs')
+        for eid in add_epoch_ids:
+            print 'adding', eid, 'to', cid
+            self.app.db.epochs.update({'_id': eid}, {'$addToSet': {'collections': bson.ObjectId(cid)}})
+        for eid in del_epoch_ids:
+            self.app.db.epochs.update({'_id': eid}, {'$pull': {'collections': bson.ObjectId(cid)}})
 
     def delete(self, cid):
         """Delete a Collection."""
-        self.abort(501)
+        cid = bson.ObjectId(cid)
+        collection = self.app.db.collections.find_one({'_id': cid}, ['permissions'])
+        if not collection:
+            self.abort(404, 'Collection not found')
+        if not self.user_is_superuser and collection['permissions'].get(self.userid) != 'admin':
+            self.abort(403, 'user must be admin on Collection to delete')
+        self.app.db.epochs.update({'collections': cid}, {'$pull': {'collections': cid}}, multi=True)
+        self.app.db.collections.remove({'_id': cid})
 
 
 class Sessions(nimsapiutil.NIMSRequestHandler):
diff --git a/experiments.py b/experiments.py
index 8ece9d1281b774c94703643bd66b1dcf8d27a698..cfe463c285db49c70e018dbca74b0a7f86534c21 100644
--- a/experiments.py
+++ b/experiments.py
@@ -368,16 +368,12 @@ class Epoch(nimsapiutil.NIMSRequestHandler):
 
     def get(self, eid):
         """Return one Epoch, conditionally with details."""
+        if not self.valid_parameters():
+            self.abort(400, 'invalid parameters')
         epoch = self.app.db.epochs.find_one({'_id': bson.ObjectId(eid)})
         if not epoch:
             self.abort(404)
-        session = self.app.db.sessions.find_one({'_id': epoch['session']})
-        if not session:
-            self.abort(500)
-        experiment = self.app.db.experiments.find_one({'_id': session['experiment']})
-        if not experiment:
-            self.abort(500)
-        if not self.user_is_superuser and self.userid not in experiment['permissions']:
+        if not self.user_access_epoch(epoch):
             self.abort(403)
         self.response.write(json.dumps(epoch, default=bson.json_util.default))
 
diff --git a/nimsapi.py b/nimsapi.py
index bf2c91e92f3805c17234e750a7af0c7314ae0af7..5ee8063ce4c498845f76f08ab45b373f952a8a65 100755
--- a/nimsapi.py
+++ b/nimsapi.py
@@ -475,7 +475,7 @@ if __name__ == '__main__':
     else:
         log.warning('private SSL key not specified, internims functionality disabled')
 
-    app.config['site_id'] = args.site_id
+    app.config['site_id'] = args.site_id or 'local'
     app.config['stage_path'] = args.stage_path or config.get('nims', 'stage_path')
 
     db_uri = args.db_uri or config.get('nims', 'db_uri')
diff --git a/nimsapiutil.py b/nimsapiutil.py
index 83eb2fd0d54d034a7c772b88059837b57c9bf45a..13be4a2a8894909d5e9821defba79c9c7387cd71 100644
--- a/nimsapiutil.py
+++ b/nimsapiutil.py
@@ -61,10 +61,7 @@ class NIMSRequestHandler(webapp2.RequestHandler):
 
         if not self.request.path.endswith('/nimsapi/log'):
             # TODO: change to log.debug
-            log.info(self.request.method + ' ' + self.request.path)
-            log.info('params: ' + str(self.request.params.mixed()))
-            log.info('headers: ' + str(dict(self.request.headers)))
-
+            log.info(self.request.method + ' ' + self.request.path + ' ' + str(self.request.params.mixed()))
 
     def dispatch(self):
         """dispatching and request forwarding"""
@@ -129,3 +126,18 @@ class NIMSRequestHandler(webapp2.RequestHandler):
 
     def schema(self, *args, **kwargs):
         self.response.write(json.dumps(self.json_schema, default=bson.json_util.default))
+
+    def valid_parameters(self):
+        # FIXME: implement, using self.request.params.mixed()
+        return True
+
+    def user_access_epoch(self, epoch):
+        if self.user_is_superuser:
+            return True
+        session = self.app.db.sessions.find_one({'_id': epoch['session']})
+        if not session:
+            self.abort(500)
+        experiment = self.app.db.experiments.find_one({'_id': session['experiment']})
+        if not experiment:
+            self.abort(500)
+        return (self.userid in experiment['permissions'])