diff --git a/package/gluon-client-bridge/luasrc/usr/lib/lua/gluon/client_bridge.lua b/package/gluon-client-bridge/luasrc/usr/lib/lua/gluon/client_bridge.lua
index f3af75044f61584ca72eff8bd710dfd1a360f67a..5c69373f9748359388c2eaa150f21040bd9065ab 100644
--- a/package/gluon-client-bridge/luasrc/usr/lib/lua/gluon/client_bridge.lua
+++ b/package/gluon-client-bridge/luasrc/usr/lib/lua/gluon/client_bridge.lua
@@ -1,8 +1,10 @@
 local site = require 'gluon.site'
 
 
-module 'gluon.client_bridge'
+local M = {}
 
-function next_node_macaddr()
+function M.next_node_macaddr()
 	return site.next_node.mac('16:41:95:40:f7:dc')
 end
+
+return M
diff --git a/package/gluon-config-mode-geo-location-osm/luasrc/usr/lib/lua/gluon/config-mode/geo-location-osm.lua b/package/gluon-config-mode-geo-location-osm/luasrc/usr/lib/lua/gluon/config-mode/geo-location-osm.lua
index 4969f1ac8f450d483a160629905033e9d6611381..6cc6d65a55680fbe7c24dbd2638dd0b3228f4950 100644
--- a/package/gluon-config-mode-geo-location-osm/luasrc/usr/lib/lua/gluon/config-mode/geo-location-osm.lua
+++ b/package/gluon-config-mode-geo-location-osm/luasrc/usr/lib/lua/gluon/config-mode/geo-location-osm.lua
@@ -1,21 +1,19 @@
 local osm = require 'gluon.web.model.osm'
 local site = require 'gluon.site'
 
-local tonumber = tonumber
 
+local M = {}
 
-module 'gluon.config-mode.geo-location-osm'
+M.MapValue = osm.MapValue
 
-MapValue = osm.MapValue
-
-function help(i18n)
+function M.help(i18n)
 	local pkg_i18n = i18n 'gluon-config-mode-geo-location-osm'
 	return pkg_i18n.translate(
 		'You may also select the position on the map displayed below if your computer is connected to the internet at the moment.'
 	)
 end
 
-function options()
+function M.options()
 	local config = site.config_mode.geo_location.osm
 
 	return {
@@ -24,3 +22,5 @@ function options()
 		pos = config.center(),
 	}
 end
+
+return M
diff --git a/package/gluon-core/files/lib/netifd/proto/gluon_wired.sh b/package/gluon-core/files/lib/netifd/proto/gluon_wired.sh
index 4a44a30eaa444ce12d1c65048569b93d71d12033..0778b354847070cddf43ef742274e4b2846ef137 100755
--- a/package/gluon-core/files/lib/netifd/proto/gluon_wired.sh
+++ b/package/gluon-core/files/lib/netifd/proto/gluon_wired.sh
@@ -40,12 +40,12 @@ proto_gluon_wired_setup() {
 
                 json_init
                 json_add_string name "$meshif"
-                [ -n "$index" ] && json_add_string macaddr "$(lua -lgluon.util -e "print(gluon.util.generate_mac($index))")"
+                [ -n "$index" ] && json_add_string macaddr "$(lua -e "print(require('gluon.util').generate_mac($index))")"
                 json_add_string proto 'vxlan6'
                 json_add_string tunlink "$config"
                 json_add_string ip6addr "$(interface_linklocal "$ifname")"
                 json_add_string peer6addr 'ff02::15c'
-                json_add_int vid "$(lua -lgluon.util -e 'print(tonumber(gluon.util.domain_seed_bytes("gluon-mesh-vxlan", 3), 16))')"
+                json_add_int vid "$(lua -e 'print(tonumber(require("gluon.util").domain_seed_bytes("gluon-mesh-vxlan", 3), 16))')"
                 json_add_boolean rxcsum '0'
                 json_add_boolean txcsum '0'
                 json_close_object
diff --git a/package/gluon-core/luasrc/usr/lib/lua/gluon/iputil.lua b/package/gluon-core/luasrc/usr/lib/lua/gluon/iputil.lua
index 39e0897afda7f609dd4406fd10f77d9c029d012e..bfb09a7530570aee706849738c9b2c96ae147d65 100644
--- a/package/gluon-core/luasrc/usr/lib/lua/gluon/iputil.lua
+++ b/package/gluon-core/luasrc/usr/lib/lua/gluon/iputil.lua
@@ -1,10 +1,9 @@
 local bit = require 'bit'
-local string = string
-local tonumber = tonumber
-local table = table
-module 'gluon.iputil'
 
-function IPv6(address)
+
+local M = {}
+
+function M.IPv6(address)
 	--[[
 	(c) 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
 	(c) 2008 Steven Barth <steven@midlink.org>
@@ -75,7 +74,7 @@ function IPv6(address)
 	end
 end
 
-function mac_to_ip(prefix, mac)
+function M.mac_to_ip(prefix, mac)
 	local m1, m2, m3, m6, m7, m8 = string.match(mac, '(%x%x):(%x%x):(%x%x):(%x%x):(%x%x):(%x%x)')
 	local m4 = 0xff
 	local m5 = 0xfe
@@ -89,8 +88,9 @@ function mac_to_ip(prefix, mac)
 	local prefix, plen = string.match(prefix, '(.*)/(%d+)')
 	plen = tonumber(plen, 10)
 
-	local p1, p2, p3, p4, p5, p6, p7, p8 = IPv6(prefix)
+	local p1, p2, p3, p4, p5, p6, p7, p8 = M.IPv6(prefix)
 
 	return string.format("%x:%x:%x:%x:%x:%x:%x:%x/%d", p1, p2, p3, p4, h1, h2, h3, h4, 128)
 end
 
+return M
diff --git a/package/gluon-core/luasrc/usr/lib/lua/gluon/platform.lua b/package/gluon-core/luasrc/usr/lib/lua/gluon/platform.lua
index 17d881f620df541d4dec1a7635fd37603a131fcf..2a9cfe775b63af412bc363d25b2ee1de03359603 100644
--- a/package/gluon-core/luasrc/usr/lib/lua/gluon/platform.lua
+++ b/package/gluon-core/luasrc/usr/lib/lua/gluon/platform.lua
@@ -1,49 +1,45 @@
 local platform_info = require 'platform_info'
 local util = require 'gluon.util'
 
-local setmetatable = setmetatable
 
+local M = setmetatable({}, {
+	__index = platform_info,
+})
 
-module 'gluon.platform'
+function M.match(target, subtarget, boards)
+	if M.get_target() ~= target then
+		return false
+	end
 
-setmetatable(_M,
-	     {
-		__index = platform_info,
-	     }
-)
+	if M.get_subtarget() ~= subtarget then
+		return false
+	end
 
-function match(target, subtarget, boards)
-   if get_target() ~= target then
-      return false
-   end
+	if boards and not util.contains(boards, M.get_board_name()) then
+		return false
+	end
 
-   if get_subtarget() ~= subtarget then
-      return false
-   end
-
-   if boards and not util.contains(boards, get_board_name()) then
-      return false
-   end
-
-   return true
+	return true
 end
 
-function is_outdoor_device()
-   if match('ar71xx', 'generic', {
-      'cpe510-520-v1',
-      'ubnt-nano-m',
-      'ubnt-nano-m-xw',
-      }) then
-      return true
+function M.is_outdoor_device()
+	if M.match('ar71xx', 'generic', {
+		'cpe510-520-v1',
+		'ubnt-nano-m',
+		'ubnt-nano-m-xw',
+	}) then
+		return true
 
-   elseif match('ar71xx', 'generic', {'unifiac-lite'}) and
-	   get_model() == 'Ubiquiti UniFi-AC-MESH' then
-      return true
+	elseif M.match('ar71xx', 'generic', {'unifiac-lite'}) and
+		M.get_model() == 'Ubiquiti UniFi-AC-MESH' then
+		return true
 
-   elseif match('ar71xx', 'generic', {'unifiac-pro'}) and
-	   get_model() == 'Ubiquiti UniFi-AC-MESH-PRO' then
-      return true
-   end
+	elseif M.match('ar71xx', 'generic', {'unifiac-pro'}) and
+		M.get_model() == 'Ubiquiti UniFi-AC-MESH-PRO' then
+		return true
+	end
 
-   return false
+	return false
 end
+
+return M
diff --git a/package/gluon-core/luasrc/usr/lib/lua/gluon/sysconfig.lua b/package/gluon-core/luasrc/usr/lib/lua/gluon/sysconfig.lua
index fabfe05f9344fc1637adeceeb2839003aac85d34..93eb6ed161e8df5b22b5e7720f5734e6f6f089a5 100644
--- a/package/gluon-core/luasrc/usr/lib/lua/gluon/sysconfig.lua
+++ b/package/gluon-core/luasrc/usr/lib/lua/gluon/sysconfig.lua
@@ -20,15 +20,7 @@ local function set(_, name, val)
 	end
 end
 
-local setmetatable = setmetatable
-
-module 'gluon.sysconfig'
-
-setmetatable(_M,
-	{
-		__index = get,
-		__newindex = set,
-	}
-)
-
-return _M
+return setmetatable({}, {
+	__index = get,
+	__newindex = set,
+})
diff --git a/package/gluon-core/luasrc/usr/lib/lua/gluon/users.lua b/package/gluon-core/luasrc/usr/lib/lua/gluon/users.lua
index 1dbc3728648f437b0ff1a1753e1985b83ed56152..9b6d47303c2c176f5750f37a354c5dc299a1c898 100644
--- a/package/gluon-core/luasrc/usr/lib/lua/gluon/users.lua
+++ b/package/gluon-core/luasrc/usr/lib/lua/gluon/users.lua
@@ -1,20 +1,19 @@
 local util = require 'gluon.util'
 
-local os = os
-local string = string
 
+local M = {}
 
-module 'gluon.users'
-
-function remove_user(username)
+function M.remove_user(username)
 	os.execute('exec lock /var/lock/passwd')
 	util.replace_prefix('/etc/passwd', username .. ':')
 	util.replace_prefix('/etc/shadow', username .. ':')
 	os.execute('exec lock -u /var/lock/passwd')
 end
 
-function remove_group(groupname)
+function M.remove_group(groupname)
 	os.execute('exec lock /var/lock/group')
 	util.replace_prefix('/etc/group', groupname .. ':')
 	os.execute('exec lock -u /var/lock/group')
 end
+
+return M
diff --git a/package/gluon-core/luasrc/usr/lib/lua/gluon/util.lua b/package/gluon-core/luasrc/usr/lib/lua/gluon/util.lua
index c7e00d3c4ee7964d6447f61cdf941d574c6d1a25..6b52f43546921ed5dc7beae9bb48f8af6a3128d5 100644
--- a/package/gluon-core/luasrc/usr/lib/lua/gluon/util.lua
+++ b/package/gluon-core/luasrc/usr/lib/lua/gluon/util.lua
@@ -1,3 +1,12 @@
+local bit = require 'bit'
+local posix_glob = require 'posix.glob'
+local hash = require 'hash'
+local sysconfig = require 'gluon.sysconfig'
+local site = require 'gluon.site'
+
+
+local M = {}
+
 -- Writes all lines from the file input to the file output except those starting with prefix
 -- Doesn't close the output file, but returns the file object
 local function do_filter_prefix(input, output, prefix)
@@ -13,29 +22,11 @@ local function do_filter_prefix(input, output, prefix)
 	return f
 end
 
-
-local io = io
-local os = os
-local string = string
-local tonumber = tonumber
-local ipairs = ipairs
-local pairs = pairs
-local table = table
-
-local bit = require 'bit'
-local posix_glob = require 'posix.glob'
-local hash = require 'hash'
-local sysconfig = require 'gluon.sysconfig'
-local site = require 'gluon.site'
-
-
-module 'gluon.util'
-
-function trim(str)
+function M.trim(str)
 	return str:gsub("^%s*(.-)%s*$", "%1")
 end
 
-function contains(table, value)
+function M.contains(table, value)
 	for k, v in pairs(table) do
 		if value == v then
 			return k
@@ -44,7 +35,7 @@ function contains(table, value)
 	return false
 end
 
-function add_to_set(t, itm)
+function M.add_to_set(t, itm)
 	for _,v in ipairs(t) do
 		if v == itm then return false end
 	end
@@ -52,7 +43,7 @@ function add_to_set(t, itm)
 	return true
 end
 
-function remove_from_set(t, itm)
+function M.remove_from_set(t, itm)
 	local i = 1
 	local changed = false
 	while i <= #t do
@@ -67,7 +58,7 @@ function remove_from_set(t, itm)
 end
 
 -- Removes all lines starting with a prefix from a file, optionally adding a new one
-function replace_prefix(file, prefix, add)
+function M.replace_prefix(file, prefix, add)
 	local tmp = file .. '.tmp'
 	local f = do_filter_prefix(file, tmp, prefix)
 	if add then
@@ -87,23 +78,23 @@ local function readall(f)
 	return data
 end
 
-function readfile(file)
+function M.readfile(file)
 	return readall(io.open(file))
 end
 
-function exec(command)
+function M.exec(command)
 	return readall(io.popen(command))
 end
 
-function node_id()
+function M.node_id()
 	return string.gsub(sysconfig.primary_mac, ':', '')
 end
 
-function default_hostname()
-	return site.hostname_prefix('') .. node_id()
+function M.default_hostname()
+	return site.hostname_prefix('') .. M.node_id()
 end
 
-function domain_seed_bytes(key, length)
+function M.domain_seed_bytes(key, length)
 	local ret = ''
 	local v = ''
 	local i = 0
@@ -119,7 +110,7 @@ function domain_seed_bytes(key, length)
 	return ret:sub(0, 2*length)
 end
 
-function get_mesh_devices(uconn)
+function M.get_mesh_devices(uconn)
 	local dump = uconn:call("network.interface", "dump", {})
 	local devices = {}
 	for _, interface in ipairs(dump.interface) do
@@ -132,13 +123,13 @@ end
 
 -- Safe glob: returns an empty table when the glob fails because of
 -- a non-existing path
-function glob(pattern)
+function M.glob(pattern)
 	return posix_glob.glob(pattern) or {}
 end
 
 local function find_phy_by_path(path)
-	local phy = glob('/sys/devices/' .. path .. '/ieee80211/phy*')[1]
-		or glob('/sys/devices/platform/' .. path .. '/ieee80211/phy*')[1]
+	local phy = M.glob('/sys/devices/' .. path .. '/ieee80211/phy*')[1]
+		or M.glob('/sys/devices/platform/' .. path .. '/ieee80211/phy*')[1]
 
 	if phy then
 		return phy:match('([^/]+)$')
@@ -147,14 +138,14 @@ end
 
 local function find_phy_by_macaddr(macaddr)
 	local addr = macaddr:lower()
-	for _, file in ipairs(glob('/sys/class/ieee80211/*/macaddress')) do
-		if trim(readfile(file)) == addr then
+	for _, file in ipairs(M.glob('/sys/class/ieee80211/*/macaddress')) do
+		if M.trim(M.readfile(file)) == addr then
 			return file:match('([^/]+)/macaddress$')
 		end
 	end
 end
 
-function find_phy(config)
+function M.find_phy(config)
 	if not config or config.type ~= 'mac80211' then
 		return nil
 	elseif config.path then
@@ -167,7 +158,7 @@ function find_phy(config)
 end
 
 local function get_addresses(uci, radio)
-	local phy = find_phy(radio)
+	local phy = M.find_phy(radio)
 	if not phy then
 		return function() end
 	end
@@ -187,7 +178,7 @@ end
 -- 5: mesh1
 -- 6: ibss1
 -- 7: wan_radio1 (private WLAN); mesh VPN
-function generate_mac(i)
+function M.generate_mac(i)
 	if i > 7 or i < 0 then return nil end -- max allowed id (0b111)
 
 	local hashed = string.sub(hash.md5(sysconfig.primary_mac), 0, 12)
@@ -231,19 +222,19 @@ local function get_wlan_mac_from_driver(uci, radio, vif)
 	end
 end
 
-function get_wlan_mac(uci, radio, index, vif)
+function M.get_wlan_mac(uci, radio, index, vif)
 	local addr = get_wlan_mac_from_driver(uci, radio, vif)
 	if addr then
 		return addr
 	end
 
-	return generate_mac(4*(index-1) + (vif-1))
+	return M.generate_mac(4*(index-1) + (vif-1))
 end
 
 -- Iterate over all radios defined in UCI calling
 -- f(radio, index, site.wifiX) for each radio found while passing
 --  site.wifi24 for 2.4 GHz devices and site.wifi5 for 5 GHz ones.
-function foreach_radio(uci, f)
+function M.foreach_radio(uci, f)
 	local radios = {}
 
 	uci:foreach('wireless', 'wifi-device', function(radio)
@@ -261,11 +252,13 @@ function foreach_radio(uci, f)
 	end
 end
 
-function get_uptime()
-	local uptime_file = readfile("/proc/uptime")
+function M.get_uptime()
+	local uptime_file = M.readfile("/proc/uptime")
 	if uptime_file == nil then
 		-- Something went wrong reading "/proc/uptime"
 		return nil
 	end
 	return tonumber(uptime_file:match('^[^ ]+'))
 end
+
+return M
diff --git a/package/gluon-mesh-batman-adv/files/lib/netifd/proto/gluon_bat0.sh b/package/gluon-mesh-batman-adv/files/lib/netifd/proto/gluon_bat0.sh
index ea562c498efdb6b7de42f89e3f9c50216013102e..5ce051ea475ae7e2760f4c22c706e3f4413dae12 100755
--- a/package/gluon-mesh-batman-adv/files/lib/netifd/proto/gluon_bat0.sh
+++ b/package/gluon-mesh-batman-adv/files/lib/netifd/proto/gluon_bat0.sh
@@ -36,7 +36,7 @@ proto_gluon_bat0_renew() {
 proto_gluon_bat0_setup() {
 	local config="$1"
 
-	local primary0_mac="$(lua -lgluon.util -e 'print(gluon.util.generate_mac(3))')"
+	local primary0_mac="$(lua -e 'print(require("gluon.util").generate_mac(3))')"
 
 	ip link add primary0 type dummy
 	echo 1 > /proc/sys/net/ipv6/conf/primary0/disable_ipv6
diff --git a/package/gluon-web-mesh-vpn-fastd/luasrc/lib/gluon/config-mode/model/admin/mesh_vpn_fastd.lua b/package/gluon-web-mesh-vpn-fastd/luasrc/lib/gluon/config-mode/model/admin/mesh_vpn_fastd.lua
index a60056936360279059ebd00760288052e4c0e31b..44f82144062d92e734f4a27a9aa16a00398de9bd 100644
--- a/package/gluon-web-mesh-vpn-fastd/luasrc/lib/gluon/config-mode/model/admin/mesh_vpn_fastd.lua
+++ b/package/gluon-web-mesh-vpn-fastd/luasrc/lib/gluon/config-mode/model/admin/mesh_vpn_fastd.lua
@@ -1,5 +1,5 @@
 local uci = require("simple-uci").cursor()
-local util = gluon.util
+local util = require 'gluon.util'
 
 local f = Form(translate('Mesh VPN'))
 
diff --git a/package/gluon-web-model/luasrc/usr/lib/lua/gluon/web/model.lua b/package/gluon-web-model/luasrc/usr/lib/lua/gluon/web/model.lua
index 513fb65e59ece23ddff8212487df93faa3cdb073..5916fdcffdadca6e1f35f3c55546837f37359a73 100644
--- a/package/gluon-web-model/luasrc/usr/lib/lua/gluon/web/model.lua
+++ b/package/gluon-web-model/luasrc/usr/lib/lua/gluon/web/model.lua
@@ -2,8 +2,6 @@
 -- Copyright 2017-2018 Matthias Schiffer <mschiffer@universe-factory.net>
 -- Licensed to the public under the Apache License 2.0.
 
-module('gluon.web.model', package.seeall)
-
 local unistd = require 'posix.unistd'
 local classes = require 'gluon.web.model.classes'
 
diff --git a/package/gluon-web-model/luasrc/usr/lib/lua/gluon/web/model/classes.lua b/package/gluon-web-model/luasrc/usr/lib/lua/gluon/web/model/classes.lua
index 927b3f0594e3645265baf912ee82a63a82a8830a..1ef61c12c78b695b4352d87ebaacd416202877ce 100644
--- a/package/gluon-web-model/luasrc/usr/lib/lua/gluon/web/model/classes.lua
+++ b/package/gluon-web-model/luasrc/usr/lib/lua/gluon/web/model/classes.lua
@@ -2,17 +2,18 @@
 -- Copyright 2017-2018 Matthias Schiffer <mschiffer@universe-factory.net>
 -- Licensed to the public under the Apache License 2.0.
 
-module("gluon.web.model.classes", package.seeall)
-
 local util = require "gluon.web.util"
 
 local datatypes  = require "gluon.web.model.datatypes"
 local class      = util.class
 local instanceof = util.instanceof
 
-FORM_NODATA  =  0
-FORM_VALID   =  1
-FORM_INVALID = -1
+
+local M = {}
+
+M.FORM_NODATA  =  0
+M.FORM_VALID   =  1
+M.FORM_INVALID = -1
 
 
 local function parse_datatype(code)
@@ -41,7 +42,8 @@ local function verify_datatype(dt, value)
 end
 
 
-Node = class()
+local Node = class()
+M.Node = Node
 
 function Node:__init__(name, title, description)
 	self.children = {}
@@ -107,7 +109,8 @@ function Node:handle()
 end
 
 
-Template = class(Node)
+local Template = class(Node)
+M.Template = Template
 
 function Template:__init__(template)
 	Node.__init__(self)
@@ -115,75 +118,8 @@ function Template:__init__(template)
 end
 
 
-Form = class(Node)
-
-function Form:__init__(title, description, name)
-	Node.__init__(self, name, title, description)
-	self.template = "model/form"
-end
-
-function Form:submitstate(http)
-	return http:getenv("REQUEST_METHOD") == "POST" and http:formvalue(self:id()) ~= nil
-end
-
-function Form:parse(http)
-	if not self:submitstate(http) then
-		self.state = FORM_NODATA
-		return
-	end
-
-	Node.parse(self, http)
-
-	while self:resolve_depends() do end
-
-	for _, s in ipairs(self.children) do
-		for _, v in ipairs(s.children) do
-			if v.state == FORM_INVALID then
-				self.state = FORM_INVALID
-				return
-			end
-		end
-	end
-
-	self.state = FORM_VALID
-end
-
-function Form:handle()
-	if self.state == FORM_VALID then
-		Node.handle(self)
-		self:write()
-	end
-end
-
-function Form:write()
-end
-
-function Form:section(t, ...)
-	assert(instanceof(t, Section), "class must be a descendent of Section")
-
-	local obj  = t(...)
-	self:append(obj)
-	return obj
-end
-
-
-Section = class(Node)
-
-function Section:__init__(title, description, name)
-	Node.__init__(self, name, title, description)
-	self.template = "model/section"
-end
-
-function Section:option(t, ...)
-	assert(instanceof(t, AbstractValue), "class must be a descendant of AbstractValue")
-
-	local obj  = t(...)
-	self:append(obj)
-	return obj
-end
-
-
-AbstractValue = class(Node)
+local AbstractValue = class(Node)
+M.AbstractValue = AbstractValue
 
 function AbstractValue:__init__(...)
 	Node.__init__(self, ...)
@@ -195,7 +131,7 @@ function AbstractValue:__init__(...)
 
 	self.template  = "model/valuewrapper"
 
-	self.state = FORM_NODATA
+	self.state = M.FORM_NODATA
 end
 
 function AbstractValue:depends(field, value)
@@ -234,7 +170,7 @@ function AbstractValue:formvalue(http)
 end
 
 function AbstractValue:cfgvalue()
-	if self.state == FORM_NODATA then
+	if self.state == M.FORM_NODATA then
 		return self:defaultvalue()
 	else
 		return self.data
@@ -250,7 +186,7 @@ function AbstractValue:add_error(type, msg)
 		self.tag_missing = true
 	end
 
-	self.state = FORM_INVALID
+	self.state = M.FORM_INVALID
 end
 
 function AbstractValue:reset()
@@ -258,7 +194,7 @@ function AbstractValue:reset()
 	self.tag_invalid = nil
 	self.tag_missing = nil
 	self.data = nil
-	self.state = FORM_NODATA
+	self.state = M.FORM_NODATA
 
 end
 
@@ -275,18 +211,18 @@ function AbstractValue:parse(http)
 		return
 	end
 
-	self.state = FORM_VALID
+	self.state = M.FORM_VALID
 end
 
 function AbstractValue:resolve_depends()
-	if self.state == FORM_NODATA or #self.deps == 0 then
+	if self.state == M.FORM_NODATA or #self.deps == 0 then
 		return false
 	end
 
 	for _, d in ipairs(self.deps) do
 		local valid = true
 		for k, v in pairs(d) do
-			if k.state ~= FORM_VALID or k.data ~= v then
+			if k.state ~= M.FORM_VALID or k.data ~= v then
 				valid = false
 				break
 			end
@@ -316,7 +252,7 @@ function AbstractValue:validate()
 end
 
 function AbstractValue:handle()
-	if self.state == FORM_VALID then
+	if self.state == M.FORM_VALID then
 		self:write(self.data)
 	end
 end
@@ -325,7 +261,8 @@ function AbstractValue:write(value)
 end
 
 
-Value = class(AbstractValue)
+local Value = class(AbstractValue)
+M.Value = Value
 
 function Value:__init__(...)
 	AbstractValue.__init__(self, ...)
@@ -333,7 +270,8 @@ function Value:__init__(...)
 end
 
 
-Flag = class(AbstractValue)
+local Flag = class(AbstractValue)
+M.Flag = Flag
 
 function Flag:__init__(...)
 	AbstractValue.__init__(self, ...)
@@ -351,7 +289,8 @@ function Flag:validate()
 end
 
 
-ListValue = class(AbstractValue)
+local ListValue = class(AbstractValue)
+M.ListValue = ListValue
 
 function ListValue:__init__(...)
 	AbstractValue.__init__(self, ...)
@@ -411,7 +350,8 @@ function ListValue:validate()
 end
 
 
-DynamicList = class(AbstractValue)
+local DynamicList = class(AbstractValue)
+M.DynamicList = DynamicList
 
 function DynamicList:__init__(...)
 	AbstractValue.__init__(self, ...)
@@ -450,9 +390,83 @@ function DynamicList:validate()
 end
 
 
-TextValue = class(AbstractValue)
+local TextValue = class(AbstractValue)
+M.TextValue = TextValue
 
 function TextValue:__init__(...)
 	AbstractValue.__init__(self, ...)
 	self.subtemplate  = "model/tvalue"
 end
+
+
+local Section = class(Node)
+M.Section = Section
+
+function Section:__init__(title, description, name)
+	Node.__init__(self, name, title, description)
+	self.template = "model/section"
+end
+
+function Section:option(t, ...)
+	assert(instanceof(t, AbstractValue), "class must be a descendant of AbstractValue")
+
+	local obj  = t(...)
+	self:append(obj)
+	return obj
+end
+
+
+local Form = class(Node)
+M.Form = Form
+
+function Form:__init__(title, description, name)
+	Node.__init__(self, name, title, description)
+	self.template = "model/form"
+end
+
+function Form:submitstate(http)
+	return http:getenv("REQUEST_METHOD") == "POST" and http:formvalue(self:id()) ~= nil
+end
+
+function Form:parse(http)
+	if not self:submitstate(http) then
+		self.state = M.FORM_NODATA
+		return
+	end
+
+	Node.parse(self, http)
+
+	while self:resolve_depends() do end
+
+	for _, s in ipairs(self.children) do
+		for _, v in ipairs(s.children) do
+			if v.state == M.FORM_INVALID then
+				self.state = M.FORM_INVALID
+				return
+			end
+		end
+	end
+
+	self.state = M.FORM_VALID
+end
+
+function Form:handle()
+	if self.state == M.FORM_VALID then
+		Node.handle(self)
+		self:write()
+	end
+end
+
+function Form:write()
+end
+
+function Form:section(t, ...)
+	assert(instanceof(t, Section), "class must be a descendent of Section")
+
+	local obj  = t(...)
+	self:append(obj)
+	return obj
+end
+
+
+return M
diff --git a/package/gluon-web-model/luasrc/usr/lib/lua/gluon/web/model/datatypes.lua b/package/gluon-web-model/luasrc/usr/lib/lua/gluon/web/model/datatypes.lua
index 49f55c70474b53cc57eb9cf8d5e486c08439927c..fdee7d7dbe2d10518b8ab967dfe04a9c2cb9485a 100644
--- a/package/gluon-web-model/luasrc/usr/lib/lua/gluon/web/model/datatypes.lua
+++ b/package/gluon-web-model/luasrc/usr/lib/lua/gluon/web/model/datatypes.lua
@@ -2,13 +2,9 @@
 -- Copyright 2017 Matthias Schiffer <mschiffer@universe-factory.net>
 -- Licensed to the public under the Apache License 2.0.
 
-local tonumber = tonumber
+local M = {}
 
-
-module "gluon.web.model.datatypes"
-
-
-function bool(val)
+function M.bool(val)
 	if val == "1" or val == "yes" or val == "on" or val == "true" then
 		return true
 	elseif val == "0" or val == "no" or val == "off" or val == "false" then
@@ -32,29 +28,29 @@ local function int(val)
 	end
 end
 
-function uinteger(val)
+function M.uinteger(val)
 	local n = int(val)
 	return (n ~= nil and n >= 0)
 end
 
-function integer(val)
+function M.integer(val)
 	return (int(val) ~= nil)
 end
 
-function ufloat(val)
+function M.ufloat(val)
 	local n = dec(val)
 	return (n ~= nil and n >= 0)
 end
 
-function float(val)
+function M.float(val)
 	return (dec(val) ~= nil)
 end
 
-function ipaddr(val)
-	return ip4addr(val) or ip6addr(val)
+function M.ipaddr(val)
+	return M.ip4addr(val) or M.ip6addr(val)
 end
 
-function ip4addr(val)
+function M.ip4addr(val)
 	local g = '(%d%d?%d?)'
 	local v1, v2, v3, v4 = val:match('^'..((g..'%.'):rep(3))..g..'$')
 	local n1, n2, n3, n4 = tonumber(v1), tonumber(v2), tonumber(v3), tonumber(v4)
@@ -69,7 +65,7 @@ function ip4addr(val)
 	)
 end
 
-function ip6addr(val)
+function M.ip6addr(val)
 	local g1 = '%x%x?%x?%x?'
 
 	if not val:match('::') then
@@ -100,7 +96,7 @@ function ip6addr(val)
 	return false
 end
 
-function wpakey(val)
+function M.wpakey(val)
 	if #val == 64 then
 		return (val:match("^%x+$") ~= nil)
 	else
@@ -108,11 +104,11 @@ function wpakey(val)
 	end
 end
 
-function range(val, vmin, vmax)
-	return min(val, vmin) and max(val, vmax)
+function M.range(val, vmin, vmax)
+	return M.min(val, vmin) and M.max(val, vmax)
 end
 
-function min(val, min)
+function M.min(val, min)
 	val = dec(val)
 	min = tonumber(min)
 
@@ -123,7 +119,7 @@ function min(val, min)
 	return false
 end
 
-function max(val, max)
+function M.max(val, max)
 	val = dec(val)
 	max = tonumber(max)
 
@@ -134,19 +130,19 @@ function max(val, max)
 	return false
 end
 
-function irange(val, vmin, vmax)
-	return integer(val) and range(val, vmin, vmax)
+function M.irange(val, vmin, vmax)
+	return M.integer(val) and M.range(val, vmin, vmax)
 end
 
-function imin(val, vmin)
-	return integer(val) and min(val, vmin)
+function M.imin(val, vmin)
+	return M.integer(val) and M.min(val, vmin)
 end
 
-function imax(val, vmax)
-	return integer(val) and max(val, vmax)
+function M.imax(val, vmax)
+	return M.integer(val) and M.max(val, vmax)
 end
 
-function minlength(val, min)
+function M.minlength(val, min)
 	min = tonumber(min)
 
 	if min ~= nil then
@@ -156,7 +152,7 @@ function minlength(val, min)
 	return false
 end
 
-function maxlength(val, max)
+function M.maxlength(val, max)
 	max = tonumber(max)
 
 	if max ~= nil then
@@ -165,3 +161,5 @@ function maxlength(val, max)
 
 	return false
 end
+
+return M
diff --git a/package/gluon-web-osm/luasrc/usr/lib/lua/gluon/web/model/osm.lua b/package/gluon-web-osm/luasrc/usr/lib/lua/gluon/web/model/osm.lua
index 138a5ec81fa13458c9b81c45ff22829b3a520e8f..cf07c2285343743d46a8402b0a8223623f3b192c 100644
--- a/package/gluon-web-osm/luasrc/usr/lib/lua/gluon/web/model/osm.lua
+++ b/package/gluon-web-osm/luasrc/usr/lib/lua/gluon/web/model/osm.lua
@@ -1,5 +1,3 @@
-module('gluon.web.model.osm', package.seeall)
-
 local classes = require 'gluon.web.model.classes'
 local util = require "gluon.web.util"
 
@@ -9,7 +7,10 @@ local class = util.class
 local DEFAULT_URL = 'https://cdn.rawgit.com/openlayers/openlayers.github.io/master/en/v5.2.0'
 
 
-MapValue = class(classes.AbstractValue)
+local M = {}
+
+local MapValue = class(classes.AbstractValue)
+M.MapValue = MapValue
 
 function MapValue:__init__(title, options)
 	classes.AbstractValue.__init__(self, title)
@@ -41,3 +42,5 @@ end
 function MapValue:validate()
 	return true
 end
+
+return M
diff --git a/package/gluon-web/luasrc/usr/lib/lua/gluon/web/http.lua b/package/gluon-web/luasrc/usr/lib/lua/gluon/web/http.lua
index 2a6a7428544bd30b3c48fbb87c73940bc910ea82..127a492ac5c9ee6d472dc7c2f41f78f04a61c41c 100644
--- a/package/gluon-web/luasrc/usr/lib/lua/gluon/web/http.lua
+++ b/package/gluon-web/luasrc/usr/lib/lua/gluon/web/http.lua
@@ -2,17 +2,15 @@
 -- Copyright 2017 Matthias Schiffer <mschiffer@universe-factory.net>
 -- Licensed to the public under the Apache License 2.0.
 
-local string = string
-local table = table
 local protocol = require "gluon.web.http.protocol"
 local util  = require "gluon.web.util"
 
-local ipairs, pairs, tostring = ipairs, pairs, tostring
 
-module "gluon.web.http"
+local M = {}
 
+local Http = util.class()
+M.Http = Http
 
-Http = util.class()
 function Http:__init__(env, input, output)
 	self.input = input
 	self.output = output
@@ -120,3 +118,5 @@ function Http:redirect(url)
 	self:header("Location", url)
 	self:close()
 end
+
+return M
diff --git a/package/gluon-web/luasrc/usr/lib/lua/gluon/web/http/protocol.lua b/package/gluon-web/luasrc/usr/lib/lua/gluon/web/http/protocol.lua
index 4e8df203ed8cfa9b428eba9a1c17da169640594c..48797b6a0ce0e4d0a2e7e48d4c64ab8f5aad9303 100644
--- a/package/gluon-web/luasrc/usr/lib/lua/gluon/web/http/protocol.lua
+++ b/package/gluon-web/luasrc/usr/lib/lua/gluon/web/http/protocol.lua
@@ -4,11 +4,8 @@
 
 -- This class contains several functions useful for http message- and content
 -- decoding and to retrive form data from raw http messages.
-module("gluon.web.http.protocol", package.seeall)
-
-
-HTTP_MAX_CONTENT      = 1024*8		-- 8 kB maximum content size
 
+local M = {}
 
 local function pump(src, snk)
 	while true do
@@ -26,7 +23,7 @@ local function pump(src, snk)
 	end
 end
 
-function urlencode(s)
+function M.urlencode(s)
 	return (string.gsub(s, '[^a-zA-Z0-9%-_%.~]',
 		function(c)
 			local ret = ''
@@ -41,7 +38,7 @@ function urlencode(s)
 end
 
 -- the "+" sign to " " - and return the decoded string.
-function urldecode(str, no_plus)
+function M.urldecode(str, no_plus)
 
 	local function chrdec(hex)
 		return string.char(tonumber(hex, 16))
@@ -75,7 +72,7 @@ end
 -- Simple parameters are stored as string values associated with the parameter
 -- name within the table. Parameters with multiple values are stored as array
 -- containing the corresponding values.
-function urldecode_params(url)
+function M.urldecode_params(url)
 	local params = {}
 
 	if url:find("?") then
@@ -85,8 +82,8 @@ function urldecode_params(url)
 	for pair in url:gmatch("[^&;]+") do
 
 		-- find key and value
-		local key = urldecode(pair:match("^([^=]+)"))
-		local val = urldecode(pair:match("^[^=]+=(.+)$"))
+		local key = M.urldecode(pair:match("^([^=]+)"))
+		local val = M.urldecode(pair:match("^[^=]+=(.+)$"))
 
 		-- store
 		if key and key:len() > 0 then
@@ -110,7 +107,7 @@ end
 --  o Table containing decoded (name, file) and raw (headers) mime header data
 --  o String value containing a chunk of the file data
 --  o Boolean which indicates whether the current chunk is the last one (eof)
-function mimedecode_message_body(src, msg, filecb)
+local function mimedecode_message_body(src, msg, filecb)
 
 	if msg and msg.env.CONTENT_TYPE then
 		msg.mime_boundary = msg.env.CONTENT_TYPE:match("^multipart/form%-data; boundary=(.+)$")
@@ -257,7 +254,7 @@ end
 -- This function will examine the Content-Type within the given message object
 -- to select the appropriate content decoder.
 -- Currently only the multipart/form-data mime type is supported.
-function parse_message_body(src, msg, filecb)
+function M.parse_message_body(src, msg, filecb)
 	if not (msg.env.REQUEST_METHOD == "POST" and msg.env.CONTENT_TYPE) then
 		return
 	end
@@ -266,3 +263,5 @@ function parse_message_body(src, msg, filecb)
 		return mimedecode_message_body(src, msg, filecb)
 	end
 end
+
+return M
diff --git a/package/gluon-web/luasrc/usr/lib/lua/gluon/web/util.lua b/package/gluon-web/luasrc/usr/lib/lua/gluon/web/util.lua
index 045a16fbc04f5fa7646947d586f982a3fa95470b..fe8fbbff36c1a206e586d048ba25cf5db515b11c 100644
--- a/package/gluon-web/luasrc/usr/lib/lua/gluon/web/util.lua
+++ b/package/gluon-web/luasrc/usr/lib/lua/gluon/web/util.lua
@@ -4,10 +4,8 @@
 
 local tparser = require "gluon.web.template.parser"
 
-local getmetatable, setmetatable = getmetatable, setmetatable
-local tostring = tostring
 
-module "gluon.web.util"
+local M = {}
 
 --
 -- Class helper routines
@@ -33,14 +31,14 @@ end
 -- to the __init__ function of this class - if such a function exists.
 -- The __init__ function must be used to set any object parameters that are not shared
 -- with other objects of this class. Any return values will be ignored.
-function class(base)
+function M.class(base)
 	return setmetatable({}, {
 		__call  = _instantiate,
 		__index = base
 	})
 end
 
-function instanceof(object, class)
+function M.instanceof(object, class)
 	while object do
 		if object == class then
 			return true
@@ -56,6 +54,8 @@ end
 -- String and data manipulation routines
 --
 
-function pcdata(value)
+function M.pcdata(value)
 	return value and tparser.pcdata(tostring(value))
 end
+
+return M