diff --git a/fs.go b/fs.go
index 1d8d048..6750a93 100644
--- a/fs.go
+++ b/fs.go
@@ -34,9 +34,15 @@ var fsCmd = &cli.Command{
 			},
 		},
 		&cli.DurationFlag{
-			Name:  "auto-publish",
-			Usage: "delay after which edited articles will be auto-published.",
-			Value: time.Hour * 24 * 365 * 2,
+			Name:  "auto-publish-notes",
+			Usage: "delay after which new notes will be auto-published, set to -1 to not publish.",
+			Value: time.Second * 30,
+		},
+		&cli.DurationFlag{
+			Name:        "auto-publish-articles",
+			Usage:       "delay after which edited articles will be auto-published.",
+			Value:       time.Hour * 24 * 365 * 2,
+			DefaultText: "basically infinite",
 		},
 	),
 	DisableSliceFlagSeparator: true,
@@ -53,6 +59,15 @@ var fsCmd = &cli.Command{
 			kr = keyer.NewReadOnlyUser(c.String("pubkey"))
 		}
 
+		apnt := c.Duration("auto-publish-notes")
+		if apnt < 0 {
+			apnt = time.Hour * 24 * 365 * 3
+		}
+		apat := c.Duration("auto-publish-articles")
+		if apat < 0 {
+			apat = time.Hour * 24 * 365 * 3
+		}
+
 		root := nostrfs.NewNostrRoot(
 			context.WithValue(
 				context.WithValue(
@@ -65,7 +80,8 @@ var fsCmd = &cli.Command{
 			kr,
 			mountpoint,
 			nostrfs.Options{
-				AutoPublishTimeout: c.Duration("auto-publish"),
+				AutoPublishNotesTimeout:    apnt,
+				AutoPublishArticlesTimeout: apat,
 			},
 		)
 
diff --git a/go.mod b/go.mod
index 10cdd41..7a5cb32 100644
--- a/go.mod
+++ b/go.mod
@@ -17,7 +17,7 @@ require (
 	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.3-0.20250312034958-cc23d81e8055
+	github.com/nbd-wtf/go-nostr v0.51.5
 	github.com/urfave/cli/v3 v3.0.0-beta1
 	golang.org/x/exp v0.0.0-20250305212735-054e65f0b394
 )
@@ -75,5 +75,3 @@ require (
 	golang.org/x/sys v0.31.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 2885144..c39a094 100644
--- a/go.sum
+++ b/go.sum
@@ -151,6 +151,8 @@ 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.5 h1:kztpm/JuavVefyuEjG0QaCgDtzHIW9K/Hzq+y9Ph2DY=
+github.com/nbd-wtf/go-nostr v0.51.5/go.mod h1:raIUNOilCdhiVIqgwe+9enCtdXu1iuPjbLh1hO7wTqI=
 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=
diff --git a/nostrfs/entitydir.go b/nostrfs/entitydir.go
index bfdd275..852a53f 100644
--- a/nostrfs/entitydir.go
+++ b/nostrfs/entitydir.go
@@ -269,17 +269,17 @@ func (e *EntityDir) handleWrite() {
 	log := e.root.ctx.Value("log").(func(msg string, args ...any))
 	logverbose := e.root.ctx.Value("logverbose").(func(msg string, args ...any))
 
-	if e.root.opts.AutoPublishTimeout.Hours() < 24*365 {
+	if e.root.opts.AutoPublishArticlesTimeout.Hours() < 24*365 {
 		if e.publisher.IsRunning() {
 			log(", timer reset")
 		}
-		log(", will publish the ")
+		log(", publishing the ")
 		if e.IsNew() {
 			log("new")
 		} else {
 			log("updated")
 		}
-		log(" event in %d seconds...\n", e.root.opts.AutoPublishTimeout.Seconds())
+		log(" event in %d seconds...\n", int(e.root.opts.AutoPublishArticlesTimeout.Seconds()))
 	} else {
 		log(".\n")
 	}
@@ -339,9 +339,9 @@ func (e *EntityDir) handleWrite() {
 		}
 		logverbose("%s\n", evt)
 
-		relays := e.root.sys.FetchWriteRelays(e.root.ctx, evt.PubKey, 8)
+		relays := e.root.sys.FetchWriteRelays(e.root.ctx, e.root.rootPubKey, 8)
 		if len(relays) == 0 {
-			relays = e.root.sys.FetchOutboxRelays(e.root.ctx, evt.PubKey, 6)
+			relays = e.root.sys.FetchOutboxRelays(e.root.ctx, e.root.rootPubKey, 6)
 		}
 
 		log("publishing to %d relays... ", len(relays))
@@ -398,7 +398,7 @@ func (r *NostrRoot) CreateEntityDir(
 ) *fs.Inode {
 	return parent.EmbeddedInode().NewPersistentInode(
 		r.ctx,
-		&EntityDir{root: r, event: event, publisher: debouncer.New(r.opts.AutoPublishTimeout)},
+		&EntityDir{root: r, event: event, publisher: debouncer.New(r.opts.AutoPublishArticlesTimeout)},
 		fs.StableAttr{Mode: syscall.S_IFDIR},
 	)
 }
diff --git a/nostrfs/npubdir.go b/nostrfs/npubdir.go
index b721002..d34a226 100644
--- a/nostrfs/npubdir.go
+++ b/nostrfs/npubdir.go
@@ -122,6 +122,7 @@ func (h *NpubDir) OnAdd(_ context.Context) {
 					paginate:    true,
 					relays:      relays,
 					replaceable: false,
+					createable:  true,
 				},
 				fs.StableAttr{Mode: syscall.S_IFDIR},
 			),
diff --git a/nostrfs/root.go b/nostrfs/root.go
index 510969c..d821bc8 100644
--- a/nostrfs/root.go
+++ b/nostrfs/root.go
@@ -15,7 +15,8 @@ import (
 )
 
 type Options struct {
-	AutoPublishTimeout time.Duration // a negative number means do not publish
+	AutoPublishNotesTimeout    time.Duration
+	AutoPublishArticlesTimeout time.Duration
 }
 
 type NostrRoot struct {
diff --git a/nostrfs/viewdir.go b/nostrfs/viewdir.go
index 838ab05..b861290 100644
--- a/nostrfs/viewdir.go
+++ b/nostrfs/viewdir.go
@@ -2,12 +2,17 @@ package nostrfs
 
 import (
 	"context"
+	"slices"
+	"strings"
 	"sync/atomic"
 	"syscall"
 
+	"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"
+	"github.com/nbd-wtf/go-nostr/nip27"
 )
 
 type ViewDir struct {
@@ -19,14 +24,187 @@ type ViewDir struct {
 	relays      []string
 	replaceable bool
 	createable  bool
+	publisher   *debouncer.Debouncer
+	publishing  struct {
+		note string
+	}
 }
 
 var (
 	_ = (fs.NodeOpendirer)((*ViewDir)(nil))
 	_ = (fs.NodeGetattrer)((*ViewDir)(nil))
 	_ = (fs.NodeMkdirer)((*ViewDir)(nil))
+	_ = (fs.NodeSetattrer)((*ViewDir)(nil))
+	_ = (fs.NodeCreater)((*ViewDir)(nil))
+	_ = (fs.NodeUnlinker)((*ViewDir)(nil))
 )
 
+func (f *ViewDir) Setattr(_ context.Context, _ fs.FileHandle, _ *fuse.SetAttrIn, _ *fuse.AttrOut) syscall.Errno {
+	return fs.OK
+}
+
+func (n *ViewDir) Create(
+	_ context.Context,
+	name string,
+	flags uint32,
+	mode uint32,
+	out *fuse.EntryOut,
+) (node *fs.Inode, fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) {
+	if !n.createable || n.root.rootPubKey != n.filter.Authors[0] {
+		return nil, nil, 0, syscall.EPERM
+	}
+	if n.publisher == nil {
+		n.publisher = debouncer.New(n.root.opts.AutoPublishNotesTimeout)
+	}
+	if n.filter.Kinds[0] != 1 {
+		return nil, nil, 0, syscall.ENOTSUP
+	}
+
+	switch name {
+	case "new":
+		log := n.root.ctx.Value("log").(func(msg string, args ...any))
+
+		if n.publisher.IsRunning() {
+			log("pending note updated, timer reset.")
+		} else {
+			log("new note detected")
+			if n.root.opts.AutoPublishNotesTimeout.Hours() < 24*365 {
+				log(", publishing it in %d seconds...\n", int(n.root.opts.AutoPublishNotesTimeout.Seconds()))
+			} else {
+				log(".\n")
+			}
+			log("- `touch publish` to publish immediately\n")
+			log("- `rm new` to erase and cancel the publication.\n")
+		}
+
+		n.publisher.Call(n.publishNote)
+
+		first := true
+
+		return n.NewPersistentInode(
+			n.root.ctx,
+			n.root.NewWriteableFile(n.publishing.note, uint64(nostr.Now()), uint64(nostr.Now()), func(s string) {
+				if !first {
+					log("pending note updated, timer reset.\n")
+				}
+				first = false
+				n.publishing.note = strings.TrimSpace(s)
+				n.publisher.Call(n.publishNote)
+			}),
+			fs.StableAttr{},
+		), nil, 0, fs.OK
+	case "publish":
+		if n.publisher.IsRunning() {
+			// this causes the publish process to be triggered faster
+			log := n.root.ctx.Value("log").(func(msg string, args ...any))
+			log("publishing now!\n")
+			n.publisher.Flush()
+			return nil, nil, 0, syscall.ENOTDIR
+		}
+	}
+
+	return nil, nil, 0, syscall.ENOTSUP
+}
+
+func (n *ViewDir) Unlink(ctx context.Context, name string) syscall.Errno {
+	if !n.createable || n.root.rootPubKey != n.filter.Authors[0] {
+		return syscall.EPERM
+	}
+	if n.publisher == nil {
+		n.publisher = debouncer.New(n.root.opts.AutoPublishNotesTimeout)
+	}
+	if n.filter.Kinds[0] != 1 {
+		return syscall.ENOTSUP
+	}
+
+	switch name {
+	case "new":
+		log := n.root.ctx.Value("log").(func(msg string, args ...any))
+		log("publishing canceled.\n")
+		n.publisher.Stop()
+		n.publishing.note = ""
+		return fs.OK
+	}
+
+	return syscall.ENOTSUP
+}
+
+func (n *ViewDir) publishNote() {
+	log := n.root.ctx.Value("log").(func(msg string, args ...any))
+
+	log("publishing note...\n")
+	evt := nostr.Event{
+		Kind:      1,
+		CreatedAt: nostr.Now(),
+		Content:   n.publishing.note,
+		Tags:      make(nostr.Tags, 0, 2),
+	}
+
+	// our write relays
+	relays := n.root.sys.FetchWriteRelays(n.root.ctx, n.root.rootPubKey, 8)
+	if len(relays) == 0 {
+		relays = n.root.sys.FetchOutboxRelays(n.root.ctx, n.root.rootPubKey, 6)
+	}
+
+	// add "p" tags from people mentioned and "q" tags from events mentioned
+	for ref := range nip27.ParseReferences(evt) {
+		tag := ref.Pointer.AsTag()
+		key := tag[0]
+		val := tag[1]
+		if key == "e" || key == "a" {
+			key = "q"
+		}
+		if existing := evt.Tags.FindWithValue(key, val); existing == nil {
+			evt.Tags = append(evt.Tags, tag)
+
+			// add their "read" relays
+			if key == "p" {
+				for _, r := range n.root.sys.FetchInboxRelays(n.root.ctx, val, 4) {
+					if !slices.Contains(relays, r) {
+						relays = append(relays, r)
+					}
+				}
+			}
+		}
+	}
+
+	// sign and publish
+	if err := n.root.signer.SignEvent(n.root.ctx, &evt); err != nil {
+		log("failed to sign: %s\n", err)
+		return
+	}
+	log(evt.String() + "\n")
+
+	log("publishing to %d relays... ", len(relays))
+	success := false
+	first := true
+	for res := range n.root.sys.Pool.PublishMany(n.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 {
+		n.RmChild("new")
+		n.AddChild(evt.ID, n.root.CreateEventDir(n, &evt), true)
+		log("event published as %s and updated locally.\n", color.BlueString(evt.ID))
+	}
+}
+
 func (n *ViewDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
 	now := nostr.Now()
 	if n.filter.Until != nil {
diff --git a/nostrfs/writeablefile.go b/nostrfs/writeablefile.go
index c37291a..b6ca0a9 100644
--- a/nostrfs/writeablefile.go
+++ b/nostrfs/writeablefile.go
@@ -71,7 +71,10 @@ func (f *WriteableFile) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse
 	return fs.OK
 }
 
-func (f *WriteableFile) Setattr(_ context.Context, _ fs.FileHandle, _ *fuse.SetAttrIn, _ *fuse.AttrOut) syscall.Errno {
+func (f *WriteableFile) Setattr(_ context.Context, _ fs.FileHandle, in *fuse.SetAttrIn, _ *fuse.AttrOut) syscall.Errno {
+	f.attr.Mtime = in.Mtime
+	f.attr.Atime = in.Atime
+	f.attr.Ctime = in.Ctime
 	return fs.OK
 }