diff --git a/TODO b/TODO
new file mode 100644
index 000000000..1e69865f8
--- /dev/null
+++ b/TODO
@@ -0,0 +1,5 @@
+# Miration to webkit2gtk-4.1, javascriptcore-4.1 and libsoup-3.0
+
+Documents:
+- https://webkitgtk.org/reference/webkit2gtk/stable/
+- https://libsoup.org/libsoup-3.0/migrating-from-libsoup-2.html
diff --git a/clib/request.c b/clib/request.c
index 6d9f847b7..b66146396 100644
--- a/clib/request.c
+++ b/clib/request.c
@@ -85,7 +85,7 @@ luaH_request_finish(lua_State *L)
     size_t length;
     const gchar *data = lua_tolstring(L, 2, &length);
     const gchar *mime = lua_tostring(L, 3) ?: "text/html";
-    GInputStream *stream = g_memory_input_stream_new_from_data(g_memdup(data, length), length, g_free);
+    GInputStream *stream = g_memory_input_stream_new_from_data(g_memdup2(data, length), length, g_free);
     webkit_uri_scheme_request_finish(request->request, stream, length, mime);
     g_object_unref(stream);
 
diff --git a/clib/unique.c b/clib/unique.c
index 2b947ecd4..2a0c23ff0 100644
--- a/clib/unique.c
+++ b/clib/unique.c
@@ -78,7 +78,7 @@ luaH_unique_new(lua_State *L)
 
     GError *error = NULL;
     if (!globalconf.application)
-        globalconf.application = gtk_application_new(name, G_APPLICATION_FLAGS_NONE);
+        globalconf.application = gtk_application_new(name, G_APPLICATION_DEFAULT_FLAGS);
 
     g_application_register(G_APPLICATION(globalconf.application), NULL, &error);
     if (error != NULL) {
diff --git a/common/clib/soup.h b/common/clib/soup.h
index 2256f6786..30502f2d1 100644
--- a/common/clib/soup.h
+++ b/common/clib/soup.h
@@ -30,59 +30,68 @@ luaH_soup_uri_tostring(lua_State *L)
     gint port;
     /* check for uri table */
     luaH_checktable(L, 1);
-    /* create empty soup uri object */
-    SoupURI *uri = soup_uri_new(NULL);
-    soup_uri_set_scheme(uri, "http");
-
-#define GET_PROP(prop)                                          \
-    lua_pushliteral(L, #prop);                                  \
-    lua_rawget(L, 1);                                           \
-    if (!lua_isnil(L, -1) && (p = lua_tostring(L, -1)) && p[0]) \
-        soup_uri_set_##prop(uri, p);                            \
+    /* create empty(ish) uri object */
+    GUri * uri = g_uri_parse ("http://", SOUP_HTTP_URI_FLAGS, NULL);
+    GUri * old_uri = NULL;
+
+#define GET_PROP(prop,PROP)                                              \
+    lua_pushliteral(L, #prop);                                           \
+    lua_rawget(L, 1);                                                    \
+    if (!lua_isnil(L, -1) && (p = lua_tostring(L, -1)) && p[0]) {         \
+        old_uri = uri;                                                   \
+        uri = soup_uri_copy(old_uri, SOUP_URI_##PROP, p, SOUP_URI_NONE); \
+        g_free(old_uri);                                                 \
+    }                                                                    \
     lua_pop(L, 1);
 
-    GET_PROP(scheme)
+    GET_PROP(scheme, SCHEME)
 
     /* If this is a file:// uri, set a default host of ""
      * Without host set, a path of "/home/..." will become "file:/home/..."
      * instead of "file:///home/..."
      */
-    if (soup_uri_get_scheme(uri) == SOUP_URI_SCHEME_FILE) {
-        soup_uri_set_host(uri, "");
+    if (!g_strcmp0(g_uri_get_scheme(uri), "file")) {
+        uri = soup_uri_copy(uri, SOUP_URI_HOST, "", SOUP_URI_NONE);
     }
 
-    GET_PROP(user)
-    GET_PROP(password)
-    GET_PROP(host)
-    GET_PROP(path)
-    GET_PROP(query)
-    GET_PROP(fragment)
+    GET_PROP(user, USER)
+    GET_PROP(password, PASSWORD)
+    GET_PROP(host, HOST)
+    GET_PROP(path, PATH)
+    GET_PROP(query, QUERY)
+    GET_PROP(fragment, FRAGMENT)
 
     lua_pushliteral(L, "port");
     lua_rawget(L, 1);
-    if (!lua_isnil(L, -1) && (port = lua_tonumber(L, -1)))
-        soup_uri_set_port(uri, port);
+    if (!lua_isnil(L, -1) && (port = lua_tonumber(L, -1))) {
+        old_uri = uri;
+        uri = soup_uri_copy(old_uri, SOUP_URI_PORT, port, SOUP_URI_NONE);
+        g_free(old_uri);
+    }
     lua_pop(L, 1);
 
-    gchar *str = soup_uri_to_string(uri, FALSE);
+    gchar *str = g_uri_to_string(uri);
     lua_pushstring(L, str);
     g_free(str);
-    soup_uri_free(uri);
+    g_free(uri);  // GUri is a `struct _GUri` rather than a gobject,
+                  // so i'm guessing it should be `g_free`d rather than `g_object_unref`fed
+
     return 1;
 }
 
 static gint
-luaH_soup_push_uri(lua_State *L, SoupURI *uri)
+luaH_soup_push_uri(lua_State *L, GUri *uri)
 {
     const gchar *p;
+    gint port;
     /* create table for uri properties */
     lua_newtable(L);
 
-#define PUSH_PROP(prop)            \
-    if ((p = uri->prop) && p[0]) { \
-        lua_pushliteral(L, #prop); \
-        lua_pushstring(L, p);      \
-        lua_rawset(L, -3);         \
+#define PUSH_PROP(prop)                        \
+    if ((p = g_uri_get_##prop(uri)) && p[0]) { \
+        lua_pushliteral(L, #prop);             \
+        lua_pushstring(L, p);                  \
+        lua_rawset(L, -3);                     \
     }
 
     PUSH_PROP(scheme)
@@ -93,9 +102,9 @@ luaH_soup_push_uri(lua_State *L, SoupURI *uri)
     PUSH_PROP(query)
     PUSH_PROP(fragment)
 
-    if (uri->port) {
+    if ((port = g_uri_get_port(uri))) {
         lua_pushliteral(L, "port");
-        lua_pushnumber(L, uri->port);
+        lua_pushnumber(L, port);
         lua_rawset(L, -3);
     }
 
@@ -118,13 +127,14 @@ luaH_soup_parse_uri(lua_State *L)
         str = g_strdup(str);
 
     /* parse & push uri */
-    SoupURI *uri = soup_uri_new(str);
+    GUri *uri = g_uri_parse (str, SOUP_HTTP_URI_FLAGS, NULL);
     g_free(str);
     if (uri) {
         luaH_soup_push_uri(L, uri);
-        soup_uri_free(uri);
+        //g_free(uri);  // This free crashes with a double-free.
+        return 1;
     }
-    return uri ? 1 : 0;
+    return 0;
 }
 
 static void
diff --git a/common/property.c b/common/property.c
index 83f1b7bf7..26aa0206a 100644
--- a/common/property.c
+++ b/common/property.c
@@ -22,13 +22,13 @@
 #include <lauxlib.h>
 #include "common/property.h"
 
-#include <libsoup/soup-uri.h>
+#include <libsoup/soup-uri-utils.h>
 #include <webkit2/webkit2.h>
 
 static gint
 luaH_gobject_get(lua_State *L, property_t *p, GObject *object)
 {
-    SoupURI *u;
+    GUri *u; // Per https://libsoup.org/libsoup-3.0/migrating-from-libsoup-2.html
     property_tmp_t tmp;
 
 #define TG_CASE(type, dest, pfunc)                    \
@@ -51,9 +51,10 @@ luaH_gobject_get(lua_State *L, property_t *p, GObject *object)
 
       case URI:
         g_object_get(object, p->name, &u, NULL);
-        tmp.c = u ? soup_uri_to_string(u, 0) : NULL;
+        tmp.c = u ? g_uri_to_string_partial (u, G_URI_HIDE_PASSWORD) : NULL;  // Per https://libsoup.org/libsoup-3.0/migrating-from-libsoup-2.html
         lua_pushstring(L, tmp.c);
-        if (u) soup_uri_free(u);
+        if (u) g_free(u); // This is just a guess.
+                          // Another ???_free might be more appropriate.
         g_free(tmp.c);
         return 1;
 
@@ -67,7 +68,7 @@ luaH_gobject_get(lua_State *L, property_t *p, GObject *object)
 static gboolean
 luaH_gobject_set(lua_State *L, property_t *p, gint vidx, GObject *object)
 {
-    SoupURI *u;
+    GUri *u; // Per https://libsoup.org/libsoup-3.0/migrating-from-libsoup-2.html
     property_tmp_t tmp;
     size_t len;
 
@@ -103,13 +104,24 @@ luaH_gobject_set(lua_State *L, property_t *p, gint vidx, GObject *object)
             tmp.c = g_strdup(tmp.c);
         else
             tmp.c = g_strdup_printf("http://%s", tmp.c);
-        u = soup_uri_new(tmp.c);
-        gboolean valid = !u || SOUP_URI_VALID_FOR_HTTP(u);
+        u = g_uri_parse(tmp.c, SOUP_HTTP_URI_FLAGS, NULL); // Per https://libsoup.org/libsoup-3.0/migrating-from-libsoup-2.html
+
+        /* For reference:
+#define SOUP_URI_VALID_FOR_HTTP(uri)                                                                 \
+        ((uri) && ((uri)->scheme == SOUP_URI_SCHEME_HTTP || (uri)->scheme == SOUP_URI_SCHEME_HTTPS)  \
+               && (uri)->host                                                                        \
+               && (uri)->path)
+         */
+        gboolean valid = !u || ( (!g_strcmp0(g_uri_get_scheme(u), "http")
+                                     || !g_strcmp0(g_uri_get_scheme(u), "https") )
+                                 && g_uri_get_host(u)
+                                 && g_uri_get_path(u) );
         if (valid) {
             g_object_set(object, p->name, u, NULL);
             g_free(tmp.c);
         }
-        soup_uri_free(u);
+        g_free(u); // This is just a guess.
+                   // Another ???_free might be more appropriate.
         if (!valid) {
             lua_pushfstring(L, "invalid uri: %s", tmp.c);
             g_free(tmp.c);
diff --git a/config.mk b/config.mk
index 4fbf77b51..d8ee303cf 100644
--- a/config.mk
+++ b/config.mk
@@ -13,7 +13,7 @@ CPPFLAGS   += -DVERSION=\"$(VERSION)\"
 
 # === Default build options ==================================================
 
-DEVELOPMENT_PATHS ?= 0
+DEVELOPMENT_PATHS ?= 1
 USE_LUAJIT        ?= 1
 
 # === Paths ==================================================================
@@ -98,10 +98,10 @@ endif
 # Packages required to build luakit.
 PKGS += gtk+-3.0
 PKGS += gthread-2.0
-PKGS += webkit2gtk-4.0
+PKGS += webkit2gtk-4.1
 PKGS += sqlite3
 PKGS += $(LUA_PKG_NAME)
-PKGS += javascriptcoregtk-4.0
+PKGS += javascriptcoregtk-4.1
 
 # Check user has correct packages installed (and found by pkg-config).
 PKGS_OK := $(shell $(PKG_CONFIG) --print-errors --exists $(PKGS) && echo 1)
diff --git a/widgets/webview/auth.c b/widgets/webview/auth.c
index 9fe7235fe..4172f3249 100644
--- a/widgets/webview/auth.c
+++ b/widgets/webview/auth.c
@@ -24,7 +24,7 @@
 #include <gtk/gtk.h>
 #include <libsoup/soup-auth.h>
 #include <libsoup/soup-session-feature.h>
-#include <libsoup/soup-uri.h>
+//#include <libsoup/soup-uri.h>
 
 typedef struct {
     WebKitAuthenticationRequest *request;
