diff --git a/go.mod b/go.mod
index f95f2b2..10cdd41 100644
--- a/go.mod
+++ b/go.mod
@@ -3,26 +3,26 @@ module github.com/fiatjaf/nak
 go 1.24.1
 
 require (
+	fiatjaf.com/lib v0.3.1
 	github.com/bep/debounce v1.2.1
 	github.com/btcsuite/btcd/btcec/v2 v2.3.4
 	github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
-	github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0
+	github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0
 	github.com/fatih/color v1.16.0
-	github.com/fiatjaf/eventstore v0.15.0
-	github.com/fiatjaf/khatru v0.16.0
+	github.com/fiatjaf/eventstore v0.16.2
+	github.com/fiatjaf/khatru v0.17.3-0.20250312035319-596bca93c3ff
 	github.com/hanwen/go-fuse/v2 v2.7.2
 	github.com/json-iterator/go v1.1.12
 	github.com/liamg/magic v0.0.1
 	github.com/mailru/easyjson v0.9.0
 	github.com/mark3labs/mcp-go v0.8.3
 	github.com/markusmobius/go-dateparser v1.2.3
-	github.com/nbd-wtf/go-nostr v0.51.2
+	github.com/nbd-wtf/go-nostr v0.51.3-0.20250312034958-cc23d81e8055
 	github.com/urfave/cli/v3 v3.0.0-beta1
-	golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac
+	golang.org/x/exp v0.0.0-20250305212735-054e65f0b394
 )
 
 require (
-	fiatjaf.com/lib v0.2.0 // indirect
 	github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect
 	github.com/andybalholm/brotli v1.1.1 // indirect
 	github.com/btcsuite/btcd v0.24.2 // indirect
@@ -57,7 +57,7 @@ require (
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
-	github.com/puzpuzpuz/xsync/v3 v3.5.0 // indirect
+	github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
 	github.com/rs/cors v1.11.1 // indirect
 	github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect
 	github.com/tetratelabs/wazero v1.8.0 // indirect
@@ -66,12 +66,14 @@ require (
 	github.com/tidwall/pretty v1.2.1 // indirect
 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
 	github.com/valyala/bytebufferpool v1.0.0 // indirect
-	github.com/valyala/fasthttp v1.58.0 // indirect
+	github.com/valyala/fasthttp v1.59.0 // indirect
 	github.com/wasilibs/go-re2 v1.3.0 // indirect
 	github.com/x448/float16 v0.8.4 // indirect
 	golang.org/x/arch v0.15.0 // indirect
-	golang.org/x/crypto v0.32.0 // indirect
-	golang.org/x/net v0.34.0 // indirect
+	golang.org/x/crypto v0.36.0 // indirect
+	golang.org/x/net v0.37.0 // indirect
 	golang.org/x/sys v0.31.0 // indirect
-	golang.org/x/text v0.21.0 // indirect
+	golang.org/x/text v0.23.0 // indirect
 )
+
+replace github.com/nbd-wtf/go-nostr => ../go-nostr
diff --git a/go.sum b/go.sum
index dc27658..2885144 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,5 @@
-fiatjaf.com/lib v0.2.0 h1:TgIJESbbND6GjOgGHxF5jsO6EMjuAxIzZHPo5DXYexs=
-fiatjaf.com/lib v0.2.0/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g=
+fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA=
+fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g=
 github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 h1:ClzzXMDDuUbWfNNZqGeYq4PnYOlwlOVIvSyNaIy0ykg=
 github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3/go.mod h1:we0YA5CsBbH5+/NUzC/AlMmxaDtWlXeNsqrwXjTzmzA=
 github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
@@ -59,8 +59,8 @@ github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn
 github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
 github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
-github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
-github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
 github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
 github.com/dgraph-io/ristretto v1.0.0 h1:SYG07bONKMlFDUYu5pEu3DGAh8c2OFNzKm6G9J4Si84=
 github.com/dgraph-io/ristretto v1.0.0/go.mod h1:jTi2FiYEhQ1NsMmA7DeBykizjOuY88NhKBkepyu1jPc=
@@ -77,10 +77,10 @@ github.com/fasthttp/websocket v1.5.12 h1:e4RGPpWW2HTbL3zV0Y/t7g0ub294LkiuXXUuTOU
 github.com/fasthttp/websocket v1.5.12/go.mod h1:I+liyL7/4moHojiOgUOIKEWm9EIxHqxZChS+aMFltyg=
 github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
 github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
-github.com/fiatjaf/eventstore v0.15.0 h1:5UXe0+vIb30/cYcOWipks8nR3g+X8W224TFy5yPzivk=
-github.com/fiatjaf/eventstore v0.15.0/go.mod h1:KAsld5BhkmSck48aF11Txu8X+OGNmoabw4TlYVWqInc=
-github.com/fiatjaf/khatru v0.16.0 h1:xgGwnnOqE3989wEWm7c/z6Y6g4X92BFe/Xp1UWQ3Zmc=
-github.com/fiatjaf/khatru v0.16.0/go.mod h1:TLcMgPy3IAPh40VGYq6m+gxEMpDKHj+sumqcuvbSogc=
+github.com/fiatjaf/eventstore v0.16.2 h1:h4rHwSwPcqAKqWUsAbYWUhDeSgm2Kp+PBkJc3FgBYu4=
+github.com/fiatjaf/eventstore v0.16.2/go.mod h1:0gU8fzYO/bG+NQAVlHtJWOlt3JKKFefh5Xjj2d1dLIs=
+github.com/fiatjaf/khatru v0.17.3-0.20250312035319-596bca93c3ff h1:b6LYwWlc8zAW6aoZpXYC3Gx/zkP4XW5amDx0VwyeREs=
+github.com/fiatjaf/khatru v0.17.3-0.20250312035319-596bca93c3ff/go.mod h1:dAaXV6QZwuMVYlXQigp/0Uyl/m1nKOhtRssjQYsgMu0=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
 github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
@@ -151,8 +151,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
-github.com/nbd-wtf/go-nostr v0.51.2 h1:wQysG8omkF4LO7kcU6yoeCBBxD92SwUNab4TMeSuZZM=
-github.com/nbd-wtf/go-nostr v0.51.2/go.mod h1:9PcGOZ+e1VOaLvcK0peT4dbip+/eS+eTWXR3HuexQrA=
 github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -166,8 +164,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/puzpuzpuz/xsync/v3 v3.5.0 h1:i+cMcpEDY1BkNm7lPDkCtE4oElsYLn+EKF8kAu2vXT4=
-github.com/puzpuzpuz/xsync/v3 v3.5.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
+github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
+github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
 github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
 github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
 github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc=
@@ -199,8 +197,8 @@ github.com/urfave/cli/v3 v3.0.0-beta1 h1:6DTaaUarcM0wX7qj5Hcvs+5Dm3dyUTBbEwIWAjc
 github.com/urfave/cli/v3 v3.0.0-beta1/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y=
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
-github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE=
-github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw=
+github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI=
+github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU=
 github.com/wasilibs/go-re2 v1.3.0 h1:LFhBNzoStM3wMie6rN2slD1cuYH2CGiHpvNL3UtcsMw=
 github.com/wasilibs/go-re2 v1.3.0/go.mod h1:AafrCXVvGRJJOImMajgJ2M7rVmWyisVK7sFshbxnVrg=
 github.com/wasilibs/nottinygc v0.4.0 h1:h1TJMihMC4neN6Zq+WKpLxgd9xCFMw7O9ETLwY2exJQ=
@@ -214,17 +212,17 @@ golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
 golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
-golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
-golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs=
-golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo=
+golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
+golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
+golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
+golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
 golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
-golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
+golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
+golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -242,8 +240,8 @@ golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
-golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
+golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
+golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/nostrfs/deterministicfile.go b/nostrfs/deterministicfile.go
new file mode 100644
index 0000000..95fe030
--- /dev/null
+++ b/nostrfs/deterministicfile.go
@@ -0,0 +1,50 @@
+package nostrfs
+
+import (
+	"context"
+	"syscall"
+	"unsafe"
+
+	"github.com/hanwen/go-fuse/v2/fs"
+	"github.com/hanwen/go-fuse/v2/fuse"
+)
+
+type DeterministicFile struct {
+	fs.Inode
+	get func() (ctime, mtime uint64, data string)
+}
+
+var (
+	_ = (fs.NodeOpener)((*DeterministicFile)(nil))
+	_ = (fs.NodeReader)((*DeterministicFile)(nil))
+	_ = (fs.NodeGetattrer)((*DeterministicFile)(nil))
+)
+
+func (r *NostrRoot) NewDeterministicFile(get func() (ctime, mtime uint64, data string)) *DeterministicFile {
+	return &DeterministicFile{
+		get: get,
+	}
+}
+
+func (f *DeterministicFile) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) {
+	return nil, fuse.FOPEN_KEEP_CACHE, fs.OK
+}
+
+func (f *DeterministicFile) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
+	var content string
+	out.Mode = 0444
+	out.Ctime, out.Mtime, content = f.get()
+	out.Size = uint64(len(content))
+	return fs.OK
+}
+
+func (f *DeterministicFile) Read(ctx context.Context, fh fs.FileHandle, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) {
+	_, _, content := f.get()
+	data := unsafe.Slice(unsafe.StringData(content), len(content))
+
+	end := int(off) + len(dest)
+	if end > len(data) {
+		end = len(data)
+	}
+	return fuse.ReadResultData(data[off:end]), fs.OK
+}
diff --git a/nostrfs/entitydir.go b/nostrfs/entitydir.go
index c9411bf..9a242aa 100644
--- a/nostrfs/entitydir.go
+++ b/nostrfs/entitydir.go
@@ -9,9 +9,13 @@ import (
 	"net/http"
 	"path/filepath"
 	"strconv"
+	"strings"
 	"syscall"
 	"time"
+	"unsafe"
 
+	"fiatjaf.com/lib/debouncer"
+	"github.com/fatih/color"
 	"github.com/hanwen/go-fuse/v2/fs"
 	"github.com/hanwen/go-fuse/v2/fuse"
 	"github.com/nbd-wtf/go-nostr"
@@ -23,18 +27,29 @@ import (
 
 type EntityDir struct {
 	fs.Inode
-	ctx context.Context
-	wd  string
-	evt *nostr.Event
+	root *NostrRoot
+
+	publisher *debouncer.Debouncer
+	extension string
+	event     *nostr.Event
+	updating  struct {
+		title   string
+		content string
+	}
 }
 
-var _ = (fs.NodeGetattrer)((*EntityDir)(nil))
+var (
+	_ = (fs.NodeOnAdder)((*EntityDir)(nil))
+	_ = (fs.NodeGetattrer)((*EntityDir)(nil))
+	_ = (fs.NodeCreater)((*EntityDir)(nil))
+	_ = (fs.NodeUnlinker)((*EntityDir)(nil))
+)
 
 func (e *EntityDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
-	publishedAt := uint64(e.evt.CreatedAt)
+	publishedAt := uint64(e.event.CreatedAt)
 	out.Ctime = publishedAt
 
-	if tag := e.evt.Tags.Find("published_at"); tag != nil {
+	if tag := e.event.Tags.Find("published_at"); tag != nil {
 		publishedAt, _ = strconv.ParseUint(tag[1], 10, 64)
 	}
 	out.Mtime = publishedAt
@@ -42,119 +57,147 @@ func (e *EntityDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOu
 	return fs.OK
 }
 
-func (r *NostrRoot) FetchAndCreateEntityDir(
-	parent fs.InodeEmbedder,
-	extension string,
-	pointer nostr.EntityPointer,
-) (*fs.Inode, error) {
-	event, _, err := r.sys.FetchSpecificEvent(r.ctx, pointer, sdk.FetchSpecificEventParameters{
-		WithRelays: false,
-	})
-	if err != nil {
-		return nil, fmt.Errorf("failed to fetch: %w", err)
+func (e *EntityDir) Create(
+	_ context.Context,
+	name string,
+	flags uint32,
+	mode uint32,
+	out *fuse.EntryOut,
+) (node *fs.Inode, fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) {
+	if name == "publish" {
+		// this causes the publish process to be triggered faster
+		e.publisher.Flush()
+		return nil, nil, 0, syscall.ENOTDIR
 	}
 
-	return r.CreateEntityDir(parent, extension, event), nil
+	return nil, nil, 0, syscall.ENOTSUP
 }
 
-func (r *NostrRoot) CreateEntityDir(
-	parent fs.InodeEmbedder,
-	extension string,
-	event *nostr.Event,
-) *fs.Inode {
-	log := r.ctx.Value("log").(func(msg string, args ...any))
+func (e *EntityDir) Unlink(ctx context.Context, name string) syscall.Errno {
+	switch name {
+	case "content" + e.extension:
+		e.updating.content = e.event.Content
+		return syscall.ENOTDIR
+	case "title":
+		e.updating.title = ""
+		if titleTag := e.event.Tags.Find("title"); titleTag != nil {
+			e.updating.title = titleTag[1]
+		}
+		return syscall.ENOTDIR
+	default:
+		return syscall.EINTR
+	}
+}
 
-	h := parent.EmbeddedInode().NewPersistentInode(
-		r.ctx,
-		&EntityDir{ctx: r.ctx, wd: r.wd, evt: event},
-		fs.StableAttr{Mode: syscall.S_IFDIR, Ino: hexToUint64(event.ID)},
-	)
-
-	var publishedAt uint64
-	if tag := event.Tags.Find("published_at"); tag != nil {
+func (e *EntityDir) OnAdd(_ context.Context) {
+	log := e.root.ctx.Value("log").(func(msg string, args ...any))
+	publishedAt := uint64(e.event.CreatedAt)
+	if tag := e.event.Tags.Find("published_at"); tag != nil {
 		publishedAt, _ = strconv.ParseUint(tag[1], 10, 64)
 	}
 
-	npub, _ := nip19.EncodePublicKey(event.PubKey)
-	h.AddChild("@author", h.NewPersistentInode(
-		r.ctx,
+	npub, _ := nip19.EncodePublicKey(e.event.PubKey)
+	e.AddChild("@author", e.NewPersistentInode(
+		e.root.ctx,
 		&fs.MemSymlink{
-			Data: []byte(r.wd + "/" + npub),
+			Data: []byte(e.root.wd + "/" + npub),
 		},
 		fs.StableAttr{Mode: syscall.S_IFLNK},
 	), true)
 
-	eventj, _ := json.MarshalIndent(event, "", "  ")
-	h.AddChild("event.json", h.NewPersistentInode(
-		r.ctx,
-		&fs.MemRegularFile{
-			Data: eventj,
-			Attr: fuse.Attr{
-				Mode:  0444,
-				Ctime: uint64(event.CreatedAt),
-				Mtime: uint64(publishedAt),
-				Size:  uint64(len(event.Content)),
+	e.AddChild("event.json", e.NewPersistentInode(
+		e.root.ctx,
+		&DeterministicFile{
+			get: func() (ctime uint64, mtime uint64, data string) {
+				eventj, _ := json.MarshalIndent(e.event, "", "  ")
+				return uint64(e.event.CreatedAt),
+					uint64(e.event.CreatedAt),
+					unsafe.String(unsafe.SliceData(eventj), len(eventj))
 			},
 		},
 		fs.StableAttr{},
 	), true)
 
-	h.AddChild("identifier", h.NewPersistentInode(
-		r.ctx,
+	e.AddChild("identifier", e.NewPersistentInode(
+		e.root.ctx,
 		&fs.MemRegularFile{
-			Data: []byte(event.Tags.GetD()),
+			Data: []byte(e.event.Tags.GetD()),
 			Attr: fuse.Attr{
 				Mode:  0444,
-				Ctime: uint64(event.CreatedAt),
-				Mtime: uint64(publishedAt),
-				Size:  uint64(len(event.Tags.GetD())),
+				Ctime: uint64(e.event.CreatedAt),
+				Mtime: uint64(e.event.CreatedAt),
+				Size:  uint64(len(e.event.Tags.GetD())),
 			},
 		},
 		fs.StableAttr{},
 	), true)
 
-	if tag := event.Tags.Find("title"); tag != nil {
-		h.AddChild("title", h.NewPersistentInode(
-			r.ctx,
-			&fs.MemRegularFile{
-				Data: []byte(tag[1]),
-				Attr: fuse.Attr{
-					Mode:  0444,
-					Ctime: uint64(event.CreatedAt),
-					Mtime: uint64(publishedAt),
-					Size:  uint64(len(tag[1])),
+	if e.root.signer == nil {
+		// read-only
+		e.AddChild("title", e.NewPersistentInode(
+			e.root.ctx,
+			&DeterministicFile{
+				get: func() (ctime uint64, mtime uint64, data string) {
+					var title string
+					if tag := e.event.Tags.Find("title"); tag != nil {
+						title = tag[1]
+					} else {
+						title = e.event.Tags.GetD()
+					}
+					return uint64(e.event.CreatedAt), publishedAt, title
 				},
 			},
 			fs.StableAttr{},
 		), true)
-	}
-
-	h.AddChild("content"+extension, h.NewPersistentInode(
-		r.ctx,
-		&fs.MemRegularFile{
-			Data: []byte(event.Content),
-			Attr: fuse.Attr{
-				Mode:  0444,
-				Ctime: uint64(event.CreatedAt),
-				Mtime: uint64(publishedAt),
-				Size:  uint64(len(event.Content)),
+		e.AddChild("content."+e.extension, e.NewPersistentInode(
+			e.root.ctx,
+			&DeterministicFile{
+				get: func() (ctime uint64, mtime uint64, data string) {
+					return uint64(e.event.CreatedAt), publishedAt, e.event.Content
+				},
 			},
-		},
-		fs.StableAttr{},
-	), true)
+			fs.StableAttr{},
+		), true)
+	} else {
+		// writeable
+		if tag := e.event.Tags.Find("title"); tag != nil {
+			e.updating.title = tag[1]
+		}
+		e.updating.content = e.event.Content
+
+		e.AddChild("title", e.NewPersistentInode(
+			e.root.ctx,
+			e.root.NewWriteableFile(e.updating.title, uint64(e.event.CreatedAt), publishedAt, func(s string) {
+				log("title updated")
+				e.updating.title = strings.TrimSpace(s)
+				e.handleWrite()
+			}),
+			fs.StableAttr{},
+		), true)
+
+		e.AddChild("content."+e.extension, e.NewPersistentInode(
+			e.root.ctx,
+			e.root.NewWriteableFile(e.updating.content, uint64(e.event.CreatedAt), publishedAt, func(s string) {
+				log("content updated")
+				e.updating.content = strings.TrimSpace(s)
+				e.handleWrite()
+			}),
+			fs.StableAttr{},
+		), true)
+	}
 
 	var refsdir *fs.Inode
 	i := 0
-	for ref := range nip27.ParseReferences(*event) {
+	for ref := range nip27.ParseReferences(*e.event) {
 		i++
 		if refsdir == nil {
-			refsdir = h.NewPersistentInode(r.ctx, &fs.Inode{}, fs.StableAttr{Mode: syscall.S_IFDIR})
-			h.AddChild("references", refsdir, true)
+			refsdir = e.NewPersistentInode(e.root.ctx, &fs.Inode{}, fs.StableAttr{Mode: syscall.S_IFDIR})
+			e.root.AddChild("references", refsdir, true)
 		}
 		refsdir.AddChild(fmt.Sprintf("ref_%02d", i), refsdir.NewPersistentInode(
-			r.ctx,
+			e.root.ctx,
 			&fs.MemSymlink{
-				Data: []byte(r.wd + "/" + nip19.EncodePointer(ref.Pointer)),
+				Data: []byte(e.root.wd + "/" + nip19.EncodePointer(ref.Pointer)),
 			},
 			fs.StableAttr{Mode: syscall.S_IFLNK},
 		), true)
@@ -164,15 +207,15 @@ func (r *NostrRoot) CreateEntityDir(
 	addImage := func(url string) {
 		if imagesdir == nil {
 			in := &fs.Inode{}
-			imagesdir = h.NewPersistentInode(r.ctx, in, fs.StableAttr{Mode: syscall.S_IFDIR})
-			h.AddChild("images", imagesdir, true)
+			imagesdir = e.NewPersistentInode(e.root.ctx, in, fs.StableAttr{Mode: syscall.S_IFDIR})
+			e.AddChild("images", imagesdir, true)
 		}
 		imagesdir.AddChild(filepath.Base(url), imagesdir.NewPersistentInode(
-			r.ctx,
+			e.root.ctx,
 			&AsyncFile{
-				ctx: r.ctx,
+				ctx: e.root.ctx,
 				load: func() ([]byte, nostr.Timestamp) {
-					ctx, cancel := context.WithTimeout(r.ctx, time.Second*20)
+					ctx, cancel := context.WithTimeout(e.root.ctx, time.Second*20)
 					defer cancel()
 					r, err := http.NewRequestWithContext(ctx, "GET", url, nil)
 					if err != nil {
@@ -198,7 +241,7 @@ func (r *NostrRoot) CreateEntityDir(
 		), true)
 	}
 
-	images := nip92.ParseTags(event.Tags)
+	images := nip92.ParseTags(e.event.Tags)
 	for _, imeta := range images {
 		if imeta.URL == "" {
 			continue
@@ -206,9 +249,116 @@ func (r *NostrRoot) CreateEntityDir(
 		addImage(imeta.URL)
 	}
 
-	if tag := event.Tags.Find("image"); tag != nil {
+	if tag := e.event.Tags.Find("image"); tag != nil {
 		addImage(tag[1])
 	}
-
-	return h
+}
+
+func (e *EntityDir) handleWrite() {
+	log := e.root.ctx.Value("log").(func(msg string, args ...any))
+
+	if e.publisher.IsRunning() {
+		log(", timer reset")
+	}
+	log(", will publish the updated event in 30 seconds...\n")
+	if !e.publisher.IsRunning() {
+		log("- `touch publish` to publish immediately\n")
+		log("- `rm title content." + e.extension + "` to erase and cancel the edits\n")
+	}
+
+	e.publisher.Call(func() {
+		if currentTitle := e.event.Tags.Find("title"); (currentTitle != nil && currentTitle[1] == e.updating.title) || (currentTitle == nil && e.updating.title == "") && e.updating.content == e.event.Content {
+			log("back into the previous state, not publishing.\n")
+			return
+		}
+
+		evt := nostr.Event{
+			Kind:      e.event.Kind,
+			Content:   e.updating.content,
+			Tags:      make(nostr.Tags, len(e.event.Tags)),
+			CreatedAt: nostr.Now(),
+		}
+		copy(evt.Tags, e.event.Tags)
+		if e.updating.title != "" {
+			if titleTag := evt.Tags.Find("title"); titleTag != nil {
+				titleTag[1] = e.updating.title
+			} else {
+				evt.Tags = append(evt.Tags, nostr.Tag{"title", e.updating.title})
+			}
+		}
+		if publishedAtTag := evt.Tags.Find("published_at"); publishedAtTag == nil {
+			evt.Tags = append(evt.Tags, nostr.Tag{
+				"published_at",
+				strconv.FormatInt(int64(e.event.CreatedAt), 10),
+			})
+		}
+		for ref := range nip27.ParseReferences(evt) {
+			tag := ref.Pointer.AsTag()
+			if existing := evt.Tags.FindWithValue(tag[0], tag[1]); existing == nil {
+				evt.Tags = append(evt.Tags, tag)
+			}
+		}
+		if err := e.root.signer.SignEvent(e.root.ctx, &evt); err != nil {
+			log("failed to sign: '%s'.\n", err)
+			return
+		}
+
+		relays := e.root.sys.FetchWriteRelays(e.root.ctx, evt.PubKey, 8)
+		log("publishing to %d relays... ", len(relays))
+		success := false
+		first := true
+		for res := range e.root.sys.Pool.PublishMany(e.root.ctx, relays, evt) {
+			cleanUrl, ok := strings.CutPrefix(res.RelayURL, "wss://")
+			if !ok {
+				cleanUrl = res.RelayURL
+			}
+
+			if !first {
+				log(", ")
+			}
+			first = false
+
+			if res.Error != nil {
+				log("%s: %s", color.RedString(cleanUrl), res.Error)
+			} else {
+				success = true
+				log("%s: ok", color.GreenString(cleanUrl))
+			}
+		}
+		log("\n")
+
+		if success {
+			e.event = &evt
+			log("event updated locally.\n")
+		} else {
+			log("failed.\n")
+		}
+	})
+}
+
+func (r *NostrRoot) FetchAndCreateEntityDir(
+	parent fs.InodeEmbedder,
+	extension string,
+	pointer nostr.EntityPointer,
+) (*fs.Inode, error) {
+	event, _, err := r.sys.FetchSpecificEvent(r.ctx, pointer, sdk.FetchSpecificEventParameters{
+		WithRelays: false,
+	})
+	if err != nil {
+		return nil, fmt.Errorf("failed to fetch: %w", err)
+	}
+
+	return r.CreateEntityDir(parent, extension, event), nil
+}
+
+func (r *NostrRoot) CreateEntityDir(
+	parent fs.InodeEmbedder,
+	extension string,
+	event *nostr.Event,
+) *fs.Inode {
+	return parent.EmbeddedInode().NewPersistentInode(
+		r.ctx,
+		&EntityDir{root: r, event: event, publisher: debouncer.New(time.Second * 30), extension: extension},
+		fs.StableAttr{Mode: syscall.S_IFDIR, Ino: hexToUint64(event.ID)},
+	)
 }
diff --git a/nostrfs/npubdir.go b/nostrfs/npubdir.go
index 20df761..633137e 100644
--- a/nostrfs/npubdir.go
+++ b/nostrfs/npubdir.go
@@ -108,11 +108,10 @@ func (r *NostrRoot) CreateNpubDir(
 					Kinds:   []int{1},
 					Authors: []string{pointer.PublicKey},
 				},
-				paginate: true,
-				relays:   relays,
-				create: func(n *ViewDir, event *nostr.Event) (string, *fs.Inode) {
-					return event.ID, r.CreateEventDir(n, event)
-				},
+				paginate:    true,
+				relays:      relays,
+				replaceable: false,
+				extension:   "txt",
 			},
 			fs.StableAttr{Mode: syscall.S_IFDIR},
 		),
@@ -129,11 +128,10 @@ func (r *NostrRoot) CreateNpubDir(
 					Kinds:   []int{1111},
 					Authors: []string{pointer.PublicKey},
 				},
-				paginate: true,
-				relays:   relays,
-				create: func(n *ViewDir, event *nostr.Event) (string, *fs.Inode) {
-					return event.ID, r.CreateEventDir(n, event)
-				},
+				paginate:    true,
+				relays:      relays,
+				replaceable: false,
+				extension:   "txt",
 			},
 			fs.StableAttr{Mode: syscall.S_IFDIR},
 		),
@@ -150,11 +148,10 @@ func (r *NostrRoot) CreateNpubDir(
 					Kinds:   []int{20},
 					Authors: []string{pointer.PublicKey},
 				},
-				paginate: true,
-				relays:   relays,
-				create: func(n *ViewDir, event *nostr.Event) (string, *fs.Inode) {
-					return event.ID, r.CreateEventDir(n, event)
-				},
+				paginate:    true,
+				relays:      relays,
+				replaceable: false,
+				extension:   "txt",
 			},
 			fs.StableAttr{Mode: syscall.S_IFDIR},
 		),
@@ -171,11 +168,10 @@ func (r *NostrRoot) CreateNpubDir(
 					Kinds:   []int{21, 22},
 					Authors: []string{pointer.PublicKey},
 				},
-				paginate: false,
-				relays:   relays,
-				create: func(n *ViewDir, event *nostr.Event) (string, *fs.Inode) {
-					return event.ID, r.CreateEventDir(n, event)
-				},
+				paginate:    false,
+				relays:      relays,
+				replaceable: false,
+				extension:   "txt",
 			},
 			fs.StableAttr{Mode: syscall.S_IFDIR},
 		),
@@ -192,11 +188,10 @@ func (r *NostrRoot) CreateNpubDir(
 					Kinds:   []int{9802},
 					Authors: []string{pointer.PublicKey},
 				},
-				paginate: false,
-				relays:   relays,
-				create: func(n *ViewDir, event *nostr.Event) (string, *fs.Inode) {
-					return event.ID, r.CreateEventDir(n, event)
-				},
+				paginate:    false,
+				relays:      relays,
+				replaceable: false,
+				extension:   "txt",
 			},
 			fs.StableAttr{Mode: syscall.S_IFDIR},
 		),
@@ -213,15 +208,10 @@ func (r *NostrRoot) CreateNpubDir(
 					Kinds:   []int{30023},
 					Authors: []string{pointer.PublicKey},
 				},
-				paginate: false,
-				relays:   relays,
-				create: func(n *ViewDir, event *nostr.Event) (string, *fs.Inode) {
-					d := event.Tags.GetD()
-					if d == "" {
-						d = "_"
-					}
-					return d, r.CreateEntityDir(n, ".md", event)
-				},
+				paginate:    false,
+				relays:      relays,
+				replaceable: true,
+				extension:   "md",
 			},
 			fs.StableAttr{Mode: syscall.S_IFDIR},
 		),
@@ -238,15 +228,10 @@ func (r *NostrRoot) CreateNpubDir(
 					Kinds:   []int{30818},
 					Authors: []string{pointer.PublicKey},
 				},
-				paginate: false,
-				relays:   relays,
-				create: func(n *ViewDir, event *nostr.Event) (string, *fs.Inode) {
-					d := event.Tags.GetD()
-					if d == "" {
-						d = "_"
-					}
-					return d, r.CreateEntityDir(n, ".adoc", event)
-				},
+				paginate:    false,
+				relays:      relays,
+				replaceable: true,
+				extension:   "adoc",
 			},
 			fs.StableAttr{Mode: syscall.S_IFDIR},
 		),
diff --git a/nostrfs/viewdir.go b/nostrfs/viewdir.go
index 1a45e91..b4b94a1 100644
--- a/nostrfs/viewdir.go
+++ b/nostrfs/viewdir.go
@@ -12,12 +12,13 @@ import (
 
 type ViewDir struct {
 	fs.Inode
-	root     *NostrRoot
-	fetched  atomic.Bool
-	filter   nostr.Filter
-	paginate bool
-	relays   []string
-	create   func(*ViewDir, *nostr.Event) (string, *fs.Inode)
+	root        *NostrRoot
+	fetched     atomic.Bool
+	filter      nostr.Filter
+	paginate    bool
+	relays      []string
+	replaceable bool
+	extension   string
 }
 
 var (
@@ -49,27 +50,37 @@ func (n *ViewDir) Opendir(_ context.Context) syscall.Errno {
 		aMonthAgo := now - 30*24*60*60
 		n.filter.Since = &aMonthAgo
 
-		for ie := range n.root.sys.Pool.FetchMany(n.root.ctx, n.relays, n.filter, nostr.WithLabel("nakfs")) {
-			basename, inode := n.create(n, ie.Event)
-			n.AddChild(basename, inode, true)
-		}
-
 		filter := n.filter
 		filter.Until = &aMonthAgo
 
 		n.AddChild("@previous", n.NewPersistentInode(
 			n.root.ctx,
 			&ViewDir{
-				root:   n.root,
-				filter: filter,
-				relays: n.relays,
+				root:        n.root,
+				filter:      filter,
+				relays:      n.relays,
+				extension:   n.extension,
+				replaceable: n.replaceable,
 			},
 			fs.StableAttr{Mode: syscall.S_IFDIR},
 		), true)
+	}
+
+	if n.replaceable {
+		for rkey, evt := range n.root.sys.Pool.FetchManyReplaceable(n.root.ctx, n.relays, n.filter,
+			nostr.WithLabel("nakfs"),
+		).Range {
+			name := rkey.D
+			if name == "" {
+				name = "_"
+			}
+			n.AddChild(name, n.root.CreateEntityDir(n, n.extension, evt), true)
+		}
 	} else {
-		for ie := range n.root.sys.Pool.FetchMany(n.root.ctx, n.relays, n.filter, nostr.WithLabel("nakfs")) {
-			basename, inode := n.create(n, ie.Event)
-			n.AddChild(basename, inode, true)
+		for ie := range n.root.sys.Pool.FetchMany(n.root.ctx, n.relays, n.filter,
+			nostr.WithLabel("nakfs"),
+		) {
+			n.AddChild(ie.Event.ID, n.root.CreateEventDir(n, ie.Event), true)
 		}
 	}
 
diff --git a/nostrfs/writeablefile.go b/nostrfs/writeablefile.go
new file mode 100644
index 0000000..2c897fb
--- /dev/null
+++ b/nostrfs/writeablefile.go
@@ -0,0 +1,88 @@
+package nostrfs
+
+import (
+	"context"
+	"sync"
+	"syscall"
+
+	"github.com/hanwen/go-fuse/v2/fs"
+	"github.com/hanwen/go-fuse/v2/fuse"
+)
+
+type WriteableFile struct {
+	fs.Inode
+	root    *NostrRoot
+	mu      sync.Mutex
+	data    []byte
+	attr    fuse.Attr
+	onWrite func(string)
+}
+
+var (
+	_ = (fs.NodeOpener)((*WriteableFile)(nil))
+	_ = (fs.NodeReader)((*WriteableFile)(nil))
+	_ = (fs.NodeWriter)((*WriteableFile)(nil))
+	_ = (fs.NodeGetattrer)((*WriteableFile)(nil))
+	_ = (fs.NodeSetattrer)((*WriteableFile)(nil))
+	_ = (fs.NodeFlusher)((*WriteableFile)(nil))
+)
+
+func (r *NostrRoot) NewWriteableFile(data string, ctime, mtime uint64, onWrite func(string)) *WriteableFile {
+	return &WriteableFile{
+		root: r,
+		data: []byte(data),
+		attr: fuse.Attr{
+			Mode:  0666,
+			Ctime: ctime,
+			Mtime: mtime,
+			Size:  uint64(len(data)),
+		},
+		onWrite: onWrite,
+	}
+}
+
+func (f *WriteableFile) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) {
+	return nil, fuse.FOPEN_KEEP_CACHE, fs.OK
+}
+
+func (f *WriteableFile) Write(ctx context.Context, fh fs.FileHandle, data []byte, off int64) (uint32, syscall.Errno) {
+	f.mu.Lock()
+	defer f.mu.Unlock()
+	end := int64(len(data)) + off
+	if int64(len(f.data)) < end {
+		n := make([]byte, end)
+		copy(n, f.data)
+		f.data = n
+	}
+	copy(f.data[off:off+int64(len(data))], data)
+
+	f.onWrite(string(f.data))
+
+	return uint32(len(data)), fs.OK
+}
+
+func (f *WriteableFile) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
+	f.mu.Lock()
+	defer f.mu.Unlock()
+	out.Attr = f.attr
+	out.Attr.Size = uint64(len(f.data))
+	return fs.OK
+}
+
+func (f *WriteableFile) Setattr(ctx context.Context, fh fs.FileHandle, in *fuse.SetAttrIn, out *fuse.AttrOut) syscall.Errno {
+	return fs.OK
+}
+
+func (f *WriteableFile) Flush(ctx context.Context, fh fs.FileHandle) syscall.Errno {
+	return fs.OK
+}
+
+func (f *WriteableFile) Read(ctx context.Context, fh fs.FileHandle, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) {
+	f.mu.Lock()
+	defer f.mu.Unlock()
+	end := int(off) + len(dest)
+	if end > len(f.data) {
+		end = len(f.data)
+	}
+	return fuse.ReadResultData(f.data[off:end]), fs.OK
+}