diff --git a/apps/discussions/app/ipfs/index.js b/apps/discussions/app/ipfs/index.js
index a7500eb36..4d8710222 100644
--- a/apps/discussions/app/ipfs/index.js
+++ b/apps/discussions/app/ipfs/index.js
@@ -1,7 +1,22 @@
+import axios from 'axios'
 import ipfsClient from 'ipfs-http-client'
 
-export const ipfs = ipfsClient({
-  host: 'ipfs.autark.xyz',
-  port: '5001',
-  protocol: 'https',
-})
+const environments = {
+  development: { host: 'localhost', port: '5001', protocol: 'http' },
+  production: { host: 'ipfs.autark.xyz', port: '5001', protocol: 'https' },
+  staging: { host: 'ipfs.autark.xyz', port: '5001', protocol: 'https' },
+}
+
+const config = environments[process.env.NODE_ENV]
+
+export const ipfs = ipfsClient(config)
+
+export const ipfsGet = async hash => {
+  const endpoint = `${config.protocol}://${config.host}:8080/ipfs/${hash}`
+  try {
+    const { data } = await axios.get(endpoint)
+    return data
+  } catch (err) {
+    console.error('Error getting data from IPFS', err)
+  }
+}
diff --git a/apps/discussions/app/script.js b/apps/discussions/app/script.js
index 45bd92f0d..13a07ac77 100644
--- a/apps/discussions/app/script.js
+++ b/apps/discussions/app/script.js
@@ -2,7 +2,14 @@ import '@babel/polyfill'
 import { of } from 'rxjs'
 import AragonApi from '@aragon/api'
 
-import { handleHide, handlePost, handleRevise, initialState } from './state'
+import {
+  updateThread,
+  deleteThread,
+  handleHide,
+  handlePost,
+  handleRevise,
+  initialState
+} from './state'
 
 const INITIALIZATION_TRIGGER = Symbol('INITIALIZATION_TRIGGER')
 
@@ -13,16 +20,22 @@ api.store(
     let newState
     switch (event.event) {
       case INITIALIZATION_TRIGGER:
-        newState = { ...initialState, syncing: false }
+        newState = { ...initialState, isSyncing: false }
         return newState
       case 'SYNC_STATUS_SYNCING':
-        newState = { ...initialState, syncing: true }
+        newState = { ...initialState, isSyncing: true }
         return newState
       case 'SYNC_STATUS_SYNCED':
-        newState = { ...initialState, syncing: false }
+        newState = { ...initialState, isSyncing: false }
         return newState
       case 'ACCOUNTS_TRIGGER':
-        newState = { ...initialState, syncing: false }
+        newState = { ...initialState, isSyncing: false }
+        return newState
+      case 'UpdateThread':
+        newState = await updateThread(state, event.returnValues)
+        return newState
+      case 'DeleteThread':
+        newState = await deleteThread(state, event.returnValues)
         return newState
       case 'Post':
         newState = await handlePost(state, event)
diff --git a/apps/discussions/app/state/index.js b/apps/discussions/app/state/index.js
index 47f38a278..d3cc80b84 100644
--- a/apps/discussions/app/state/index.js
+++ b/apps/discussions/app/state/index.js
@@ -1,2 +1,3 @@
 export * from './initialState'
 export * from './updateState'
+export * from './threads'
diff --git a/apps/discussions/app/state/initialState.js b/apps/discussions/app/state/initialState.js
index 84a0ec732..0d84cdfda 100644
--- a/apps/discussions/app/state/initialState.js
+++ b/apps/discussions/app/state/initialState.js
@@ -1,4 +1,5 @@
 export const initialState = {
+  threads: [],
   discussions: {},
-  syncing: true,
+  isSyncing: true,
 }
diff --git a/apps/discussions/app/state/threads.js b/apps/discussions/app/state/threads.js
new file mode 100644
index 000000000..d419cc89e
--- /dev/null
+++ b/apps/discussions/app/state/threads.js
@@ -0,0 +1,44 @@
+import { ipfsGet } from '../ipfs'
+
+export const updateThread = async (state, { thread, metadata }) => {
+  const { threads = [] } = state
+  try {
+    const {
+      name,
+      title,
+      description,
+      creationDate,
+      context,
+      author,
+    } = await ipfsGet(metadata)
+    const index = threads.findIndex(t => t.address === thread)
+    if (index > -1) threads[index].description = description
+    else {
+      threads.push({
+        address: thread,
+        name,
+        title,
+        description,
+        context,
+        creationDate: new Date(creationDate),
+        author,
+      })
+    }
+    return {
+      ...state,
+      threads
+    }
+  } catch (e) {
+    console.error(e)
+    return state
+  }
+}
+
+export const deleteThread = async (state, { thread }) => {
+  const { threads = [] } = state
+  const filteredThreads = threads.filter(t => t.address !== thread)
+  return {
+    ...state,
+    threads: filteredThreads
+  }
+}
diff --git a/apps/discussions/arapp.json b/apps/discussions/arapp.json
index e9e983577..a04e29e70 100644
--- a/apps/discussions/arapp.json
+++ b/apps/discussions/arapp.json
@@ -1,8 +1,13 @@
 {
   "roles": [
     {
-      "name": "Is doing nothing, just needed to technically be an AragonApp",
-      "id": "EMPTY_ROLE",
+      "name": "Register role",
+      "id": "REGISTER_ROLE",
+      "params": []
+    },
+    {
+      "name": "Moderator role",
+      "id": "MODERATOR_ROLE",
       "params": []
     }
   ],
diff --git a/apps/discussions/contracts/DiscussionApp.sol b/apps/discussions/contracts/DiscussionApp.sol
index 7b69881b2..fdf4bc1cf 100644
--- a/apps/discussions/contracts/DiscussionApp.sol
+++ b/apps/discussions/contracts/DiscussionApp.sol
@@ -19,10 +19,16 @@ contract DiscussionApp is IForwarder, AragonApp {
     );
     event Hide(address indexed author, uint256 discussionThreadId, uint256 postId, uint256 hiddenAt);
     event CreateDiscussionThread(uint256 actionId, bytes _evmScript);
+    event UpdateThread(string thread, string metadata);
+    event DeleteThread(string thread);
 
-    bytes32 public constant EMPTY_ROLE = keccak256("EMPTY_ROLE");
+    bytes32 public constant REGISTER_ROLE = keccak256("REGISTER_ROLE");
+    bytes32 public constant MODERATOR_ROLE = keccak256("MODERATOR_ROLE");
 
     string private constant ERROR_CAN_NOT_FORWARD = "DISCUSSIONS_CAN_NOT_FORWARD";
+    string private constant ERROR_ALREADY_EXISTS = "You cannot create an existing thread.";
+    string private constant ERROR_EDIT_NOT_AUTHOR = "You cannot edit a thread you did not author.";
+    string private constant ERROR_DELETE_NOT_AUTHOR = "You cannot delete a thread you did not author.";
 
     struct DiscussionPost {
         address author;
@@ -38,6 +44,8 @@ contract DiscussionApp is IForwarder, AragonApp {
 
     mapping(uint256 => DiscussionPost[]) public discussionThreadPosts;
 
+    mapping(string => address) private threadAuthors;
+
     function initialize() external onlyInit {
         discussionThreadId = 0;
         initialized();
@@ -96,6 +104,38 @@ contract DiscussionApp is IForwarder, AragonApp {
         );
     }
 
+    /**
+     * @notice Create new thread at `thread`
+     * @param thread The address of the 3Box thread
+     * @param metadata The IPFS hash of the metadata for the thread
+    */
+    function registerThread(string thread, string metadata)
+        auth(REGISTER_ROLE) external {
+        require(threadAuthors[thread] == 0, ERROR_ALREADY_EXISTS);
+        threadAuthors[thread] = msg.sender;
+        emit UpdateThread(thread, metadata);
+    }
+
+    /**
+     * @notice Edit thread at `thread`
+     * @param thread The address of the 3Box thread
+     * @param newMetadata The IPFS hash of the new metadata for the thread
+     */
+    function editThread(string thread, string newMetadata)
+        auth(REGISTER_ROLE) external {
+        require(threadAuthors[thread] == msg.sender, ERROR_EDIT_NOT_AUTHOR);
+        emit UpdateThread(thread, newMetadata);
+    }
+
+    /**
+     * @notice Delete thread at `thread`
+     * @param thread The address of the 3Box thread
+     */
+    function deleteThread(string thread) auth(REGISTER_ROLE) external {
+        require(threadAuthors[thread] == msg.sender, ERROR_DELETE_NOT_AUTHOR);
+        emit DeleteThread(thread);
+    }
+
     // Forwarding fns
 
     /**
diff --git a/apps/discussions/package.json b/apps/discussions/package.json
index 6b53ae23f..2be9cb4ad 100644
--- a/apps/discussions/package.json
+++ b/apps/discussions/package.json
@@ -46,7 +46,7 @@
     "clean:start": "aragon clean && rm -rf ~/.aragon && npm run start:http:template",
     "start": "npm run start:ipfs",
     "start:ipfs": "aragon run",
-    "start:http": "aragon run --http localhost:8001 --http-served-from ./dist",
+    "start:http": "NODE_ENV=development aragon run --http localhost:8001 --http-served-from ./dist",
     "start:ipfs:template": "npm run start:ipfs -- --template Template --template-init @ARAGON_ENS",
     "start:http:template": "rm -rf build && npm run start:http -- --template Template --template-init @ARAGON_ENS",
     "start:app": "cd app && npm start && cd ..",
diff --git a/apps/discussions/public/meta/icon.svg b/apps/discussions/public/meta/icon.svg
index 546d85afe..394a0917f 100644
--- a/apps/discussions/public/meta/icon.svg
+++ b/apps/discussions/public/meta/icon.svg
@@ -1 +1,10 @@
-
\ No newline at end of file
+
diff --git a/package.json b/package.json
index f639af030..9689d78ed 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
     "lerna": "^3.10.8",
     "truffle-privatekey-provider": "1.1.0",
     "wait-on": "^3.2.0",
-    "web3": "^1.2.4"
+    "web3": "1.2.7"
   },
   "scripts": {
     "bootstrap": "lerna bootstrap --hoist",
@@ -74,7 +74,7 @@
     "start:dev-template": "cd templates/dev && npm run publish:aragen && npm run start:template:aragen",
     "start:address": "cd apps/address-book && npm start",
     "start:allocations": "cd apps/allocations && npm start",
-    "start:dev": "DEV=true apps/shared/test-helpers/ganache-cli.sh",
+    "start:dev": "NODE_ENV=development DEV=true apps/shared/test-helpers/ganache-cli.sh",
     "start:no:client": "NO_CLIENT=true apps/shared/test-helpers/ganache-cli.sh",
     "start:dot": "cd apps/dot-voting && npm start",
     "start:projects": "cd apps/projects && npm start",
diff --git a/templates/dev/contracts/BaseCache.sol b/templates/dev/contracts/BaseCache.sol
index 67d5ec392..d0b32907a 100644
--- a/templates/dev/contracts/BaseCache.sol
+++ b/templates/dev/contracts/BaseCache.sol
@@ -23,7 +23,7 @@ contract BaseCache is BaseTemplate {
             ENS(_deployedSetupContracts[1]),
             MiniMeTokenFactory(_deployedSetupContracts[2]),
             IFIFSResolvingRegistrar(_deployedSetupContracts[3])
-    ) {}
+    ) public {}
 
     function _cacheBase(
         ACL _acl,
diff --git a/templates/dev/contracts/BaseOEApps.sol b/templates/dev/contracts/BaseOEApps.sol
index b7638127d..98e74983b 100644
--- a/templates/dev/contracts/BaseOEApps.sol
+++ b/templates/dev/contracts/BaseOEApps.sol
@@ -123,11 +123,13 @@ contract BaseOEApps is BaseCache, TokenCache {
     /* DISCUSSIONS */
 
     function _installDiscussionsApp(Kernel _dao) internal returns (DiscussionApp) {
-        return DiscussionApp(_installNonDefaultApp(_dao, DISCUSSIONS_APP_ID));
+        bytes memory initializeData = abi.encodeWithSelector(DiscussionApp(0).initialize.selector);
+        return DiscussionApp(_installNonDefaultApp(_dao, DISCUSSIONS_APP_ID, initializeData));
     }
 
-    function _createDiscussionsPermissions(ACL _acl, DiscussionApp _discussions, address _grantee, address _manager) internal {
-        _acl.createPermission(_grantee, _discussions, _discussions.EMPTY_ROLE(), _manager);
+    function _createDiscussionsPermissions(ACL _acl, DiscussionApp _discussions, address _moderator, address _register, address _manager) internal {
+        _acl.createPermission(_moderator, _discussions, _discussions.MODERATOR_ROLE(), _manager);
+        _acl.createPermission(_register, _discussions, _discussions.REGISTER_ROLE(), _manager);
     }
 
     /* PROJECTS */
diff --git a/templates/dev/contracts/DevTemplate.sol b/templates/dev/contracts/DevTemplate.sol
index ebb0f9afd..351cf33e1 100644
--- a/templates/dev/contracts/DevTemplate.sol
+++ b/templates/dev/contracts/DevTemplate.sol
@@ -158,11 +158,6 @@ contract DevTemplate is BaseOEApps {
     )
         internal
     {
-        if (_useDiscussions) {
-            DiscussionApp discussions = _installDiscussionsApp(_dao);
-            _createDiscussionsPermissions(_acl, discussions, ANY_ENTITY, _voting);
-        }
-
         MiniMeToken token = _popTokenCache(msg.sender);
         AddressBook addressBook = _installAddressBookApp(_dao);
         Allocations allocations = _installAllocationsApp(_dao, _vault, _allocationsPeriod == 0 ? DEFAULT_PERIOD : _allocationsPeriod);
@@ -170,6 +165,14 @@ contract DevTemplate is BaseOEApps {
         Projects projects = _installProjectsApp(_dao, _vault);
         Rewards rewards = _installRewardsApp(_dao, _vault);
 
+        if (_useDiscussions) {
+            DiscussionApp discussions = _installDiscussionsApp(_dao);
+            // _createDiscussionsPermissions(_acl, discussions, msg.sender, _tokenManager, _voting); // Stack too deep
+            _acl.createPermission(msg.sender, discussions, discussions.MODERATOR_ROLE(), _voting);
+            _acl.createPermission(_tokenManager, discussions, discussions.REGISTER_ROLE(), _voting);
+        }
+
+
         _setupOEPermissions(
             _acl,
             _tokenManager,